diff --git a/.dockerignore b/.dockerignore index ec5b9bd..a811162 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,12 @@ -/public/bundles/ -/var/ +var/ +vendor/ .phpunit.result.cache -/phpunit.xml +phpunit.xml -/phpunit.xml -.phpunit.result.cache +.php-cs-fixer.php +.php-cs-fixer.cache -/.php-cs-fixer.php -/.php-cs-fixer.cache .git/ .github/ .vscode/ diff --git a/.env b/.env index fbd996b..ce2bdc4 100644 --- a/.env +++ b/.env @@ -35,7 +35,6 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###< nelmio/cors-bundle ### ###> doctrineencryptbundle/doctrine-encrypt-bundle ### -DOCTRINE_ENCRYPT_KEY="" CRYPTO_SECRET="" ###< doctrineencryptbundle/doctrine-encrypt-bundle ### @@ -52,6 +51,10 @@ PAYPAL_CLIENT_SECRET="" PAYPAL_WEBHOOK_ID="WEBHOOK_ID" ###< App\Gateway\Paypal\PaypalGateway ### +###> App\Money\MoneyService ### +CURRENCY_DEFAULT="EUR" +###< App\Money\MoneyService ### + ###> symfony/lock ### # Choose one of the stores below # postgresql+advisory://db_user:db_password@localhost/db_name diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0a3fb51..66219dc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,7 +25,7 @@ jobs: with: push: true context: . - file: docker/php/Dockerfile + file: docker/dev/php/Dockerfile target: prod tags: ${{ secrets.REGISTRY_URL }}/goteo-v4:latest diff --git a/captain-definition b/captain-definition new file mode 100644 index 0000000..414182a --- /dev/null +++ b/captain-definition @@ -0,0 +1,4 @@ +{ + "schemaVersion": 2, + "dockerfilePath": "./docker/main/Dockerfile" +} diff --git a/composer.json b/composer.json index 2b4a63f..cb4fe69 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", "doctrineencryptbundle/doctrine-encrypt-bundle": "^5.4", - "goteo/benzina": "^0.4", + "goteo/benzina": "^0.5", "jolicode/automapper": "^9.2", "mpratt/embera": "~2.0", "nelmio/cors-bundle": "^2.3", @@ -36,6 +36,7 @@ "symfony/framework-bundle": "6.4.*", "symfony/http-client": "6.4.*", "symfony/intl": "6.4.*", + "symfony/maker-bundle": "^1.62", "symfony/monolog-bundle": "^3.10", "symfony/property-access": "6.4.*", "symfony/property-info": "6.4.*", @@ -44,7 +45,8 @@ "symfony/serializer": "6.4.*", "symfony/twig-bundle": "6.4.*", "symfony/validator": "6.4.*", - "symfony/yaml": "6.4.*" + "symfony/yaml": "6.4.*", + "zenstruck/foundry": "^2.4" }, "config": { "allow-plugins": { @@ -99,8 +101,6 @@ "phpunit/phpunit": "^9.5", "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", - "symfony/maker-bundle": "^1.62", - "symfony/phpunit-bridge": "^7.2", - "zenstruck/foundry": "^2.4" + "symfony/phpunit-bridge": "^7.2" } } diff --git a/composer.lock b/composer.lock index 9526179..3b63876 100644 --- a/composer.lock +++ b/composer.lock @@ -4,28 +4,28 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6066bdd681ac8f5f7b3d2d8ae4e72149", + "content-hash": "230c17be8429a245ab35233cc148b538", "packages": [ { "name": "api-platform/doctrine-common", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "c706b23d51936ff45b45f549b81861391f967f90" + "reference": "e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/c706b23d51936ff45b45f549b81861391f967f90", - "reference": "c706b23d51936ff45b45f549b81861391f967f90", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7", + "reference": "e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "doctrine/collections": "^2.1", "doctrine/common": "^3.2.2", - "doctrine/persistence": "^3.2", + "doctrine/persistence": "^3.2 || ^4.0", "php": ">=8.2" }, "conflict": { @@ -35,7 +35,7 @@ "doctrine/mongodb-odm": "^2.10", "doctrine/orm": "^2.17 || ^3.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "suggest": { "api-platform/graphql": "For GraphQl mercure subscriptions.", @@ -56,6 +56,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -89,28 +90,28 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.1.7" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-08-18T13:30:43+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "4cfd9b739e82c6fb2c6f9e9c7ac2c42a1bc589d2" + "reference": "61a199da6f6014dba2da43ea1a66b2c9dda27263" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/4cfd9b739e82c6fb2c6f9e9c7ac2c42a1bc589d2", - "reference": "4cfd9b739e82c6fb2c6f9e9c7ac2c42a1bc589d2", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/61a199da6f6014dba2da43ea1a66b2c9dda27263", + "reference": "61a199da6f6014dba2da43ea1a66b2c9dda27263", "shasum": "" }, "require": { - "api-platform/doctrine-common": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/doctrine-common": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "doctrine/orm": "^2.17 || ^3.0", "php": ">=8.2", "symfony/property-info": "^6.4 || ^7.1" @@ -118,7 +119,7 @@ "require-dev": { "doctrine/doctrine-bundle": "^2.11", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "ramsey/uuid": "^4.7", "ramsey/uuid-doctrine": "^2.0", "symfony/cache": "^6.4 || ^7.0", @@ -140,6 +141,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -173,30 +175,30 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.1.7" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-06-06T14:56:47+00:00" }, { "name": "api-platform/documentation", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", - "reference": "20334b3b29dca992e3520685f33f75f7fcba8da6" + "reference": "1a0ac988d659008ef8667d05bc9978863026bab8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/documentation/zipball/20334b3b29dca992e3520685f33f75f7fcba8da6", - "reference": "20334b3b29dca992e3520685f33f75f7fcba8da6", + "url": "https://api.github.com/repos/api-platform/documentation/zipball/1a0ac988d659008ef8667d05bc9978863026bab8", + "reference": "1a0ac988d659008ef8667d05bc9978863026bab8", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.11", "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "project", "extra": { @@ -209,6 +211,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -234,34 +237,34 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.1.7" + "source": "https://github.com/api-platform/documentation/tree/v4.2.0-alpha.1" }, - "time": "2025-02-28T10:08:08+00:00" + "time": "2025-06-06T14:56:47+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", - "reference": "2eff0c4ae6320426586db4ce5dcd730f51665ea5" + "reference": "f65f092c90311a87ebb6dda87db3ca08b57c10d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/http-cache/zipball/2eff0c4ae6320426586db4ce5dcd730f51665ea5", - "reference": "2eff0c4ae6320426586db4ce5dcd730f51665ea5", + "url": "https://api.github.com/repos/api-platform/http-cache/zipball/f65f092c90311a87ebb6dda87db3ca08b57c10d6", + "reference": "f65f092c90311a87ebb6dda87db3ca08b57c10d6", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/http-foundation": "^6.4 || ^7.0" }, "require-dev": { "guzzlehttp/guzzle": "^6.0 || ^7.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/dependency-injection": "^6.4 || ^7.0", "symfony/http-client": "^6.4 || ^7.0" }, @@ -276,6 +279,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -311,31 +315,31 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.1.7" + "source": "https://github.com/api-platform/http-cache/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-06-06T14:56:47+00:00" }, { "name": "api-platform/hydra", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", - "reference": "f05dbc5ed805859ef4335fd844493b10fd857046" + "reference": "8c75b814af143c95ffc1857565169ff5b6f1b421" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/f05dbc5ed805859ef4335fd844493b10fd857046", - "reference": "f05dbc5ed805859ef4335fd844493b10fd857046", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/8c75b814af143c95ffc1857565169ff5b6f1b421", + "reference": "8c75b814af143c95ffc1857565169ff5b6f1b421", "shasum": "" }, "require": { - "api-platform/documentation": "^4.1", - "api-platform/json-schema": "^4.1", - "api-platform/jsonld": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/serializer": "^4.1", - "api-platform/state": "^4.1", + "api-platform/documentation": "^4.1.11", + "api-platform/json-schema": "^4.1.11", + "api-platform/jsonld": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/web-link": "^6.4 || ^7.1" }, @@ -345,7 +349,7 @@ "api-platform/doctrine-orm": "^4.1", "phpspec/prophecy": "^1.19", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -358,6 +362,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -395,26 +400,26 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.1.7" + "source": "https://github.com/api-platform/hydra/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-15T14:10:59+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "32bf153319c1d4778f6daafe00d636508ee2c4d9" + "reference": "1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/32bf153319c1d4778f6daafe00d636508ee2c4d9", - "reference": "32bf153319c1d4778f6daafe00d636508ee2c4d9", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f", + "reference": "1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.11", "php": ">=8.2", "symfony/console": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.1", @@ -423,7 +428,7 @@ }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -436,6 +441,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -473,32 +479,32 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/json-schema/tree/v4.1.7" + "source": "https://github.com/api-platform/json-schema/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-06-29T12:24:14+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", - "reference": "ef2112bd65a49761efe4253bbf1ba5328e307e00" + "reference": "e122bf1f04f895e80e6469e0f09d1f06f7508ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/jsonld/zipball/ef2112bd65a49761efe4253bbf1ba5328e307e00", - "reference": "ef2112bd65a49761efe4253bbf1ba5328e307e00", + "url": "https://api.github.com/repos/api-platform/jsonld/zipball/e122bf1f04f895e80e6469e0f09d1f06f7508ca6", + "reference": "e122bf1f04f895e80e6469e0f09d1f06f7508ca6", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/serializer": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -511,6 +517,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -550,40 +557,40 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.1.7" + "source": "https://github.com/api-platform/jsonld/tree/v4.1.23" }, - "time": "2025-04-10T12:08:09+00:00" + "time": "2025-07-25T10:05:30+00:00" }, { "name": "api-platform/metadata", - "version": "v4.1.7", + "version": "v4.1.18", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", - "reference": "8adff3619c680ff4cb69b77228ac62dd4e4e1cc3" + "reference": "5df40a760eef56963c151181dc9498bbc260e7ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/8adff3619c680ff4cb69b77228ac62dd4e4e1cc3", - "reference": "8adff3619c680ff4cb69b77228ac62dd4e4e1cc3", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/5df40a760eef56963c151181dc9498bbc260e7ae", + "reference": "5df40a760eef56963c151181dc9498bbc260e7ae", "shasum": "" }, "require": { - "doctrine/inflector": "^1.0 || ^2.0", + "doctrine/inflector": "^2.0", "php": ">=8.2", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/property-info": "^6.4 || ^7.1", "symfony/string": "^6.4 || ^7.0", - "symfony/type-info": "^7.1" + "symfony/type-info": "^7.2" }, "require-dev": { - "api-platform/json-schema": "^4.1", - "api-platform/openapi": "^4.1", - "api-platform/state": "^4.1", + "api-platform/json-schema": "^4.1.11", + "api-platform/openapi": "^4.1.11", + "api-platform/state": "^4.1.11", "phpspec/prophecy-phpunit": "^2.2", "phpstan/phpdoc-parser": "^1.29 || ^2.0", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/config": "^6.4 || ^7.0", "symfony/routing": "^6.4 || ^7.0", "symfony/var-dumper": "^6.4 || ^7.0", @@ -606,6 +613,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -646,28 +654,28 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.1.7" + "source": "https://github.com/api-platform/metadata/tree/v4.1.18" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-03T13:06:55+00:00" }, { "name": "api-platform/openapi", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "32e4591d18b19401f9c0aeb0965ffb178938b978" + "reference": "793b53e51a5c24076d4024b6aa77de29e74015cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/32e4591d18b19401f9c0aeb0965ffb178938b978", - "reference": "32e4591d18b19401f9c0aeb0965ffb178938b978", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/793b53e51a5c24076d4024b6aa77de29e74015cd", + "reference": "793b53e51a5c24076d4024b6aa77de29e74015cd", "shasum": "" }, "require": { - "api-platform/json-schema": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/json-schema": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/console": "^6.4 || ^7.0", "symfony/filesystem": "^6.4 || ^7.0", @@ -679,7 +687,7 @@ "api-platform/doctrine-odm": "^4.1", "api-platform/doctrine-orm": "^4.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -692,6 +700,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -732,27 +741,27 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.1.7" + "source": "https://github.com/api-platform/openapi/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-29T08:53:27+00:00" }, { "name": "api-platform/serializer", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "132bca30db7f0074e93e36c48af2f13f33d91958" + "reference": "70dbdeac9584870be444d78c1a796b6edb9e46a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/132bca30db7f0074e93e36c48af2f13f33d91958", - "reference": "132bca30db7f0074e93e36c48af2f13f33d91958", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/70dbdeac9584870be444d78c1a796b6edb9e46a5", + "reference": "70dbdeac9584870be444d78c1a796b6edb9e46a5", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", - "api-platform/state": "^4.1", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "php": ">=8.2", "symfony/property-access": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.1", @@ -767,7 +776,7 @@ "api-platform/openapi": "^4.1", "doctrine/collections": "^2.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/mercure-bundle": "*", "symfony/var-dumper": "^6.4 || ^7.0", "symfony/yaml": "^6.4 || ^7.0" @@ -787,6 +796,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -822,34 +832,35 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.1.7" + "source": "https://github.com/api-platform/serializer/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-08-29T15:13:26+00:00" }, { "name": "api-platform/state", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "ddae2e24bdfc83b69460662581aaa9f095523a0d" + "reference": "056b07285cdc904984fb44c2614f7df8f4620a95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/ddae2e24bdfc83b69460662581aaa9f095523a0d", - "reference": "ddae2e24bdfc83b69460662581aaa9f095523a0d", + "url": "https://api.github.com/repos/api-platform/state/zipball/056b07285cdc904984fb44c2614f7df8f4620a95", + "reference": "056b07285cdc904984fb44c2614f7df8f4620a95", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.18", "php": ">=8.2", "psr/container": "^1.0 || ^2.0", - "symfony/http-kernel": "^6.4 || ^7.0" + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", + "symfony/translation-contracts": "^3.0" }, "require-dev": { - "api-platform/serializer": "^4.1", "api-platform/validator": "^4.1", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/http-foundation": "^6.4 || ^7.0", "symfony/web-link": "^6.4 || ^7.1", "willdurand/negotiation": "^3.1" @@ -872,6 +883,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -912,35 +924,35 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.1.7" + "source": "https://github.com/api-platform/state/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-16T14:01:52+00:00" }, { "name": "api-platform/symfony", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "8e295045e1c036efaa7bf64213de90365139e68c" + "reference": "e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/8e295045e1c036efaa7bf64213de90365139e68c", - "reference": "8e295045e1c036efaa7bf64213de90365139e68c", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1", + "reference": "e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1", "shasum": "" }, "require": { - "api-platform/documentation": "^4.1", - "api-platform/http-cache": "^4.1", - "api-platform/hydra": "^4.1", - "api-platform/json-schema": "^4.1", - "api-platform/jsonld": "^4.1", - "api-platform/metadata": "^4.1", - "api-platform/openapi": "^4.1", - "api-platform/serializer": "^4.1", - "api-platform/state": "^4.1", - "api-platform/validator": "^4.1", + "api-platform/documentation": "^4.1.11", + "api-platform/http-cache": "^4.1.11", + "api-platform/hydra": "^4.1.11", + "api-platform/json-schema": "^4.1.11", + "api-platform/jsonld": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/openapi": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", + "api-platform/validator": "^4.1.11", "php": ">=8.2", "symfony/property-access": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.1", @@ -956,7 +968,7 @@ "api-platform/graphql": "^4.1", "api-platform/parameter-validator": "^3.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/expression-language": "^6.4 || ^7.0", "symfony/mercure-bundle": "*", "symfony/routing": "^6.4 || ^7.0", @@ -994,6 +1006,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -1035,35 +1048,36 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.1.7" + "source": "https://github.com/api-platform/symfony/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-09-05T07:30:37+00:00" }, { "name": "api-platform/validator", - "version": "v4.1.7", + "version": "v4.1.23", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", - "reference": "a7052710c60ca2c85a10b3e1d841f71d3e316fa8" + "reference": "9f0bde95dccf1d86e6a6165543d601a4a46eaa9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/validator/zipball/a7052710c60ca2c85a10b3e1d841f71d3e316fa8", - "reference": "a7052710c60ca2c85a10b3e1d841f71d3e316fa8", + "url": "https://api.github.com/repos/api-platform/validator/zipball/9f0bde95dccf1d86e6a6165543d601a4a46eaa9a", + "reference": "9f0bde95dccf1d86e6a6165543d601a4a46eaa9a", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1", + "api-platform/metadata": "^4.1.11", "php": ">=8.2", + "symfony/http-kernel": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/type-info": "^7.2", + "symfony/validator": "^6.4 || ^7.1", "symfony/web-link": "^6.4 || ^7.1" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0" + "phpunit/phpunit": "11.5.x-dev" }, "type": "library", "extra": { @@ -1076,6 +1090,7 @@ }, "branch-alias": { "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", "dev-main": "4.2.x-dev" } }, @@ -1108,9 +1123,9 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.1.7" + "source": "https://github.com/api-platform/validator/tree/v4.1.23" }, - "time": "2025-04-11T09:32:56+00:00" + "time": "2025-07-16T14:01:52+00:00" }, { "name": "beberlei/doctrineextensions", @@ -1497,99 +1512,6 @@ }, "time": "2024-09-05T10:17:24+00:00" }, - { - "name": "doctrine/cache", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", - "shasum": "" - }, - "require": { - "php": "~7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" - }, - "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", - "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" - ], - "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/2.2.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", - "type": "tidelift" - } - ], - "time": "2022-05-20T20:07:39+00:00" - }, { "name": "doctrine/collections", "version": "2.3.0", @@ -1769,36 +1691,39 @@ }, { "name": "doctrine/dbal", - "version": "3.9.4", + "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959" + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c6c16cf787eaba3112203dfcd715fa2059c62282", + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/deprecations": "^0.5.3|^1", "doctrine/event-manager": "^1|^2", "php": "^7.4 || ^8.0", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, + "conflict": { + "doctrine/cache": "< 1.11" + }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "13.0.1", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.1", + "phpstan/phpstan": "2.1.22", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.22", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0" }, @@ -1860,7 +1785,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.4" + "source": "https://github.com/doctrine/dbal/tree/3.10.2" }, "funding": [ { @@ -1876,7 +1801,7 @@ "type": "tidelift" } ], - "time": "2025-01-16T08:28:55+00:00" + "time": "2025-09-04T23:51:27+00:00" }, { "name": "doctrine/deprecations", @@ -2227,33 +2152,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2298,7 +2222,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -2314,7 +2238,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/instantiator", @@ -2568,16 +2492,16 @@ }, { "name": "doctrine/orm", - "version": "3.3.2", + "version": "3.5.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "c9557c588b3a70ed93caff069d0aa75737f25609" + "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/c9557c588b3a70ed93caff069d0aa75737f25609", - "reference": "c9557c588b3a70ed93caff069d0aa75737f25609", + "url": "https://api.github.com/repos/doctrine/orm/zipball/5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", + "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", "shasum": "" }, "require": { @@ -2597,7 +2521,7 @@ "symfony/var-exporter": "^6.3.9 || ^7.0" }, "require-dev": { - "doctrine/coding-standard": "^12.0", + "doctrine/coding-standard": "^13.0", "phpbench/phpbench": "^1.0", "phpdocumentor/guides-cli": "^1.4", "phpstan/extension-installer": "^1.4", @@ -2605,7 +2529,7 @@ "phpstan/phpstan-deprecation-rules": "^2", "phpunit/phpunit": "^10.4.0", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.7.2", + "squizlabs/php_codesniffer": "3.12.0", "symfony/cache": "^5.4 || ^6.2 || ^7.0" }, "suggest": { @@ -2652,40 +2576,37 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.3.2" + "source": "https://github.com/doctrine/orm/tree/3.5.2" }, - "time": "2025-02-04T19:43:15+00:00" + "time": "2025-08-08T17:00:40+00:00" }, { "name": "doctrine/persistence", - "version": "3.4.0", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff" + "reference": "dcbdfe4b211ae09478e192289cae7ab0987b29a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/0ea965320cec355dba75031c1b23d4c78362e3ff", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/dcbdfe4b211ae09478e192289cae7ab0987b29a4", + "reference": "dcbdfe4b211ae09478e192289cae7ab0987b29a4", "shasum": "" }, "require": { "doctrine/event-manager": "^1 || ^2", - "php": "^7.2 || ^8.0", + "php": "^8.1", "psr/cache": "^1.0 || ^2.0 || ^3.0" }, - "conflict": { - "doctrine/common": "<2.10" - }, "require-dev": { "doctrine/coding-standard": "^12", - "doctrine/common": "^3.0", "phpstan/phpstan": "1.12.7", "phpstan/phpstan-phpunit": "^1", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5.38 || ^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0" }, "type": "library", "autoload": { @@ -2734,7 +2655,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.4.0" + "source": "https://github.com/doctrine/persistence/tree/4.1.0" }, "funding": [ { @@ -2750,7 +2671,7 @@ "type": "tidelift" } ], - "time": "2024-10-30T19:48:12+00:00" + "time": "2025-08-21T16:00:31+00:00" }, { "name": "doctrine/sql-formatter", @@ -2876,6 +2797,69 @@ }, "time": "2024-12-14T10:54:02+00:00" }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, { "name": "gedmo/doctrine-extensions", "version": "v3.20.0", @@ -3008,22 +2992,23 @@ }, { "name": "goteo/benzina", - "version": "0.4", + "version": "0.5", "source": { "type": "git", "url": "https://github.com/goteo/benzina.git", - "reference": "ebb3ee783e3a4195f61d648cb5937e8dd9eab6ae" + "reference": "9422fcbce6d1f9d7fb266bf5fc0cd1a44c0adf62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/goteo/benzina/zipball/ebb3ee783e3a4195f61d648cb5937e8dd9eab6ae", - "reference": "ebb3ee783e3a4195f61d648cb5937e8dd9eab6ae", + "url": "https://api.github.com/repos/goteo/benzina/zipball/9422fcbce6d1f9d7fb266bf5fc0cd1a44c0adf62", + "reference": "9422fcbce6d1f9d7fb266bf5fc0cd1a44c0adf62", "shasum": "" }, "require": { "doctrine/orm": "^3.3", "ext-ctype": "*", "ext-iconv": "*", + "ocramius/doctrine-batch-utils": "^2.10", "php": ">=8.1", "symfony/console": "6.4.*", "symfony/dotenv": "6.4.*", @@ -3062,9 +3047,9 @@ "description": "Migration tool to pump v3 data into a v4 schema", "support": { "issues": "https://github.com/goteo/benzina/issues", - "source": "https://github.com/goteo/benzina/tree/0.4" + "source": "https://github.com/goteo/benzina/tree/0.5" }, - "time": "2025-02-27T11:20:31+00:00" + "time": "2025-09-08T10:27:32+00:00" }, { "name": "jolicode/automapper", @@ -3446,6 +3431,70 @@ }, "time": "2024-12-30T11:07:19+00:00" }, + { + "name": "ocramius/doctrine-batch-utils", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/DoctrineBatchUtils.git", + "reference": "35b164ea216da00ecccde6a0152f8be67bfb841f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/DoctrineBatchUtils/zipball/35b164ea216da00ecccde6a0152f8be67bfb841f", + "reference": "35b164ea216da00ecccde6a0152f8be67bfb841f", + "shasum": "" + }, + "require": { + "doctrine/orm": "^3.3.2", + "doctrine/persistence": "^4.0.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "doctrine/dbal": "^4.2.2", + "phpunit/phpunit": "^11.5.6", + "psalm/plugin-phpunit": "^0.19.2", + "roave/infection-static-analysis-plugin": "^1.36.0", + "vimeo/psalm": "^6.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "DoctrineBatchUtils\\": "src/DoctrineBatchUtils" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.io/" + } + ], + "description": "A set of utilities to operate with Doctrine ORM's batch processing functionality", + "homepage": "https://github.com/Ocramius/DoctrineBatchUtils", + "keywords": [ + "batch processing", + "doctrine", + "doctrine2", + "orm" + ], + "support": { + "issues": "https://github.com/Ocramius/DoctrineBatchUtils/issues", + "source": "https://github.com/Ocramius/DoctrineBatchUtils/tree/2.11.0" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + } + ], + "time": "2025-02-05T12:04:35+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v3.0.0", @@ -4567,16 +4616,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.21", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee" + "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/d1abcf763a7414f2e572f676f22da7a06c8cd9ee", - "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee", + "url": "https://api.github.com/repos/symfony/cache/zipball/d038cd3054aeaf1c674022a77048b2ef6376a175", + "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175", "shasum": "" }, "require": { @@ -4643,7 +4692,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.21" + "source": "https://github.com/symfony/cache/tree/v6.4.24" }, "funding": [ { @@ -4654,25 +4703,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-08T08:21:20+00:00" + "time": "2025-07-30T09:32:03+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", - "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", "shasum": "" }, "require": { @@ -4686,7 +4739,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4719,7 +4772,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" }, "funding": [ { @@ -4735,7 +4788,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-03-13T15:25:07+00:00" }, { "name": "symfony/clock", @@ -4813,16 +4866,16 @@ }, { "name": "symfony/config", - "version": "v6.4.14", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef" + "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4e55e7e4ffddd343671ea972216d4509f46c22ef", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef", + "url": "https://api.github.com/repos/symfony/config/zipball/80e2cf005cf17138c97193be0434cdcfd1b2212e", + "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e", "shasum": "" }, "require": { @@ -4868,7 +4921,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.14" + "source": "https://github.com/symfony/config/tree/v6.4.24" }, "funding": [ { @@ -4879,25 +4932,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-04T11:33:53+00:00" + "time": "2025-07-26T13:50:30+00:00" }, { "name": "symfony/console", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719" + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a3011c7b7adb58d89f6c0d822abb641d7a5f9719", - "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719", + "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", "shasum": "" }, "require": { @@ -4962,7 +5019,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.21" + "source": "https://github.com/symfony/console/tree/v6.4.25" }, "funding": [ { @@ -4973,25 +5030,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-07T15:42:41+00:00" + "time": "2025-08-22T10:21:53+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.20", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "c49796a9184a532843e78e50df9e55708b92543a" + "reference": "900da8a42eceeb4a13a0ec34caa7db49328daff3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c49796a9184a532843e78e50df9e55708b92543a", - "reference": "c49796a9184a532843e78e50df9e55708b92543a", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/900da8a42eceeb4a13a0ec34caa7db49328daff3", + "reference": "900da8a42eceeb4a13a0ec34caa7db49328daff3", "shasum": "" }, "require": { @@ -5043,7 +5104,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.20" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.25" }, "funding": [ { @@ -5054,25 +5115,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-13T09:55:08+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -5085,7 +5150,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5110,7 +5175,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -5126,7 +5191,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/doctrine-bridge", @@ -5238,16 +5303,16 @@ }, { "name": "symfony/dotenv", - "version": "v6.4.16", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5" + "reference": "234b6c602f12b00693f4b0d1054386fb30dfc8ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/1ac5e7e7e862d4d574258daf08bd569ba926e4a5", - "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/234b6c602f12b00693f4b0d1054386fb30dfc8ff", + "reference": "234b6c602f12b00693f4b0d1054386fb30dfc8ff", "shasum": "" }, "require": { @@ -5292,7 +5357,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.16" + "source": "https://github.com/symfony/dotenv/tree/v6.4.24" }, "funding": [ { @@ -5303,25 +5368,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-27T11:08:19+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/error-handler", - "version": "v6.4.20", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031" + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/aa3bcf4f7674719df078e61cc8062e5b7f752031", - "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/30fd0b3cf0e972e82636038ce4db0e4fe777112c", + "reference": "30fd0b3cf0e972e82636038ce4db0e4fe777112c", "shasum": "" }, "require": { @@ -5367,7 +5436,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.20" + "source": "https://github.com/symfony/error-handler/tree/v6.4.24" }, "funding": [ { @@ -5378,25 +5447,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-01T13:00:38+00:00" + "time": "2025-07-24T08:25:04+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.13", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + "reference": "b0cf3162020603587363f0551cd3be43958611ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff", + "reference": "b0cf3162020603587363f0551cd3be43958611ff", "shasum": "" }, "require": { @@ -5447,7 +5520,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25" }, "funding": [ { @@ -5458,25 +5531,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -5490,7 +5567,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5523,7 +5600,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -5539,7 +5616,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/expression-language", @@ -5607,16 +5684,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", "shasum": "" }, "require": { @@ -5653,7 +5730,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.4.24" }, "funding": [ { @@ -5664,25 +5741,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/finder", - "version": "v6.4.17", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" + "reference": "73089124388c8510efb8d2d1689285d285937b08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", "shasum": "" }, "require": { @@ -5717,7 +5798,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.17" + "source": "https://github.com/symfony/finder/tree/v6.4.24" }, "funding": [ { @@ -5728,25 +5809,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-29T13:51:37+00:00" + "time": "2025-07-15T12:02:45+00:00" }, { "name": "symfony/flex", - "version": "v2.5.0", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018" + "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/8ce1acd9842abe0e9b4c4a0bd3f259859516c018", - "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "url": "https://api.github.com/repos/symfony/flex/zipball/f356aa35f3cf3d2f46c31d344c1098eb2d260426", + "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426", "shasum": "" }, "require": { @@ -5785,7 +5870,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.5.0" + "source": "https://github.com/symfony/flex/tree/v2.8.2" }, "funding": [ { @@ -5796,25 +5881,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-03T07:50:46+00:00" + "time": "2025-08-22T07:17:23+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a" + "reference": "1d6a764b58e4f780df00f71c20ba3a61095ea447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a", - "reference": "d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/1d6a764b58e4f780df00f71c20ba3a61095ea447", + "reference": "1d6a764b58e4f780df00f71c20ba3a61095ea447", "shasum": "" }, "require": { @@ -5934,7 +6023,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.21" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.25" }, "funding": [ { @@ -5945,12 +6034,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T13:27:38+00:00" + "time": "2025-08-26T10:44:20+00:00" }, { "name": "symfony/http-client", @@ -6125,16 +6218,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99" + "reference": "6bc974c0035b643aa497c58d46d9e25185e4b272" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3f0c7ea41db479383b81d436b836d37168fd5b99", - "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6bc974c0035b643aa497c58d46d9e25185e4b272", + "reference": "6bc974c0035b643aa497c58d46d9e25185e4b272", "shasum": "" }, "require": { @@ -6182,7 +6275,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.21" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.25" }, "funding": [ { @@ -6193,25 +6286,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T13:27:38+00:00" + "time": "2025-08-20T06:48:20+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c" + "reference": "a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/983ca05eec6623920d24ec0f1005f487d3734a0c", - "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15", + "reference": "a0ee3cea5cabf4ed960fd2ef57668ceeacdb6e15", "shasum": "" }, "require": { @@ -6296,7 +6393,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.21" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.25" }, "funding": [ { @@ -6307,12 +6404,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-02T08:46:38+00:00" + "time": "2025-08-29T07:55:45+00:00" }, { "name": "symfony/intl", @@ -6476,6 +6577,98 @@ ], "time": "2024-10-25T15:19:46+00:00" }, + { + "name": "symfony/maker-bundle", + "version": "v1.63.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-26T01:41:37+00:00" + }, { "name": "symfony/monolog-bridge", "version": "v6.4.13", @@ -6710,16 +6903,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -6768,7 +6961,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -6779,16 +6972,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -6849,7 +7046,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -6860,6 +7057,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6869,7 +7070,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -6930,7 +7131,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -6941,6 +7142,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6950,16 +7155,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -7006,7 +7211,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -7017,25 +7222,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "000df7860439609837bbe28670b0be15783b7fbf" + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", - "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "shasum": "" }, "require": { @@ -7082,7 +7291,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" }, "funding": [ { @@ -7093,12 +7302,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-20T12:04:08+00:00" + "time": "2025-06-24T13:30:11+00:00" }, { "name": "symfony/polyfill-uuid", @@ -7179,6 +7392,71 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/process", + "version": "v6.4.25", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", + "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-14T06:23:17+00:00" + }, { "name": "symfony/property-access", "version": "v6.4.18", @@ -7344,16 +7622,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.18", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68" + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/e9bfc94953019089acdfb9be51c1b9142c4afa68", - "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68", + "url": "https://api.github.com/repos/symfony/routing/zipball/e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5", "shasum": "" }, "require": { @@ -7407,7 +7685,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.18" + "source": "https://github.com/symfony/routing/tree/v6.4.24" }, "funding": [ { @@ -7418,25 +7696,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-01-09T08:51:02+00:00" + "time": "2025-07-15T08:46:37+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.14", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "4facd4174f45cd37c65860403412b67c7381136a" + "reference": "c1cc6721646f546627236c57f835272806087337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/4facd4174f45cd37c65860403412b67c7381136a", - "reference": "4facd4174f45cd37c65860403412b67c7381136a", + "url": "https://api.github.com/repos/symfony/runtime/zipball/c1cc6721646f546627236c57f835272806087337", + "reference": "c1cc6721646f546627236c57f835272806087337", "shasum": "" }, "require": { @@ -7486,7 +7768,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.14" + "source": "https://github.com/symfony/runtime/tree/v6.4.24" }, "funding": [ { @@ -7497,12 +7779,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-05T16:39:55+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/security-bundle", @@ -7958,16 +8244,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -7985,7 +8271,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -8021,7 +8307,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -8037,20 +8323,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.19", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c" + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", - "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b67e94e06a05d9572c2fa354483b3e13e3cb1898", + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898", "shasum": "" }, "require": { @@ -8083,7 +8369,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.19" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.24" }, "funding": [ { @@ -8094,25 +8380,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-21T10:06:30+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/string", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" + "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", + "url": "https://api.github.com/repos/symfony/string/zipball/7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", + "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", "shasum": "" }, "require": { @@ -8169,7 +8459,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.21" + "source": "https://github.com/symfony/string/tree/v6.4.25" }, "funding": [ { @@ -8180,12 +8470,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-18T15:23:29+00:00" + "time": "2025-08-22T12:33:20+00:00" }, { "name": "symfony/translation-contracts", @@ -8706,16 +9000,16 @@ }, { "name": "symfony/var-dumper", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" + "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6cd92486e9fc32506370822c57bc02353a5a92c", + "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c", "shasum": "" }, "require": { @@ -8727,7 +9021,6 @@ "symfony/console": "<5.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^5.4|^6.0|^7.0", "symfony/error-handler": "^6.3|^7.0", "symfony/http-kernel": "^5.4|^6.0|^7.0", @@ -8771,7 +9064,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.25" }, "funding": [ { @@ -8782,25 +9075,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-09T07:34:50+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "717e7544aa99752c54ecba5c0e17459c48317472" + "reference": "4ff50a1b7c75d1d596aca50899d0c8c7e3de8358" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/717e7544aa99752c54ecba5c0e17459c48317472", - "reference": "717e7544aa99752c54ecba5c0e17459c48317472", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/4ff50a1b7c75d1d596aca50899d0c8c7e3de8358", + "reference": "4ff50a1b7c75d1d596aca50899d0c8c7e3de8358", "shasum": "" }, "require": { @@ -8848,7 +9145,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.21" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.25" }, "funding": [ { @@ -8859,12 +9156,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T21:06:26+00:00" + "time": "2025-08-18T13:06:32+00:00" }, { "name": "symfony/web-link", @@ -8951,16 +9252,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e" + "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f01987f45676778b474468aa266fe2eda1f2bc7e", - "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e54b060bc9c3dc3d4258bf0d165d0064e755f565", + "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565", "shasum": "" }, "require": { @@ -9003,7 +9304,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.21" + "source": "https://github.com/symfony/yaml/tree/v6.4.25" }, "funding": [ { @@ -9014,12 +9315,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T09:48:44+00:00" + "time": "2025-08-26T16:59:00+00:00" }, { "name": "twig/twig", @@ -9213,6 +9518,175 @@ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" }, "time": "2022-01-30T20:08:53+00:00" + }, + { + "name": "zenstruck/assert", + "version": "v1.5.1", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/assert.git", + "reference": "39554ce3a275fbf8c870b251e620101f644e9277" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/assert/zipball/39554ce3a275fbf8c870b251e620101f644e9277", + "reference": "39554ce3a275fbf8c870b251e620101f644e9277", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5.21", + "symfony/phpunit-bridge": "^6.3|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Zenstruck\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "Standalone, lightweight, framework agnostic, test assertion library.", + "homepage": "https://github.com/zenstruck/assert", + "keywords": [ + "assertion", + "phpunit", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/assert/issues", + "source": "https://github.com/zenstruck/assert/tree/v1.5.1" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2024-10-28T18:08:12+00:00" + }, + { + "name": "zenstruck/foundry", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/foundry.git", + "reference": "830d5a5243b5b81e79b2b91b67654836490ed833" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/foundry/zipball/830d5a5243b5b81e79b2b91b67654836490ed833", + "reference": "830d5a5243b5b81e79b2b91b67654836490ed833", + "shasum": "" + }, + "require": { + "fakerphp/faker": "^1.23", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", + "zenstruck/assert": "^1.4" + }, + "conflict": { + "doctrine/persistence": "<2.0", + "symfony/framework-bundle": "<6.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8", + "brianium/paratest": "^6|^7", + "dama/doctrine-test-bundle": "^8.0", + "doctrine/collections": "^1.7|^2.0", + "doctrine/common": "^3.2.2", + "doctrine/doctrine-bundle": "^2.10", + "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", + "doctrine/mongodb-odm": "^2.4", + "doctrine/mongodb-odm-bundle": "^4.6|^5.0", + "doctrine/orm": "^2.16|^3.0", + "doctrine/persistence": "^2.0|^3.0|^4.0", + "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0", + "symfony/console": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/maker-bundle": "^1.55", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/runtime": "^6.4|^7.0", + "symfony/translation-contracts": "^3.4", + "symfony/uid": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin" + }, + "bamarni-bin": { + "bin-links": true, + "forward-command": false, + "target-directory": "bin/tools" + } + }, + "autoload": { + "files": [ + "src/functions.php", + "src/Persistence/functions.php", + "src/symfony_console.php" + ], + "psr-4": { + "Zenstruck\\Foundry\\": "src/", + "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + }, + { + "name": "Nicolas PHILIPPE", + "email": "nikophil@gmail.com" + } + ], + "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.", + "homepage": "https://github.com/zenstruck/foundry", + "keywords": [ + "Fixture", + "dev", + "doctrine", + "factory", + "faker", + "symfony", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/foundry/issues", + "source": "https://github.com/zenstruck/foundry/tree/v2.6.0" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2025-06-05T08:34:41+00:00" } ], "packages-dev": [ @@ -9472,69 +9946,6 @@ }, "time": "2023-08-08T05:53:35+00:00" }, - { - "name": "fakerphp/faker", - "version": "v1.24.1", - "source": { - "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "conflict": { - "fzaninotto/faker": "*" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." - }, - "type": "library", - "autoload": { - "psr-4": { - "Faker\\": "src/Faker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" - }, - "time": "2024-11-21T13:46:39+00:00" - }, { "name": "fidry/cpu-core-counter", "version": "1.2.0", @@ -12056,98 +12467,6 @@ ], "time": "2025-02-14T17:58:34+00:00" }, - { - "name": "symfony/maker-bundle", - "version": "v1.63.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/maker-bundle.git", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425", - "shasum": "" - }, - "require": { - "doctrine/inflector": "^2.0", - "nikic/php-parser": "^5.0", - "php": ">=8.1", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" - }, - "conflict": { - "doctrine/doctrine-bundle": "<2.10", - "doctrine/orm": "<2.15" - }, - "require-dev": { - "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0", - "doctrine/orm": "^2.15|^3", - "symfony/http-client": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4.1|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", - "twig/twig": "^3.0|^4.x-dev" - }, - "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Bundle\\MakerBundle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", - "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", - "keywords": [ - "code generator", - "dev", - "generator", - "scaffold", - "scaffolding" - ], - "support": { - "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-26T01:41:37+00:00" - }, { "name": "symfony/options-resolver", "version": "v6.4.16", @@ -12297,67 +12616,6 @@ ], "time": "2025-04-09T08:35:42+00:00" }, - { - "name": "symfony/process", - "version": "v6.4.20", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", - "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v6.4.20" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-03-10T17:11:00+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", @@ -12407,171 +12665,6 @@ } ], "time": "2024-03-03T12:36:25+00:00" - }, - { - "name": "zenstruck/assert", - "version": "v1.5.1", - "source": { - "type": "git", - "url": "https://github.com/zenstruck/assert.git", - "reference": "39554ce3a275fbf8c870b251e620101f644e9277" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zenstruck/assert/zipball/39554ce3a275fbf8c870b251e620101f644e9277", - "reference": "39554ce3a275fbf8c870b251e620101f644e9277", - "shasum": "" - }, - "require": { - "php": ">=8.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5.4|^6.0|^7.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^9.5.21", - "symfony/phpunit-bridge": "^6.3|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Zenstruck\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Bond", - "email": "kevinbond@gmail.com" - } - ], - "description": "Standalone, lightweight, framework agnostic, test assertion library.", - "homepage": "https://github.com/zenstruck/assert", - "keywords": [ - "assertion", - "phpunit", - "test" - ], - "support": { - "issues": "https://github.com/zenstruck/assert/issues", - "source": "https://github.com/zenstruck/assert/tree/v1.5.1" - }, - "funding": [ - { - "url": "https://github.com/kbond", - "type": "github" - } - ], - "time": "2024-10-28T18:08:12+00:00" - }, - { - "name": "zenstruck/foundry", - "version": "v2.4.2", - "source": { - "type": "git", - "url": "https://github.com/zenstruck/foundry.git", - "reference": "277472cc5f938fd98c2a5cf0a317b8283d897be6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zenstruck/foundry/zipball/277472cc5f938fd98c2a5cf0a317b8283d897be6", - "reference": "277472cc5f938fd98c2a5cf0a317b8283d897be6", - "shasum": "" - }, - "require": { - "doctrine/persistence": "^2.0|^3.0|^4.0", - "fakerphp/faker": "^1.23", - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.2|^3.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", - "zenstruck/assert": "^1.4" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "brianium/paratest": "^6|^7", - "dama/doctrine-test-bundle": "^8.0", - "doctrine/collections": "^1.7|^2.0", - "doctrine/common": "^3.2.2", - "doctrine/doctrine-bundle": "^2.10", - "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", - "doctrine/mongodb-odm": "^2.4", - "doctrine/mongodb-odm-bundle": "^4.6|^5.0", - "doctrine/orm": "^2.16|^3.0", - "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0", - "symfony/console": "^6.4|^7.0", - "symfony/dotenv": "^6.4|^7.0", - "symfony/maker-bundle": "^1.55", - "symfony/phpunit-bridge": "^6.4|^7.0", - "symfony/runtime": "^6.4|^7.0", - "symfony/translation-contracts": "^3.4", - "symfony/uid": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" - }, - "type": "library", - "extra": { - "psalm": { - "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin" - }, - "bamarni-bin": { - "bin-links": true, - "forward-command": false, - "target-directory": "bin/tools" - } - }, - "autoload": { - "files": [ - "src/functions.php", - "src/Persistence/functions.php", - "src/symfony_console.php" - ], - "psr-4": { - "Zenstruck\\Foundry\\": "src/", - "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Bond", - "email": "kevinbond@gmail.com" - }, - { - "name": "Nicolas PHILIPPE", - "email": "nikophil@gmail.com" - } - ], - "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.", - "homepage": "https://github.com/zenstruck/foundry", - "keywords": [ - "Fixture", - "dev", - "doctrine", - "factory", - "faker", - "symfony", - "test" - ], - "support": { - "issues": "https://github.com/zenstruck/foundry/issues", - "source": "https://github.com/zenstruck/foundry/tree/v2.4.2" - }, - "funding": [ - { - "url": "https://github.com/kbond", - "type": "github" - } - ], - "time": "2025-04-17T06:26:28+00:00" } ], "aliases": [], diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 93a3cf4..446bc45 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -32,7 +32,7 @@ api_platform: standard_put: true pagination_enabled: true pagination_items_per_page: 30 - pagination_client_enabled: true + pagination_maximum_items_per_page: 100 pagination_client_items_per_page: true use_symfony_listeners: true swagger: diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml index c766508..7b20162 100644 --- a/config/packages/nelmio_cors.yaml +++ b/config/packages/nelmio_cors.yaml @@ -4,6 +4,7 @@ nelmio_cors: allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] allow_headers: ['Content-Type', 'Authorization'] + allow_credentials: true expose_headers: ['Link'] max_age: 3600 paths: diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 191ba3a..92747af 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -25,6 +25,8 @@ security: access_token: token_handler: App\Security\AccessTokenHandler + custom_authenticators: + - App\Security\CookieAuthenticator # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/config/packages/zenstruck_foundry.yaml b/config/packages/zenstruck_foundry.yaml index e3ed04b..71fd8fe 100644 --- a/config/packages/zenstruck_foundry.yaml +++ b/config/packages/zenstruck_foundry.yaml @@ -1,5 +1,7 @@ when@dev: &dev # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration zenstruck_foundry: + persistence: + flush_once: true when@test: *dev diff --git a/config/services.yaml b/config/services.yaml index b0e2c1f..d7d9d2c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,8 +14,14 @@ services: _instanceof: App\Gateway\GatewayInterface: tags: ['app.gateway.gateway'] - App\Library\Economy\Currency\ExchangeInterface: - tags: ['app.lib.economy.currency.exchange'] + App\Money\Conversion\ExchangeInterface: + tags: ['app.money.conversion.exchange'] + App\Money\Totalization\TotalizerInterface: + tags: ['app.money.totalization.totalizer'] + App\Matchfunding\Formula\FormulaInterface: + tags: ['app.matchfunding.formula'] + App\Matchfunding\Rule\RuleInterface: + tags: ['app.matchfunding.rule'] App\Service\Embed\Filter\EmbedFilterInterface: tags: ['app.embed.embed_filter'] App\Service\Embed\Provider\EmbedProviderFactoryInterface: @@ -63,6 +69,14 @@ services: tags: - { name: api_platform.uri_variables.transformer } + App\Matchfunding\Formula\FormulaLocator: + arguments: + - !tagged 'app.matchfunding.formula' + + App\Matchfunding\Rule\RuleLocator: + arguments: + - !tagged 'app.matchfunding.rule' + App\Service\Auth\AuthService: arguments: - '%env(APP_SECRET)%' @@ -82,9 +96,21 @@ services: $hosts: - 'peertube.plataformess.org' - App\Library\Economy\Currency\ExchangeLocator: + App\State\MoneyTotalStateProvider: arguments: - - !tagged 'app.lib.economy.currency.exchange' + - !tagged 'api_platform.doctrine.orm.query_extension.collection' + + App\Money\MoneyService: + arguments: + - '%env(CURRENCY_DEFAULT)%' + + App\Money\Conversion\ExchangeLocator: + arguments: + - !tagged 'app.money.conversion.exchange' + + App\Money\Totalization\TotalizerLocator: + arguments: + - !tagged 'app.money.totalization.totalizer' App\Mapping\AutoMapper: arguments: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a776fbc..b06cbb2 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,9 +1,9 @@ services: php: volumes: - # ./docker/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro - - ./docker/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro - - ./docker/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro + # ./docker/dev/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro + - ./docker/dev/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro + - ./docker/dev/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro extra_hosts: - "host.docker.internal:host-gateway" user: "${UID:-1000}:${GID:-1000}" diff --git a/docker-compose.yml b/docker-compose.yml index dcaac30..344ab10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,20 +2,20 @@ services: php: build: context: . - dockerfile: docker/php/Dockerfile + dockerfile: docker/dev/php/Dockerfile target: dev container_name: goteo-api-php volumes: - .:/app - - ./docker/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro - - ./docker/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro + - ./docker/dev/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro + - ./docker/dev/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro nginx: image: nginx:alpine container_name: goteo-api-nginx volumes: - .:/app - - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf + - ./docker/dev/nginx/nginx.conf:/etc/nginx/conf.d/default.conf ports: - "${APP_HTTP_PORT:-8090}:80" - "${APP_HTTPS_PORT:-8091}:443" diff --git a/docker/nginx/nginx.conf b/docker/dev/nginx/nginx.conf similarity index 100% rename from docker/nginx/nginx.conf rename to docker/dev/nginx/nginx.conf diff --git a/docker/php/Dockerfile b/docker/dev/php/Dockerfile similarity index 94% rename from docker/php/Dockerfile rename to docker/dev/php/Dockerfile index dd8ab34..f33d406 100644 --- a/docker/php/Dockerfile +++ b/docker/dev/php/Dockerfile @@ -47,7 +47,7 @@ FROM base AS prod COPY . /app RUN chown -R www-data:www-data /app -COPY /docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh +COPY /docker/dev/php/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh USER www-data diff --git a/docker/php/conf.d/opcache.dev.ini b/docker/dev/php/conf.d/opcache.dev.ini similarity index 100% rename from docker/php/conf.d/opcache.dev.ini rename to docker/dev/php/conf.d/opcache.dev.ini diff --git a/docker/php/conf.d/opcache.ini b/docker/dev/php/conf.d/opcache.ini similarity index 100% rename from docker/php/conf.d/opcache.ini rename to docker/dev/php/conf.d/opcache.ini diff --git a/docker/php/conf.d/openssl.ini b/docker/dev/php/conf.d/openssl.ini similarity index 100% rename from docker/php/conf.d/openssl.ini rename to docker/dev/php/conf.d/openssl.ini diff --git a/docker/php/conf.d/xdebug.dev.ini b/docker/dev/php/conf.d/xdebug.dev.ini similarity index 100% rename from docker/php/conf.d/xdebug.dev.ini rename to docker/dev/php/conf.d/xdebug.dev.ini diff --git a/docker/php/entrypoint.sh b/docker/dev/php/entrypoint.sh similarity index 100% rename from docker/php/entrypoint.sh rename to docker/dev/php/entrypoint.sh diff --git a/docker/php/ssl/.gitignore b/docker/dev/php/ssl/.gitignore similarity index 100% rename from docker/php/ssl/.gitignore rename to docker/dev/php/ssl/.gitignore diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile new file mode 100644 index 0000000..0cf0932 --- /dev/null +++ b/docker/main/Dockerfile @@ -0,0 +1,70 @@ +FROM php:8.3-fpm-alpine AS system-setup + +RUN apk --no-cache add \ + curl \ + git \ + linux-headers \ + icu-dev \ + zip \ + libzip-dev \ + unzip \ + nginx \ + supervisor \ + $PHPIZE_DEPS && \ + pecl install apcu + +RUN curl --remote-name https://curl.se/ca/cacert.pem \ + && mv cacert.pem /etc/ssl/certs/ca-certificates.crt + +RUN docker-php-ext-install \ + pdo \ + pdo_mysql \ + intl \ + zip \ + opcache && \ + docker-php-ext-enable apcu + +COPY --from=composer:2.8.9 /usr/bin/composer /usr/local/bin/composer + +COPY ./docker/main/default.conf /etc/nginx/http.d/default.conf +COPY ./docker/main/supervisord.conf /etc/supervisord.conf +COPY ./docker/main/www.conf /usr/local/etc/php-fpm.d/www.conf +COPY ./docker/main/local.ini /usr/local/etc/php/conf.d/local.ini + +# FPM configuration via ENV +ENV FPM_PM=dynamic +ENV FPM_PM_MAX_CHILDREN=5 +ENV FPM_PM_START_SERVERS=2 +ENV FPM_PM_MIN_SPARE_SERVERS=1 +ENV FPM_PM_MAX_SPARE_SERVERS=3 + +# PHP configuration via ENV +ENV LOG_TO_STDOUT=1 +ENV PHP_MEMORY_LIMIT=512M +ENV PHP_MAX_EXECUTION_TIME=30 +ENV PHP_UPLOAD_MAX_FILESIZE=120M +ENV PHP_POST_MAX_SIZE=128M +ENV PHP_SESSION_SAVE_HANDLER=files +ENV PHP_SESSION_SAVE_PATH= +ENV PHP_DISPLAY_ERRORS=0 +ENV PHP_DISPLAY_STARTUP_ERRORS=0 + +FROM system-setup AS app-setup + +COPY . /app +RUN chown -R nginx:nginx /app + +COPY ./docker/main/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER nginx +WORKDIR /app + +RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader + +EXPOSE 80 + +HEALTHCHECK --interval=15s --timeout=5s --retries=3 \ + CMD curl -s -o /dev/null -f http://localhost/v4/index.json || exit 1 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/main/default.conf b/docker/main/default.conf new file mode 100644 index 0000000..8f99d47 --- /dev/null +++ b/docker/main/default.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name localhost; + root /app/public; + + index index.php; + + error_log /dev/stdout; + access_log /dev/stdout; + + location / { + try_files $uri /index.php$is_args$args; + } + + location ~ ^/index\.php(/|$) { + fastcgi_pass localhost:9000; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + fastcgi_busy_buffers_size 256k; + + internal; + } + + location ~ \.php$ { + return 404; + } +} diff --git a/docker/main/entrypoint.sh b/docker/main/entrypoint.sh new file mode 100644 index 0000000..c408ab1 --- /dev/null +++ b/docker/main/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +# Check if migrations need to be applied +if ! php bin/console doctrine:migrations:up-to-date --quiet; then + echo "Applying migrations..." + php bin/console doctrine:migrations:migrate --no-interaction +fi + +# Check if FORCE_SCHEMA_UPDATE is set to true +if [ "$FORCE_SCHEMA_UPDATE" = "true" ]; then + echo "Forcing schema update..." + php bin/console doctrine:schema:update --force +fi + +php bin/console cache:warmup +php bin/console assets:install + +echo "Start Supervisord" + +supervisord -c /etc/supervisord.conf diff --git a/docker/main/local.ini b/docker/main/local.ini new file mode 100644 index 0000000..00adf04 --- /dev/null +++ b/docker/main/local.ini @@ -0,0 +1,9 @@ +memory_limit = ${PHP_MEMORY_LIMIT} +max_execution_time = ${PHP_MAX_EXECUTION_TIME} +upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE} +post_max_size = ${PHP_POST_MAX_SIZE} +allow_url_fopen = 1 +session.save_handler = ${PHP_SESSION_SAVE_HANDLER} +session.save_path = ${PHP_SESSION_SAVE_PATH} +display_errors = ${PHP_DISPLAY_ERRORS} +display_startup_errors = ${PHP_DISPLAY_STARTUP_ERRORS} diff --git a/docker/main/supervisord.conf b/docker/main/supervisord.conf new file mode 100644 index 0000000..2492f86 --- /dev/null +++ b/docker/main/supervisord.conf @@ -0,0 +1,26 @@ +[supervisord] +logfile=/dev/null +logfile_maxbytes=0 +logfile_backups=0 +loglevel=info +nodaemon=true + +[program:php-fpm] +command=php-fpm --nodaemonize +autorestart=true +stdout_events_enabled=true +stderr_events_enabled=true +stdout_logfile_maxbytes=0 +stderr_logfile_maxbytes=0 +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr + +[program:nginx] +command=nginx -g 'daemon off;' +autorestart=true +stdout_events_enabled=true +stderr_events_enabled=true +stdout_logfile_maxbytes=0 +stderr_logfile_maxbytes=0 +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr diff --git a/docker/main/www.conf b/docker/main/www.conf new file mode 100644 index 0000000..0d45d03 --- /dev/null +++ b/docker/main/www.conf @@ -0,0 +1,18 @@ +[global] +daemonize = no + +[www] +user = nginx +group = nginx +listen = 9000 +listen.owner = nginx +listen.group = nginx +listen.mode = 0660 + +pm = ${FPM_PM} +pm.max_children = ${FPM_PM_MAX_CHILDREN} +pm.start_servers = ${FPM_PM_START_SERVERS} +pm.min_spare_servers = ${FPM_PM_MIN_SPARE_SERVERS} +pm.max_spare_servers = ${FPM_PM_MAX_SPARE_SERVERS} + +catch_workers_output = yes diff --git a/src/ApiResource/Accounting/AccountingApiResource.php b/src/ApiResource/Accounting/AccountingApiResource.php index befca59..74a4ee6 100644 --- a/src/ApiResource/Accounting/AccountingApiResource.php +++ b/src/ApiResource/Accounting/AccountingApiResource.php @@ -4,18 +4,17 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; +use App\ApiResource\Matchfunding\MatchCallApiResource; use App\ApiResource\Project\ProjectApiResource; use App\ApiResource\TipjarApiResource; use App\ApiResource\User\UserApiResource; use App\Entity\Accounting\Accounting; -use App\Entity\Money; +use App\Entity\Matchfunding\MatchCall; use App\Entity\Project\Project; use App\Entity\Tipjar; use App\Entity\User\User; -use App\Mapping\Transformer\AccountingBalanceMapTransformer; use App\State\Accounting\AccountingStateProcessor; use App\State\Accounting\AccountingStateProvider; -use AutoMapper\Attribute\MapFrom; /** * v4 features an advanced economy model under the hood. @@ -44,16 +43,21 @@ class AccountingApiResource */ public string $currency; - /** - * The money currently held by the Accounting. - */ - #[MapFrom(Accounting::class, transformer: AccountingBalanceMapTransformer::class)] - #[API\ApiProperty(writable: false, security: 'is_granted("ACCOUNTING_VIEW", object)')] - public Money $balance; - #[API\ApiProperty(readable: false, writable: false)] public string $ownerClass; + #[API\ApiProperty(readable: false, writable: false)] + public ?UserApiResource $user = null; + + #[API\ApiProperty(readable: false, writable: false)] + public ?ProjectApiResource $project = null; + + #[API\ApiProperty(readable: false, writable: false)] + public ?TipjarApiResource $tipjar = null; + + #[API\ApiProperty(readable: false, writable: false)] + public ?MatchCallApiResource $matchCall; + /** * The resource owning this Accounting. * @@ -68,17 +72,10 @@ class AccountingApiResource return $this->project; case Tipjar::class: return $this->tipjar; + case MatchCall::class: + return $this->matchCall; } return null; } - - #[API\ApiProperty(readable: false, writable: false)] - public ?UserApiResource $user = null; - - #[API\ApiProperty(readable: false, writable: false)] - public ?ProjectApiResource $project = null; - - #[API\ApiProperty(readable: false, writable: false)] - public ?TipjarApiResource $tipjar = null; } diff --git a/src/ApiResource/Accounting/AccountingBalance.php b/src/ApiResource/Accounting/AccountingBalance.php new file mode 100644 index 0000000..d8a7b57 --- /dev/null +++ b/src/ApiResource/Accounting/AccountingBalance.php @@ -0,0 +1,38 @@ + new API\Link( + fromClass: AccountingApiResource::class, + fromProperty: 'id', + description: 'Accounting identifier' + ), + ] +)] +class AccountingBalance +{ + #[API\ApiProperty(identifier: true)] + public AccountingApiResource $accounting; + + /** + * The money currently held by the Accounting. + */ + #[API\ApiProperty(security: 'is_granted("ACCOUNTING_VIEW", object.accounting)')] + public ApiMoney $balance; +} diff --git a/src/ApiResource/Accounting/AccountingBalancePoint.php b/src/ApiResource/Accounting/AccountingBalancePoint.php index 6ca16fc..80c3c11 100644 --- a/src/ApiResource/Accounting/AccountingBalancePoint.php +++ b/src/ApiResource/Accounting/AccountingBalancePoint.php @@ -4,7 +4,7 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Metadata as API; use ApiPlatform\Metadata\QueryParameter; -use App\Entity\Money; +use App\ApiResource\ApiMoney; use App\State\Accounting\AccountingBalancePointStateProvider; use Symfony\Component\Validator\Constraints as Assert; @@ -57,7 +57,7 @@ class AccountingBalancePoint /** * Resulting balance for items in this point. */ - public Money $balance; + public ApiMoney $balance; /** * The number of items aggregated in this point. diff --git a/src/ApiResource/Accounting/TransactionApiResource.php b/src/ApiResource/Accounting/TransactionApiResource.php index 85e13c6..06ba51a 100644 --- a/src/ApiResource/Accounting/TransactionApiResource.php +++ b/src/ApiResource/Accounting/TransactionApiResource.php @@ -5,8 +5,8 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; +use App\ApiResource\ApiMoney; use App\Entity\Accounting\Transaction; -use App\Entity\Money; use App\State\ApiResourceStateProvider; /** @@ -33,7 +33,7 @@ class TransactionApiResource /** * The monetary value received at target and issued at origin. */ - public Money $money; + public ApiMoney $money; /** * The Accounting from which the Transaction comes from. diff --git a/src/ApiResource/ApiMoney.php b/src/ApiResource/ApiMoney.php new file mode 100644 index 0000000..daab79e --- /dev/null +++ b/src/ApiResource/ApiMoney.php @@ -0,0 +1,69 @@ +amount = $amount; + $this->currency = $currency; + } + + public static function of(MoneyInterface $moneyInterface): self + { + $money = new self( + $moneyInterface->getAmount(), + $moneyInterface->getCurrency(), + ); + + $money->conversion = $moneyInterface->getConversion()?->toArray(); + + return $money; + } + + public function getAmount(): int + { + return $this->amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function getConversion(): ?Conversion + { + return $this->conversion + ? Conversion::fromArray($this->conversion) + : null; + } +} diff --git a/src/ApiResource/Gateway/ChargeApiResource.php b/src/ApiResource/Gateway/ChargeApiResource.php index 35b1533..1b2ea13 100644 --- a/src/ApiResource/Gateway/ChargeApiResource.php +++ b/src/ApiResource/Gateway/ChargeApiResource.php @@ -6,9 +6,10 @@ use ApiPlatform\Doctrine\Orm\Filter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; +use App\ApiResource\ApiMoney; use App\Dto\Gateway\ChargeUpdationDto; use App\Entity\Gateway\Charge; -use App\Entity\Money; +use App\Filter\GatewayFilter; use App\Gateway\ChargeStatus; use App\Gateway\ChargeType; use App\State\ApiResourceStateProvider; @@ -38,6 +39,8 @@ class ChargeApiResource * The Checkout to which this Charge item belongs to. */ #[API\ApiProperty(writable: false, security: 'is_granted("IS_AUTHENTICATED_FULLY")')] + #[API\ApiFilter(GatewayFilter::class, properties: ['checkout.gateway'])] + #[API\ApiFilter(Filter\SearchFilter::class, properties: ['checkout.trackings.value'])] public CheckoutApiResource $checkout; /** @@ -76,17 +79,22 @@ class ChargeApiResource * It is money before fees and taxes, not accountable. */ #[Assert\NotBlank()] - #[API\ApiFilter(Filter\SearchFilter::class, properties: ['money.amount' => 'exact'])] #[API\ApiFilter(Filter\RangeFilter::class, properties: ['money.amount'])] - public Money $money; + #[API\ApiFilter(Filter\SearchFilter::class, properties: ['money.currency' => 'exact'])] + public ApiMoney $money; /** * The status of the charge item with the Gateway. */ - #[Assert\NotBlank()] + #[API\ApiProperty(writable: false)] #[API\ApiFilter(Filter\SearchFilter::class, strategy: 'exact')] public ChargeStatus $status = ChargeStatus::InPending; + #[API\ApiProperty(writable: false)] + #[API\ApiFilter(Filter\DateFilter::class)] public \DateTimeInterface $dateCreated; + + #[API\ApiProperty(writable: false)] + #[API\ApiFilter(Filter\DateFilter::class)] public \DateTimeInterface $dateUpdated; } diff --git a/src/ApiResource/Gateway/CheckoutApiResource.php b/src/ApiResource/Gateway/CheckoutApiResource.php index 7e904a5..45c73b8 100644 --- a/src/ApiResource/Gateway/CheckoutApiResource.php +++ b/src/ApiResource/Gateway/CheckoutApiResource.php @@ -5,7 +5,7 @@ namespace App\ApiResource\Gateway; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; -use App\Dto\CheckoutUpdationDto; +use App\Dto\Gateway\CheckoutUpdationDto; use App\Entity\Gateway\Checkout; use App\Gateway\CheckoutStatus; use App\Gateway\Link; @@ -60,7 +60,7 @@ class CheckoutApiResource * * @var ChargeApiResource[] */ - #[API\ApiProperty(readableLink: true, writableLink: true)] + #[API\ApiProperty(readableLink: true)] #[Assert\NotBlank()] #[Assert\Count(min: 1)] public array $charges = []; @@ -106,20 +106,16 @@ class CheckoutApiResource * @var Tracking[] */ #[API\ApiProperty(writable: false)] - #[MapTo(Checkout::class, transformer: 'parseTrackings')] - #[MapFrom(Checkout::class, transformer: 'parseTrackings')] public array $trackings = []; + #[API\ApiProperty(writable: false)] public \DateTimeInterface $dateCreated; + + #[API\ApiProperty(writable: false)] public \DateTimeInterface $dateUpdated; public static function parseLinks(array $values) { return \array_map(fn($value) => Link::tryFrom($value), $values); } - - public static function parseTrackings(array $values) - { - return \array_map(fn($value) => Tracking::tryFrom($value), $values); - } } diff --git a/src/ApiResource/Matchfunding/FormulaApiResource.php b/src/ApiResource/Matchfunding/FormulaApiResource.php new file mode 100644 index 0000000..f84873c --- /dev/null +++ b/src/ApiResource/Matchfunding/FormulaApiResource.php @@ -0,0 +1,38 @@ +name = $interface::getName(); + $resource->expression = $interface::getAsExpression(); + + return $resource; + } +} diff --git a/src/ApiResource/Matchfunding/MatchCallApiResource.php b/src/ApiResource/Matchfunding/MatchCallApiResource.php new file mode 100644 index 0000000..1048333 --- /dev/null +++ b/src/ApiResource/Matchfunding/MatchCallApiResource.php @@ -0,0 +1,93 @@ + + * + * @return array + */ + public static function rulesToNames(array $values) + { + return \array_map(fn($r) => $r->name, $values); + } +} diff --git a/src/ApiResource/Matchfunding/RuleApiResource.php b/src/ApiResource/Matchfunding/RuleApiResource.php new file mode 100644 index 0000000..b5d72c2 --- /dev/null +++ b/src/ApiResource/Matchfunding/RuleApiResource.php @@ -0,0 +1,38 @@ +name = \array_slice(\explode('\\', $interface::class), -1, 1)[0]; + $resource->description = $interface::getDescription(); + + return $resource; + } +} diff --git a/src/ApiResource/Money.php b/src/ApiResource/Money.php deleted file mode 100644 index 9fba1f0..0000000 --- a/src/ApiResource/Money.php +++ /dev/null @@ -1,23 +0,0 @@ - [ + 'description' => 'Totalized money', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/ProjectSupport.TotalizedMoney', + ], + ], + ], + ], + ] + ) +)] #[API\Get()] #[API\Patch( security: 'is_granted("SUPPORT_EDIT", previous_object)', @@ -32,40 +61,40 @@ class SupportApiResource public int $id; /** - * The User who created the ProjectSupport record.\ - * \ - * When `anonymous` is *true* it will only be public to admins and the User. - */ - #[API\ApiProperty(writable: false, security: 'is_granted("SUPPORT_VIEW", object)')] - #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')] - public ?UserApiResource $owner; - - /** - * The Project being targeted in the Charges. + * The Project being supported. */ #[API\ApiProperty(writable: false)] #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')] public ProjectApiResource $project; /** - * The Charges that were paid by the User. - * - * @var array + * The Accounting of origin for the Transactions under this ProjectSupport record.\ + * \ + * When `anonymous` is *true* it will only be public to admins and the User. */ - #[API\ApiProperty(writable: false)] - public array $charges; + #[API\ApiProperty(writable: false, security: 'is_granted("SUPPORT_VIEW", object)')] + #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')] + public ?AccountingApiResource $origin; /** - * The total monetary value of the Charges paid by the User. + * The Transactions that were issued to the Project by the origin. + * + * @var array */ - #[MapFrom(Support::class, transformer: SupportMoneyMapTransformer::class)] - public Money $money; + #[API\ApiProperty(writable: false)] + public array $transactions; + + /** + * The total monetary value of the Transactions going to the Project. + */ + public ApiMoney $money; /** * User's will to have their support to the Project be shown publicly. */ #[Assert\NotNull()] #[Assert\Type('bool')] + #[API\ApiFilter(BooleanFilter::class)] public bool $anonymous = true; /** diff --git a/src/ApiResource/TipjarApiResource.php b/src/ApiResource/TipjarApiResource.php index 5b58421..90a877d 100644 --- a/src/ApiResource/TipjarApiResource.php +++ b/src/ApiResource/TipjarApiResource.php @@ -2,6 +2,7 @@ namespace App\ApiResource; +use ApiPlatform\Doctrine\Orm\Filter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; @@ -38,5 +39,6 @@ class TipjarApiResource * Human readable, non white space, unique string. */ #[Assert\NotBlank()] + #[API\ApiFilter(Filter\SearchFilter::class, strategy: 'partial')] public string $name; } diff --git a/src/Benzina/CheckoutsPump.php b/src/Benzina/CheckoutsPump.php deleted file mode 100644 index 010a92a..0000000 --- a/src/Benzina/CheckoutsPump.php +++ /dev/null @@ -1,248 +0,0 @@ -hasAllKeys($sample, self::INVEST_KEYS)) { - return true; - } - - return false; - } - - public function pump(mixed $record, array $context): void - { - if (!$record['user'] || empty($record['user'])) { - return; - } - - if (!$record['amount'] || $record['amount'] < 1) { - return; - } - - if (!$record['method'] || empty($record['method'])) { - return; - } - - $user = $this->getUser($record); - if ($user === null) { - return; - } - - $project = $this->getProject($record); - $tipjar = $this->getPlatformTipjar(); - - $checkout = new Checkout(); - $checkout->setOrigin($user->getAccounting()); - $checkout->setStatus($this->getCheckoutStatus($record)); - $checkout->setGatewayName($this->getCheckoutGateway($record)); - - foreach ($this->getCheckoutTrackings($record) as $tracking) { - $checkout->addTracking($tracking); - } - - $checkout->setMigrated(true); - $checkout->setMigratedId($record['id']); - - $checkout->setDateCreated(new \DateTime($record['invested'])); - $checkout->setDateUpdated(new \DateTime()); - - $charge = new Charge(); - $charge->setType($this->getChargeType($record)); - $charge->setMoney($this->getChargeMoney($record['amount'], $record['currency'])); - - if ($project === null) { - $charge->setTitle(self::CHARGE_TITLE_POOL); - $charge->setTarget($user->getAccounting()); - } else { - $charge->setTitle(self::CHARGE_TITLE_PROJECT); - $charge->setTarget($project->getAccounting()); - } - - $checkout->addCharge($charge); - - if ($record['donate_amount'] > 0) { - $tip = new Charge(); - $tip->setType(ChargeType::Single); - $tip->setTitle(self::CHARGE_TITLE_TIP); - $tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency'])); - $tip->setTarget($tipjar->getAccounting()); - - $checkout->addCharge($tip); - } - - if ($checkout->getStatus() === CheckoutStatus::Charged) { - $checkout = $this->checkoutService->chargeCheckout($checkout); - } - - $this->persist($checkout, $context); - } - - private function getUser(array $record): ?User - { - return $this->userRepository->findOneBy(['migratedId' => $record['user']]); - } - - private function getProject(array $record): ?Project - { - if (empty($record['project'])) { - return null; - } - - return $this->projectRepository->findOneBy(['migratedId' => $record['project']]); - } - - private function getPlatformTipjar(): Tipjar - { - $tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]); - - if ($tipjar) { - return $tipjar; - } - - $tipjar = new Tipjar(); - $tipjar->setName(self::PLATFORM_TIPJAR_NAME); - - $this->entityManager->persist($tipjar); - $this->entityManager->flush(); - - return $tipjar; - } - - private function getChargeType(array $record): ChargeType - { - if (\in_array($record['method'], ['stripe_subscription'])) { - return ChargeType::Recurring; - } - - return ChargeType::Single; - } - - private function getChargeMoney(int $amount, string $currency): Money - { - $amount = $amount * 100; - - if ($amount >= self::MAX_INT) { - $amount = self::MAX_INT; - } - - return new Money($amount, $currency); - } - - private function getCheckoutStatus(array $record): CheckoutStatus - { - if ($record['status'] < 1) { - return CheckoutStatus::InPending; - } - - return CheckoutStatus::Charged; - } - - private function getCheckoutGateway(array $record): string - { - switch ($record['method']) { - case 'stripe_subscription': - return StripeGateway::getName(); - case 'pool': - return WalletGateway::getName(); - case 'paypal': - return PaypalGateway::getName(); - case 'tpv': - return CecaGateway::getName(); - case 'cash': - return CashGateway::getName(); - case 'drop': - return DropGateway::getName(); - default: - return ''; - } - } - - /** - * @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'])) { - $payment = new Tracking(); - $payment->title = self::TRACKING_TITLE_PAYMENT; - $payment->value = $record['payment']; - - $trackings[] = $payment; - } - - if (!empty($record['transaction'])) { - $transaction = new Tracking(); - $transaction->title = self::TRACKING_TITLE_TRANSACTION; - $transaction->value = $record['transaction']; - - $trackings[] = $transaction; - } - - if (!empty($record['preapproval'])) { - $preapproval = new Tracking(); - $preapproval->title = self::TRACKING_TITLE_PREAPPROVAL; - $preapproval->value = $record['preapproval']; - - $trackings[] = $preapproval; - } - - return $trackings; - } -} diff --git a/src/Benzina/InvestRewardsPump.php b/src/Benzina/InvestRewardsPump.php new file mode 100644 index 0000000..1cd96b0 --- /dev/null +++ b/src/Benzina/InvestRewardsPump.php @@ -0,0 +1,153 @@ + */ + private array $userCache = []; + + /** @var array */ + private array $chargeCache = []; + + /** @var array */ + private array $rewardCache = []; + + public function __construct( + private RewardRepository $rewardRepository, + private UserRepository $userRepository, + private ChargeRepository $chargeRepository, + private MoneyService $moneyService, + ) {} + + public function supports(mixed $sample): bool + { + if ($this->hasAllKeys($sample, ['invest', 'reward', 'fulfilled'])) { + return true; + } + + return false; + } + + public function pump(mixed $record, array $context): void + { + $reward = $this->getReward($record); + if ($reward === null) { + return; + } + + $invest = $this->getInvest($record, $context); + if ($invest === null) { + return; + } + + $charge = $this->getCharge($invest); + if ($charge === null) { + return; + } + + $user = $this->getUser($invest); + if ($user === null) { + return; + } + + if ($charge->getStatus() !== ChargeStatus::Charged) { + return; + } + + $claim = new RewardClaim(); + $claim->setOwner($user); + $claim->setCharge($charge); + + $reward->addClaim($claim); + $claim->setReward($reward); + + $this->persist($claim, $context); + } + + private function getInvest(array $record, array $context): ?array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `invest` i WHERE i.id = :invest' + ); + + $query->execute(['invest' => $record['invest']]); + + $result = $query->fetch(\PDO::FETCH_ASSOC); + + if (!$result) { + return null; + } + + return $result; + } + + private function getReward(array $record): ?Reward + { + $id = $record['reward']; + + if (isset($this->rewardCache[$id])) { + return $this->rewardRepository->find($this->rewardCache[$id]); + } + + $reward = $this->rewardRepository->findOneBy(['migratedId' => $id]); + if (!$reward) { + return null; + } + + $this->rewardCache[$id] = $reward->getId(); + + return $reward; + } + + private function getCharge(array $invest): ?Charge + { + $id = $invest['id']; + + if (isset($this->chargeCache[$id])) { + return $this->chargeRepository->find($this->chargeCache[$id]); + } + + $charge = $this->chargeRepository->findOneBy(['migratedId' => $id]); + if (!$charge) { + return null; + } + + $this->chargeCache[$id] = $charge->getId(); + + return $charge; + } + + private function getUser(array $invest): ?User + { + $id = $invest['user']; + + if (isset($this->userCache[$id])) { + return $this->userRepository->find($this->userCache[$id]); + } + + $user = $this->userRepository->findOneBy(['migratedId' => $id]); + + $this->userCache[$id] = $user->getId(); + + return $user; + } +} diff --git a/src/Benzina/InvestsPump.php b/src/Benzina/InvestsPump.php new file mode 100644 index 0000000..0f888cc --- /dev/null +++ b/src/Benzina/InvestsPump.php @@ -0,0 +1,502 @@ + */ + private array $userCache = []; + + /** @var array */ + private array $projectCache = []; + + /** @var array */ + private array $supportCache = []; + + /** @var array */ + private array $rewardCache = []; + + public function __construct( + private UserRepository $userRepository, + private ProjectRepository $projectRepository, + private SupportRepository $supportRepository, + private TipjarRepository $tipjarRepository, + private RewardRepository $rewardRepository, + private CheckoutService $checkoutService, + private MoneyService $moneyService, + ) {} + + public function supports(mixed $sample): bool + { + if ($this->hasAllKeys($sample, self::INVEST_KEYS)) { + return true; + } + + return false; + } + + public function pump(mixed $record, array $context): void + { + if (!$record['user'] || empty($record['user'])) { + return; + } + + if (!$record['amount'] || $record['amount'] < 1) { + return; + } + + if (!$record['method'] || empty($record['method'])) { + return; + } + + $user = $this->getUser($record); + if ($user === null) { + return; + } + + $project = $this->getProject($record); + $tipjar = $this->getPlatformTipjar(); + + $now = new \DateTime(); + $invested = new \DateTime($record['datetime'] ?? $record['invested']); + + $checkout = new Checkout(); + $checkout->setMigrated(true); + $checkout->setMigratedId($record['id']); + $checkout->setDateCreated($invested); + $checkout->setDateUpdated($now); + + $checkout->setOrigin($user->getAccounting()); + $checkout->setStatus($this->getCheckoutStatus($record)); + $checkout->setGatewayName($this->getCheckoutGateway($record)); + $checkout->setReturnUrl(self::PLATFORM_RETURN_URL); + + foreach ($this->getCheckoutTrackings($record) as $tracking) { + $checkout->addTracking($tracking); + } + + $charge = new Charge(); + $charge->setMigrated(true); + $charge->setMigratedId($record['id']); + $charge->setDateCreated($invested); + $charge->setDateUpdated($now); + $charge->setStatus($this->getChargeStatus($record)); + $charge->setType($this->getChargeType($record)); + $charge->setMoney($this->getChargeMoney($record['amount'], self::CURRENCY)); + + if ($project === null) { + $charge->setTitle(self::CHARGE_TITLE_POOL); + $charge->setTarget($user->getAccounting()); + } else { + $charge->setTitle(self::CHARGE_TITLE_PROJECT); + $charge->setTarget($project->getAccounting()); + } + + $checkout->addCharge($charge); + + if ($record['donate_amount'] > 0) { + $tip = new Charge(); + $tip->setMigrated(true); + $tip->setMigratedId($record['id']); + $tip->setDateCreated($invested); + $tip->setDateUpdated($now); + $tip->setStatus($this->getChargeStatus($record)); + $tip->setType(ChargeType::Single); + $tip->setTitle(self::CHARGE_TITLE_TIP); + $tip->setMoney($this->getChargeMoney($record['donate_amount'], self::CURRENCY)); + $tip->setTarget($tipjar->getAccounting()); + + $checkout->addCharge($tip); + } + + foreach ($checkout->getCharges() as $charge) { + if (!\in_array($charge->getStatus(), [ChargeStatus::Charged, ChargeStatus::Refunded])) { + continue; + } + + $transaction = new Transaction(); + $transaction->setDateCreated($invested); + $transaction->setMoney($charge->getMoney()); + $transaction->setOrigin($checkout->getOrigin()); + $transaction->setTarget($charge->getTarget()); + + $charge->addTransaction($transaction); + + if ( + $charge->getTarget()->getOwner() instanceof Project + && $charge->getStatus() === ChargeStatus::Charged + ) { + $support = $this->getSupport($charge); + $support->setProject($charge->getTarget()->getOwner()); + $support->setOrigin($checkout->getOrigin()); + $support->addTransaction($transaction); + $support->setMoney($this->calcSupportMoney($support)); + $support->setAnonymous($this->getSupportAnon($record, $support)); + $support->setMessage($this->getSupportMessage($record, $support, $context)); + + $this->entityManager->persist($support); + } + + if ($charge->getStatus() !== ChargeStatus::Refunded) { + continue; + } + + $returned = new Transaction(); + $returned->setDateCreated(new \DateTime($record['returned'] ?? 'now')); + $returned->setMoney($charge->getMoney()); + $returned->setOrigin($charge->getTarget()); + $returned->setTarget($checkout->getOrigin()); + + $charge->addTransaction($returned); + } + + $this->persist($checkout, $context); + } + + private function getUser(array $record): ?User + { + $id = $record['user']; + + if (isset($this->userCache[$id])) { + return $this->userRepository->find($this->userCache[$id]); + } + + $user = $this->userRepository->findOneBy(['migratedId' => $id]); + + $this->userCache[$id] = $user->getId(); + + return $user; + } + + private function getProject(array $record): ?Project + { + $id = $record['project'] ?? null; + + if (!$id) { + return null; + } + + if (isset($this->projectCache[$id])) { + return $this->projectRepository->find($this->projectCache[$id]); + } + + $project = $this->projectRepository->findOneBy(['migratedId' => $id]); + + if (!$project) { + return null; + } + + $this->projectCache[$id] = $project->getId(); + + return $project; + } + + private function getPlatformTipjar(): Tipjar + { + if ($this->tipjarCache !== null) { + return $this->tipjarRepository->find($this->tipjarCache); + } + + $tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]); + + if ($tipjar) { + $this->tipjarCache = $tipjar->getId(); + + return $tipjar; + } + + $tipjar = new Tipjar(); + $tipjar->setName(self::PLATFORM_TIPJAR_NAME); + + $this->entityManager->persist($tipjar); + $this->entityManager->flush(); + + return $tipjar; + } + + private function getSupport(Charge $charge): ?Support + { + $origin = $charge->getCheckout()->getOrigin(); + $target = $charge->getTarget()->getOwner(); + + $originId = $origin->getId(); + $projectId = $target->getId(); + + $cacheKey = $projectId.'-'.$originId; + + if (isset($this->supportCache[$cacheKey])) { + return $this->supportRepository->find($this->supportCache[$cacheKey]); + } + + $support = $this->supportRepository->findOneBy([ + 'origin' => $origin, + 'project' => $target, + ]); + + if ($support !== null) { + $this->supportCache[$cacheKey] = $support->getId(); + + return $support; + } + + return new Support(); + } + + private function getSupportAnon(array $record, Support $support): bool + { + if ($record['anonymous'] === 1) { + return true; + } + + if ($support->isAnonymous()) { + return true; + } + + return false; + } + + private function getSupportMessage(array $record, Support $support, array $context): ?string + { + $message = $support->getMessage(); + $msg = $this->getInvestMsg($record, $context); + + if (!$msg || empty($msg['msg'])) { + return $message; + } + + if (!$message) { + return $msg['msg']; + } + + return \sprintf("\n***\n%s", $msg['msg']); + } + + private function calcSupportMoney(Support $support): EmbeddableMoney + { + $transactions = $support->getTransactions(); + if ($transactions->isEmpty()) { + return EmbeddableMoney::of(new Money(0, self::CURRENCY)); + } + + $first = $transactions->first(); + $money = new Money(0, $first->getMoney()->getCurrency()); + + foreach ($transactions as $transaction) { + $money = $this->moneyService->add($transaction->getMoney(), $money); + } + + return EmbeddableMoney::of($money); + } + + private function getInvestMsg(array $record, array $context) + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `invest_msg` m WHERE m.invest = :invest' + ); + + $query->execute(['invest' => $record['id']]); + + return $query->fetch(\PDO::FETCH_ASSOC); + } + + private function getChargeType(array $record): ChargeType + { + if (\in_array($record['method'], ['stripe_subscription'])) { + return ChargeType::Recurring; + } + + return ChargeType::Single; + } + + private function getChargeMoney(int $amount, string $currency): EmbeddableMoney + { + $amount = $amount * 100; + + if ($amount >= self::MAX_INT) { + $amount = self::MAX_INT; + } + + return new EmbeddableMoney($amount, $currency); + } + + private function getChargeStatus(array $record): ChargeStatus + { + switch ($record['status']) { + case 0: + case 1: + case 3: + case 7: + if ($record['issue'] === 1) { + return ChargeStatus::InPending; + } + + return ChargeStatus::Charged; + case 2: + case 4: + case 6: + return ChargeStatus::Refunded; + default: + return ChargeStatus::InPending; + } + } + + private function getCheckoutStatus(array $record): CheckoutStatus + { + switch ($record['status']) { + case 0: + case 1: + case 3: + case 7: + if ($record['issue'] === 1) { + return CheckoutStatus::InPending; + } + + return CheckoutStatus::Charged; + default: + return CheckoutStatus::InPending; + } + } + + private function getCheckoutGateway(array $record): string + { + switch ($record['method']) { + case 'stripe_subscription': + return StripeGateway::getName(); + case 'pool': + return WalletGateway::getName(); + case 'paypal': + return PaypalGateway::getName(); + case 'tpv': + return CecaGateway::getName(); + case 'cash': + return CashGateway::getName(); + case 'drop': + return DropGateway::getName(); + default: + return ''; + } + } + + /** + * @return Tracking[] + */ + private function getCheckoutTrackings(array $record): array + { + $v3Tracking = new Tracking(); + $v3Tracking->setTitle(self::TRACKING_TITLE_V3); + $v3Tracking->setValue($record['id']); + + $trackings = [$v3Tracking]; + + if (!empty($record['payment'])) { + $payment = new Tracking(); + $payment->setTitle(self::TRACKING_TITLE_PAYMENT); + $payment->setValue($record['payment']); + + $trackings[] = $payment; + } + + if (!empty($record['transaction'])) { + $transaction = new Tracking(); + $transaction->setTitle(self::TRACKING_TITLE_TRANSACTION); + $transaction->setValue($record['transaction']); + + $trackings[] = $transaction; + } + + if (!empty($record['preapproval'])) { + $preapproval = new Tracking(); + $preapproval->setTitle(self::TRACKING_TITLE_PREAPPROVAL); + $preapproval->setValue($record['preapproval']); + + $trackings[] = $preapproval; + } + + return $trackings; + } + + private function getInvestReward(array $record, array $context) + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `invest_reward` ir WHERE ir.invest = :invest' + ); + + $query->execute(['invest' => $record['id']]); + + return $query->fetch(\PDO::FETCH_ASSOC); + } + + private function getReward(array $record, array $context): ?Reward + { + $invested = $this->getInvestReward($record, $context); + + if (!$invested) { + return null; + } + + $id = $invested['reward']; + + if (isset($this->rewardCache[$id])) { + return $this->rewardRepository->find($this->rewardCache[$id]); + } + + $reward = $this->rewardRepository->findOneBy(['migratedId' => $invested['reward']]); + + $this->rewardCache[$id] = $reward->getId(); + + return $reward; + } +} diff --git a/src/Benzina/CheckoutsPumpTrait.php b/src/Benzina/InvestsPumpTrait.php similarity index 96% rename from src/Benzina/CheckoutsPumpTrait.php rename to src/Benzina/InvestsPumpTrait.php index a2f6fa8..4aad917 100644 --- a/src/Benzina/CheckoutsPumpTrait.php +++ b/src/Benzina/InvestsPumpTrait.php @@ -2,7 +2,7 @@ namespace App\Benzina; -trait CheckoutsPumpTrait +trait InvestsPumpTrait { private const INVEST_KEYS = [ 'id', diff --git a/src/Benzina/LocalizedPumpTrait.php b/src/Benzina/LocalizedPumpTrait.php new file mode 100644 index 0000000..7abdcf8 --- /dev/null +++ b/src/Benzina/LocalizedPumpTrait.php @@ -0,0 +1,90 @@ + */ + private static array $translatableFieldsCache = []; + + public function localize( + LocalizedEntityInterface $entity, + array $localizations = [], + array $context = [], + array $fieldCallbacks = [], + ): void { + if ($this->isDryRun($context)) { + return; + } + + $em = $this->getEntityManager(); + $meta = $em->getClassMetadata($entity::class); + $translations = $em->getRepository(Translation::class); + + $translatableFields = $this->getTranslatableFields($meta); + + foreach ($localizations as $localization) { + $locale = $localization['lang'] ?? null; + if (!$locale) { + continue; + } + + $entity->addLocale($locale); + + foreach ($translatableFields as $field) { + if (isset($fieldCallbacks[$field]) && is_callable($fieldCallbacks[$field])) { + $value = $fieldCallbacks[$field]($localization); + } else { + $value = $meta->getFieldValue($entity, $field); + } + + if ($value !== null) { + $translations->translate($entity, $field, $locale, $value); + } + } + } + + if ($this->preventFlushAndClear) { + return; + } + + $em->flush(); + $em->clear(); + } + + /** + * Get all #[Gedmo\Translatable] fields for this entity. + * + * @return string[] + */ + private function getTranslatableFields($meta): array + { + $className = $meta->getName(); + + if (isset(self::$translatableFieldsCache[$className])) { + return self::$translatableFieldsCache[$className]; + } + + $reflection = $meta->getReflectionClass(); + $fields = []; + + foreach ($reflection->getProperties() as $property) { + $attrs = $property->getAttributes(\Gedmo\Mapping\Annotation\Translatable::class); + if (!empty($attrs)) { + $fields[] = $property->getName(); + } + } + + self::$translatableFieldsCache[$className] = $fields; + + return $fields; + } +} diff --git a/src/Benzina/ProjectsBudgetPump.php b/src/Benzina/ProjectsBudgetPump.php index 52908b0..746b442 100644 --- a/src/Benzina/ProjectsBudgetPump.php +++ b/src/Benzina/ProjectsBudgetPump.php @@ -2,7 +2,7 @@ namespace App\Benzina; -use App\Entity\Money; +use App\Entity\EmbeddableMoney; use App\Entity\Project\BudgetItem; use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; @@ -15,7 +15,11 @@ use Goteo\Benzina\Pump\PumpInterface; class ProjectsBudgetPump implements PumpInterface { use ArrayPumpTrait; + use DatabasePumpTrait; use DoctrinePumpTrait; + use LocalizedPumpTrait; + + private const MAX_INT = 2147483647; private const COST_KEYS = [ 'id', @@ -49,24 +53,34 @@ class ProjectsBudgetPump implements PumpInterface return; } - $project = $this->getBudgetProject($record); + $project = $this->getProject($record); if ($project === null) { return; } $budgetItem = new BudgetItem(); + $budgetItem->setMigrated(true); + $budgetItem->setMigratedId($record['id']); $budgetItem->setTranslatableLocale($project->getLocales()[0]); $budgetItem->setProject($project); $budgetItem->setType($this->getCostType($record)); $budgetItem->setTitle($record['cost']); $budgetItem->setDescription($record['description'] ?? $record['cost']); - $budgetItem->setMoney(new Money($record['amount'] * 100, 'EUR')); + $budgetItem->setMoney($this->getCostMoney($record['amount'], 'EUR')); $budgetItem->setDeadline($this->getDeadline($record)); + $this->setPreventFlushAndClear(true); $this->persist($budgetItem, $context); + + $localizations = $this->getCostLocalizations($budgetItem, $context); + + $this->setPreventFlushAndClear(false); + $this->localize($budgetItem, $localizations, $context, [ + 'title' => fn($l) => $l['cost'], + ]); } - private function getBudgetProject(array $record): ?Project + private function getProject(array $record): ?Project { return $this->projectRepository->findOneBy(['migratedId' => $record['project']]); } @@ -91,4 +105,26 @@ class ProjectsBudgetPump implements PumpInterface return ProjectDeadline::Optimum; } + + private function getCostLocalizations(BudgetItem $budgetItem, array $context): array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `cost_lang` l WHERE l.id = :cost' + ); + + $query->execute(['cost' => $budgetItem->getMigratedId()]); + + return $query->fetchAll(); + } + + private function getCostMoney(int $amount, string $currency): EmbeddableMoney + { + $amount = $amount * 100; + + if ($amount >= self::MAX_INT) { + $amount = self::MAX_INT; + } + + return new EmbeddableMoney($amount, $currency); + } } diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index b436580..db23ac3 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -3,17 +3,19 @@ namespace App\Benzina; use App\Entity\Project\Project; +use App\Entity\Project\ProjectCalendar; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; -use App\Entity\Project\ProjectTerritory; use App\Entity\Project\ProjectVideo; use App\Entity\Project\Update; +use App\Entity\Territory; use App\Entity\User\User; +use App\Repository\Project\ProjectRepository; use App\Repository\User\UserRepository; use App\Service\Embed\EmbedService; -use App\Service\Project\CalendarService; use App\Service\Project\TerritoryService; +use Doctrine\Common\Collections\ArrayCollection; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; use Goteo\Benzina\Pump\PumpInterface; @@ -24,12 +26,13 @@ class ProjectsPump implements PumpInterface use DoctrinePumpTrait; use DatabasePumpTrait; use ProjectsPumpTrait; + use LocalizedPumpTrait; public function __construct( + private ProjectRepository $projectRepository, private UserRepository $userRepository, private TerritoryService $territoryService, private EmbedService $embedService, - private CalendarService $calendarService, ) {} public function supports(mixed $sample): bool @@ -48,7 +51,12 @@ class ProjectsPump implements PumpInterface } $status = $this->getProjectStatus($record); - if (\in_array($status, [ProjectStatus::InEditing, ProjectStatus::Rejected])) { + if (\in_array($status, [ProjectStatus::Rejected])) { + return; + } + + $created = new \DateTime($record['created']); + if (\in_array($status, [ProjectStatus::InDraft, ProjectStatus::InEditing]) && $created < new \DateTime('2024-01-01')) { return; } @@ -57,38 +65,49 @@ class ProjectsPump implements PumpInterface return; } - $project = new Project(); - $project->setTranslatableLocale($record['lang']); - $project->setTitle($record['name']); + $project = $this->getProject($record); + if ($project === null) { + $project = new Project(); + } + $project->setSlug($record['id']); - $project->setSubtitle($record['subtitle']); $project->setCategory($this->getProjectCategory($record)); $project->setTerritory($this->getProjectTerritory($record)); - $project->setDescription($record['description']); $project->setVideo($this->getProjectVideo($record)); $project->setOwner($owner); $project->setStatus($status); $project->setMigrated(true); $project->setMigratedId($record['id']); - $project->setDateCreated(new \DateTime($record['created'])); + $project->setDateCreated($created); $project->setDateUpdated(new \DateTime()); - - $updates = $this->getProjectUpdates($project, $context); - foreach ($updates as $update) { - $project->addUpdate($update); - } + $project->setTranslatableLocale($record['lang']); + $project->setUpdates(new ArrayCollection($this->getProjectUpdates($project, $context))); $conf = $this->getProjectConf($project, $context); $project->setDeadline($this->getProjectDeadline($conf)); - $project->setCalendar($this->calendarService->makeCalendar( - $project->getDeadline(), - new \DateTimeImmutable($record['published']), - $conf['days_round1'], - $conf['days_round2'], - )); + $project->setCalendar($this->getProjectCalendar($record)); + $project->addLocale($record['lang']); + $project->setTitle($record['name'] ?? ''); + $project->setSubtitle($record['subtitle'] ?? ''); + $project->setDescription($this->getProjectDescription($record)); + + $this->setPreventFlushAndClear(true); $this->persist($project, $context); + + $localizations = $this->getProjectLocalizations($project, $context); + + $this->setPreventFlushAndClear(false); + $this->localize($project, $localizations, $context, [ + 'title' => fn($l) => $l['name'], + 'description' => fn($l) => $this->getProjectDescription($l), + ]); + } + + private function getProject(array $record): ?Project + { + return $this->projectRepository->findOneBy(['migratedId' => $record['id']]); } private function getProjectOwner(array $record): ?User @@ -96,6 +115,55 @@ class ProjectsPump implements PumpInterface return $this->userRepository->findOneBy(['migratedId' => $record['owner']]); } + private function getProjectDescription(array $record): string + { + $lang = $record['lang']; + $hasTitles = \array_key_exists($lang, self::PROJECT_DESC_TITLES); + + $description = $record['description']; + + if ($hasTitles) { + $description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['about']); + } + + $description .= \sprintf("\n%s", $record['about']); + + if ($hasTitles) { + $description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['motivation']); + } + + $description .= \sprintf("\n\n%s", $record['motivation']); + + if ($hasTitles) { + $description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['related']); + } + + $description .= \sprintf("\n%s", $record['related']); + + return $description; + } + + private function getProjectLocalizations(Project $project, array $context): array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `project_lang` l WHERE l.id = :project' + ); + + $query->execute(['project' => $project->getMigratedId()]); + + return $query->fetchAll(); + } + + private function getProjectCalendar(array $record): ProjectCalendar + { + $calendar = new ProjectCalendar(); + $calendar->release = new \DateTimeImmutable($record['published']); + $calendar->minimum = new \DateTimeImmutable($record['passed'] ?? $record['closed']); + $calendar->optimum = new \DateTimeImmutable($record['success'] ?? $record['closed']); + + return $calendar; + } + private function getProjectStatus(array $record): ProjectStatus { switch ($record['status']) { @@ -148,24 +216,37 @@ class ProjectsPump implements PumpInterface } } - private function getProjectTerritory(array $record): ProjectTerritory + private function getProjectTerritory(array $record): Territory { + if ($record['project_location'] === null) { + return Territory::unknown(); + } + $cleanAddress = $this->cleanProjectLocation($record['project_location'], 2); if ($cleanAddress === '') { - return ProjectTerritory::unknown(); + return Territory::unknown(); } return $this->territoryService->search($cleanAddress); } - private function getProjectVideo(array $record): ?ProjectVideo + private function getProjectVideoSource(array $record): ?string { - if ($record['video'] === null) { - return null; + if ($record['video'] !== null) { + return \trim($record['video']); } - $url = \trim($record['video']); + if ($record['media'] !== null) { + return \trim($record['media']); + } + + return null; + } + + private function getProjectVideo(array $record): ?ProjectVideo + { + $url = $this->getProjectVideoSource($record); if ($url === '') { return null; @@ -206,12 +287,16 @@ class ProjectsPump implements PumpInterface $posts = $this->getProjectBlogPosts($project, $context); foreach ($posts as $post) { + if ($post['publish'] == 0) { + continue; + } + $update = new Update(); $update->setProject($project); $update->setTranslatableLocale($project->getLocales()[0]); $update->setTitle($post['title']); $update->setSubtitle($post['subtitle'] ?? ''); - $update->setBody($post['text']); + $update->setBody($post['text'] ?? ''); $update->setDate(new \DateTime($post['date'])); $updates[] = $update; diff --git a/src/Benzina/ProjectsPumpTrait.php b/src/Benzina/ProjectsPumpTrait.php index 1e51b2b..9bf33a1 100644 --- a/src/Benzina/ProjectsPumpTrait.php +++ b/src/Benzina/ProjectsPumpTrait.php @@ -23,7 +23,7 @@ trait ProjectsPumpTrait // Remove secondary conjoined places from locations // e.g: "España y el mundo" -> "España" - foreach ([' / ', ' | ', ' - ', ' y ', ' and ', ' & '] as $conjoinment) { + foreach ([' / ', ' | ', ' - ', ' y ', ' i ', ' and ', ' & '] as $conjoinment) { if (\str_contains($location, $conjoinment)) { $location = \explode($conjoinment, $location)[0]; } @@ -41,18 +41,6 @@ trait ProjectsPumpTrait // Process comma-separated address pieces $location = \explode(',', $location); - $location = \array_map(function ($l) { - $l = trim($l); - - // Normalize typos and name variations - foreach (self::COMMON_VARIATIONS as $standard => $variations) { - if (in_array(\mb_strtoupper($l), $variations)) { - $l = $standard; - } - } - - return $l; - }, $location); // Clean non desired location pieces $location = \array_filter($location, function ($l) { @@ -68,11 +56,26 @@ trait ProjectsPumpTrait return true; }); + // Normalize location pieces + $location = \array_map(function ($l) { + // Trim spaces and numbers at start of piece + $l = \preg_replace('/^[\d ]+/', '', \trim($l)); + + // Normalize typos and name variations + foreach (self::COMMON_VARIATIONS as $standard => $variations) { + if (in_array(\mb_strtoupper($l), $variations)) { + $l = $standard; + } + } + + return $l; + }, $location); + $location = \join(', ', \array_slice($location, -1 * $detailLevel)); // Trim remaining numbers and punctuation marks - $location = \preg_replace('/^[\d\.\-;]+/', '', $location); - $location = \preg_replace('/[\d\.\-;]+$/', '', $location); + $location = \preg_replace('/^[\d\.\,\-;]+/', '', $location); + $location = \preg_replace('/[\d\.\,\-;]+$/', '', $location); return \mb_strtoupper(\trim($location)); } @@ -81,22 +84,90 @@ trait ProjectsPumpTrait * @var array The standard preferred name and a list of possible variations and misspellings */ private const COMMON_VARIATIONS = [ - 'ESPAÑA' => ['ESPANYA', 'ESPANHA', 'ESPAGNE', 'SPAGNA', 'SPANIEN', 'SPAIN'], - 'CATALUÑA' => ['CATALUNYA', 'PAÏSOS CATALANS'], - 'ANDALUCÍA' => ['ANDALUCIA', 'ANDALUSIA'], + 'PERÚ' => ['PÉROU'], + 'ECUADOR' => ['EQUADOR'], + 'MÉXICO' => ['MESSICO', 'MX'], + 'CIUDAD DE MÉXICO' => ['CMDX'], + 'EUROPA' => ['EUROPEAN UNION'], 'FRANCIA' => ['FRANCE'], 'ITALIA' => ['ITALY'], 'ALEMANIA' => ['GERMANY'], + 'FINLANDIA' => ['FINLAND'], + 'SUECIA' => ['SWEDEN', 'SVERIGE'], + 'ESPAÑA' => ['ESPANYA', 'ESPANHA', 'ESPAGNE', 'SPAGNA', 'SPANIEN', 'SPAIN', 'ESTADO ESPAÑOL', 'ESPAINIA'], + 'ANDALUCÍA' => ['ANDALUCIA', 'ANDALUSIA'], 'CÁDIZ' => ['CADIZ'], + 'CÓRDOBA' => ['CORDOBA'], + 'COMUNIDAD VALENCIANA' => ['COMUNITAT VALENCIANA', 'PAÍS VALENCIÀ'], 'VALENCIA' => ['VALÈNCIA', 'PROVINCIA DE VALENCIA'], 'ALICANTE' => ['ALACANT'], + 'CASTELLÓN DE LA PLANA' => ['CASTELLÓ DE LA PLANA'], + 'LAS PALMAS DE GRAN CANARIA' => ['LAS PALMAS'], 'CÁCERES' => ['CACERES'], + 'MÉRIDA' => ['MERIDA'], + 'LEÓN' => ['LEON'], + 'LEGANÉS' => ['LEGANES'], 'BALEARES' => ['BALEAREN', 'ISLAS BALEARES', 'ILLES BALEARS', 'BALEARIC ISLANDS'], 'GALICIA' => ['GALIZA'], - 'BILBAO' => ['BILBO'], + 'A CORUÑA' => ['LA CORUÑA'], + 'OURENSE' => ['ORENSE'], + 'CATALUÑA' => ['CATALUNYA', 'PAÏSOS CATALANS'], 'LLEIDA' => ['LÉRIDA'], - 'BARCELONA' => ['PROVINCIA DE BARCELONA'], + 'TARRASA' => ['TERRASA'], + 'BARCELONA' => ['PROVINCIA DE BARCELONA', 'BARCELONE', 'A BARCELONA'], + 'HOSPITALET DE LLOBREGAT' => ["L'HOSPITALET DE LLOBREGAT"], + 'SANT ADRIÀ DE BESÒS' => ['SANT ADRIÀ DEL BESÒS'], 'MURCIA' => ['REGIÓN DE MURCIA'], + 'PAÍS VASCO' => ['EUSKADI', 'EUSKAL HERRIA'], + 'BILBAO' => ['BILBO'], + 'SAN SEBASTIÁN' => ['DONOSTIA', 'DONOSTIA-SAN SEBASTIÁN'], + 'VIZCAYA' => ['BIZKAIA'], + 'IRÚN' => ['IRUN'], + 'NAVARRA' => ['NAVARRE', 'NAFARROA'], + 'PAMLONA' => ['IRUÑEA'], + ]; + + private const PROJECT_DESC_TITLES = [ + 'es' => [ + 'about' => 'Descripción del proyecto. Características, fortalezas y diferenciales.', + 'motivation' => 'Motivación y a quién va dirigido el proyecto.', + 'related' => 'Experiencia previa y equipo.', + ], + 'gl' => [ + 'about' => 'Características básicas.', + 'motivation' => 'Motivación e a quen vai dirixido o proxecto.', + 'related' => 'Experiencia previa e equipo.', + ], + 'eu' => [ + 'about' => 'Oinarrizko ezaugarriak eta helburuak.', + 'motivation' => 'Zerk bultzatzen zaitu eta zeini zuzenduta dago proiektua.', + 'related' => 'Proiektuaren deskribapenean derrigorrekoa da azaltzea kontutan hartzen den taldearen esperientzia.', + ], + 'ca' => [ + 'about' => 'Característiques bàsiques i objectius.', + 'motivation' => 'Motivació i a qui va dirigit el projecte.', + 'related' => 'Experiència prèvia i equip.', + ], + 'en' => [ + 'about' => 'Main features and goals of the crowdfunding campaign.', + 'motivation' => 'Why this is important.', + 'related' => 'Team and experience.', + ], + 'fr' => [ + 'about' => 'Caractéristiques principales.', + 'motivation' => 'Pourquoi est-ce important.', + 'related' => 'Expérience antérieure et équipe.', + ], + 'it' => [ + 'about' => 'Caratteristiche fondamentali.', + 'motivation' => 'Obiettivo e a chi si dirige il progetto.', + 'related' => 'Esperienza e team.', + ], + 'de' => [ + 'about' => 'Hauptmerkmale.', + 'motivation' => 'Warum das wichtig ist.', + 'related' => 'Team und Erfahrung.', + ], ]; private const PROJECT_KEYS = [ diff --git a/src/Benzina/ProjectsRewardsPump.php b/src/Benzina/ProjectsRewardsPump.php index 8f17ae8..aa197ea 100644 --- a/src/Benzina/ProjectsRewardsPump.php +++ b/src/Benzina/ProjectsRewardsPump.php @@ -2,7 +2,7 @@ namespace App\Benzina; -use App\Entity\Money; +use App\Entity\EmbeddableMoney; use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\Reward; @@ -14,7 +14,9 @@ use Goteo\Benzina\Pump\PumpInterface; class ProjectsRewardsPump implements PumpInterface { use ArrayPumpTrait; + use DatabasePumpTrait; use DoctrinePumpTrait; + use LocalizedPumpTrait; private const REWARD_KEYS = [ 'id', @@ -59,24 +61,34 @@ class ProjectsRewardsPump implements PumpInterface return; } - $project = $this->getBudgetProject($record); + $project = $this->getProject($record); if ($project === null) { return; } $reward = new Reward(); + $reward->addLocale($project->getLocales()[0]); $reward->setProject($project); + $reward->setMigrated(true); + $reward->setMigratedId($record['id']); $reward->setTitle($record['reward']); $reward->setDescription($record['description'] ?? $record['reward']); - $reward->setMoney(new Money($record['amount'] * 100, 'EUR')); - $reward->setHasUnits($record['units'] > 0); + $reward->setMoney(new EmbeddableMoney($record['amount'] * 100, 'EUR')); + $reward->setIsFinite($record['units'] > 0); $reward->setUnitsTotal($record['units'] ?? 0); - $reward->setUnitsAvailable($record['units'] ?? 0); + $this->setPreventFlushAndClear(true); $this->persist($reward, $context); + + $localizations = $this->getRewardLocalizations($reward, $context); + + $this->setPreventFlushAndClear(false); + $this->localize($reward, $localizations, $context, [ + 'title' => fn($l) => $l['reward'], + ]); } - private function getBudgetProject(array $record): ?Project + private function getProject(array $record): ?Project { return $this->projectRepository->findOneBy(['migratedId' => $record['project']]); } @@ -92,4 +104,15 @@ class ProjectsRewardsPump implements PumpInterface return BudgetItemType::Infrastructure; } } + + private function getRewardLocalizations(Reward $reward, array $context): array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `reward_lang` l WHERE l.id = :reward' + ); + + $query->execute(['reward' => $reward->getMigratedId()]); + + return $query->fetchAll(); + } } diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index a1ff962..f7b0892 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -59,20 +59,18 @@ class UsersPump implements PumpInterface $handle = UserService::asHandle($record['email']); } - $sequenceNumber = \substr($this->userCount, -2, 2); - - return \sprintf('%s_%s', $handle, $sequenceNumber); + return \sprintf('%s_%02d', $handle, $this->userCount % 100); } private function getDateCreated(array $record): \DateTime { - $created = new \DateTime($record['created']); + $created = new \DateTime($record['created'] ?? '0000-00-00'); if ($created > new \DateTime('2011-01-01')) { return $created; } - return new \DateTime($record['modified']); + return new \DateTime($record['modified'] ?? 'now'); } private function getUserType(array $record): UserType diff --git a/src/Dto/Gateway/ChargeUpdationDto.php b/src/Dto/Gateway/ChargeUpdationDto.php index d42c745..9ec3656 100644 --- a/src/Dto/Gateway/ChargeUpdationDto.php +++ b/src/Dto/Gateway/ChargeUpdationDto.php @@ -2,33 +2,21 @@ namespace App\Dto\Gateway; +use ApiPlatform\Metadata as API; use App\Gateway\ChargeStatus; -use App\Gateway\ChargeType; +use Symfony\Component\Validator\Constraints as Assert; class ChargeUpdationDto { /** - * The unique identifier for the charge. + * The unique identifier for the GatewayCharge. */ + #[API\ApiProperty(writable: false)] public int $id; /** - * The type of the charge. - */ - public ChargeType $type; - - /** - * The title or name of the charge. - */ - public string $title; - - /** - * A detailed description of the charge. - */ - public string $description; - - /** - * The current payment status of the charge. + * To ask for a refund, set the status `to_refund`. */ + #[Assert\Choice([ChargeStatus::ToRefund])] public ChargeStatus $status; } diff --git a/src/Dto/CheckoutUpdationDto.php b/src/Dto/Gateway/CheckoutUpdationDto.php similarity index 94% rename from src/Dto/CheckoutUpdationDto.php rename to src/Dto/Gateway/CheckoutUpdationDto.php index 02c3a13..6560f4a 100644 --- a/src/Dto/CheckoutUpdationDto.php +++ b/src/Dto/Gateway/CheckoutUpdationDto.php @@ -1,6 +1,6 @@ currency = 'EUR'; - } - public function getId(): ?int { return $this->id; @@ -107,6 +102,8 @@ class Accounting return $this->getProject(); case Tipjar::class: return $this->getTipjar(); + case MatchCall::class: + return $this->getMatchCall(); } return null; @@ -127,6 +124,8 @@ class Accounting return $this->setProject($owner); case Tipjar::class: return $this->setTipjar($owner); + case MatchCall::class: + return $this->setMatchCall($owner); } return $this; @@ -197,4 +196,21 @@ class Accounting return $this; } + + public function getMatchCall(): ?MatchCall + { + return $this->matchCall; + } + + public function setMatchCall(MatchCall $matchCall): static + { + // set the owning side of the relation if necessary + if ($matchCall->getAccounting() !== $this) { + $matchCall->setAccounting($this); + } + + $this->matchCall = $matchCall; + + return $this; + } } diff --git a/src/Entity/Accounting/Transaction.php b/src/Entity/Accounting/Transaction.php index 15d70d9..c630345 100644 --- a/src/Entity/Accounting/Transaction.php +++ b/src/Entity/Accounting/Transaction.php @@ -2,7 +2,7 @@ namespace App\Entity\Accounting; -use App\Entity\Money; +use App\Entity\EmbeddableMoney as Money; use App\Entity\Trait\TimestampedCreationEntity; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Accounting\TransactionRepository; @@ -21,6 +21,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[MapProvider(EntityMapProvider::class)] #[ORM\Table(name: 'accounting_transaction')] #[ORM\Entity(repositoryClass: TransactionRepository::class)] +#[ORM\Index(fields: ['origin'])] +#[ORM\Index(fields: ['target'])] class Transaction { use TimestampedCreationEntity; diff --git a/src/Entity/EmbeddableMoney.php b/src/Entity/EmbeddableMoney.php new file mode 100644 index 0000000..7fe9ef0 --- /dev/null +++ b/src/Entity/EmbeddableMoney.php @@ -0,0 +1,69 @@ +amount = $amount; + $this->currency = $currency; + $this->conversion = $conversion?->toArray(); + } + + public static function of(MoneyInterface $moneyInterface): self + { + return new self( + $moneyInterface->getAmount(), + $moneyInterface->getCurrency(), + $moneyInterface->getConversion() + ); + } + + public function getAmount(): int + { + return $this->amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function getConversion(): ?Conversion + { + return $this->conversion + ? Conversion::fromArray($this->conversion) + : null; + } +} diff --git a/src/Entity/Gateway/Charge.php b/src/Entity/Gateway/Charge.php index 50a4e49..a81ca5d 100644 --- a/src/Entity/Gateway/Charge.php +++ b/src/Entity/Gateway/Charge.php @@ -4,7 +4,8 @@ namespace App\Entity\Gateway; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; -use App\Entity\Money; +use App\Entity\EmbeddableMoney as Money; +use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; use App\Gateway\ChargeStatus; @@ -25,9 +26,11 @@ use Symfony\Component\Validator\Constraints as Assert; #[MapProvider(EntityMapProvider::class)] #[Gedmo\Loggable()] #[ORM\Table(name: 'checkout_charge')] +#[ORM\Index(fields: ['migratedId'])] #[ORM\Entity(repositoryClass: ChargeRepository::class)] class Charge { + use MigratedEntity; use TimestampedCreationEntity; use TimestampedUpdationEntity; @@ -77,7 +80,7 @@ class Charge /** * @var Collection */ - #[ORM\JoinTable(name: 'checkout_charge_transaction')] + #[ORM\JoinTable(name: 'checkout_charge_trxs')] #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] private Collection $transactions; @@ -86,12 +89,11 @@ class Charge */ #[Gedmo\Versioned] #[ORM\Column()] - private ?ChargeStatus $status = null; + private ?ChargeStatus $status = ChargeStatus::InPending; public function __construct() { $this->transactions = new ArrayCollection(); - $this->status = ChargeStatus::InPending; } public function getId(): ?int diff --git a/src/Entity/Gateway/Checkout.php b/src/Entity/Gateway/Checkout.php index de40774..0b44f56 100644 --- a/src/Entity/Gateway/Checkout.php +++ b/src/Entity/Gateway/Checkout.php @@ -9,7 +9,6 @@ use App\Entity\Trait\TimestampedUpdationEntity; use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\RefundStrategy; -use App\Gateway\Tracking; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Gateway\CheckoutRepository; use App\Validator\SupportedChargeTypes; @@ -77,8 +76,6 @@ class Checkout /** * The address to where the user must be redirected to. */ - #[Assert\NotBlank()] - #[Assert\Url()] #[ORM\Column(type: Types::TEXT)] private ?string $returnUrl = null; @@ -99,18 +96,16 @@ class Checkout private array $links = []; /** - * A list of tracking codes provided by the Gateway for this checkout.\ - * e.g: Order ID, Payment Capture ID, Checkout Session Token. - * - * @var Tracking[] + * @var Collection */ - #[ORM\Column] - private array $trackings = []; + #[ORM\OneToMany(targetEntity: Tracking::class, mappedBy: 'checkout', cascade: ['persist'])] + private Collection $trackings; public function __construct() { $this->status = CheckoutStatus::InPending; $this->charges = new ArrayCollection(); + $this->trackings = new ArrayCollection(); } public function getId(): ?int @@ -268,41 +263,31 @@ class Checkout } /** - * @return Tracking[] + * @return Collection */ - public function getTrackings(): array + public function getTrackings(): Collection { - return array_map(fn($t) => Tracking::tryFrom($t), $this->trackings); - } - - /** - * @param Tracking[] $trackings - */ - public function setTrackings(array $trackings): static - { - $this->trackings = $trackings; - - return $this; + return $this->trackings; } public function addTracking(Tracking $tracking): static { - $this->removeTracking($tracking); - - $this->trackings = [...$this->trackings, $tracking]; + if (!$this->trackings->contains($tracking)) { + $this->trackings->add($tracking); + $tracking->setCheckout($this); + } return $this; } public function removeTracking(Tracking $tracking): static { - $this->trackings = \array_filter( - \array_map(fn($t) => Tracking::tryFrom($t), $this->trackings), - function (Tracking $existingTracking) use ($tracking) { - return $existingTracking->title !== $tracking->title - && $existingTracking->value !== $tracking->value; + if ($this->trackings->removeElement($tracking)) { + // set the owning side to null (unless already changed) + if ($tracking->getCheckout() === $this) { + $tracking->setCheckout(null); } - ); + } return $this; } diff --git a/src/Entity/Gateway/Tracking.php b/src/Entity/Gateway/Tracking.php new file mode 100644 index 0000000..733b63b --- /dev/null +++ b/src/Entity/Gateway/Tracking.php @@ -0,0 +1,78 @@ +title = $title ?? $title; + $this->value = $value ?? $value; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCheckout(): ?Checkout + { + return $this->checkout; + } + + public function setCheckout(?Checkout $checkout): static + { + $this->checkout = $checkout; + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(string $value): static + { + $this->value = $value; + + return $this; + } +} diff --git a/src/Entity/Matchfunding/MatchAgainst.php b/src/Entity/Matchfunding/MatchAgainst.php new file mode 100644 index 0000000..fc1959c --- /dev/null +++ b/src/Entity/Matchfunding/MatchAgainst.php @@ -0,0 +1,26 @@ + + */ + #[ORM\OneToMany(targetEntity: MatchStrategy::class, mappedBy: 'call', cascade: ['persist'])] + private Collection $strategies; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'call', targetEntity: MatchCallSubmission::class)] + private Collection $submissions; + + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: User::class)] + private Collection $managers; + + #[ORM\Column(length: 255)] + private ?string $title = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $description = null; + + #[ORM\Embedded(class: Territory::class)] + private ?Territory $territory = null; + + #[ORM\Column(enumType: MatchCallStatus::class)] + private ?MatchCallStatus $status = MatchCallStatus::InEditing; + + public function __construct() + { + $this->accounting = Accounting::of($this); + $this->managers = new ArrayCollection(); + $this->submissions = new ArrayCollection(); + $this->strategies = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getAccounting(): ?Accounting + { + return $this->accounting; + } + + public function setAccounting(Accounting $accounting): static + { + $this->accounting = $accounting; + + return $this; + } + + /** + * @return Collection + */ + public function getStrategies(): Collection + { + return $this->strategies; + } + + /** + * @param Collection $strategies + */ + public function setStrategies(Collection $strategies): static + { + $this->strategies = $strategies; + + return $this; + } + + public function addStrategy(MatchStrategy $strategy): static + { + if (!$this->strategies->contains($strategy)) { + $this->strategies = $this->strategies->map(function (MatchStrategy $s) use ($strategy) { + if ($s->getRanking() >= $strategy->getRanking()) { + $s->setRanking($s->getRanking() + 1); + } + + return $s; + }); + + $this->strategies->add($strategy); + + $strategy->setCall($this); + } + + return $this; + } + + public function removeStrategy(MatchStrategy $strategy): static + { + if ($this->strategies->removeElement($strategy)) { + $this->strategies = $this->strategies->map(function (MatchStrategy $s) use ($strategy) { + if ($s->getRanking() > $strategy->getRanking()) { + $s->setRanking($s->getRanking() - 1); + } + + return $s; + }); + } + + return $this; + } + + /** + * @return Collection + */ + public function getSubmissions(): Collection + { + return $this->submissions; + } + + public function addSubmission(MatchCallSubmission $submission): static + { + if (!$this->submissions->contains($submission)) { + $this->submissions->add($submission); + $submission->setCall($this); + } + + return $this; + } + + public function removeSubmission(MatchCallSubmission $submission): static + { + if ($this->submissions->removeElement($submission)) { + // set the owning side to null (unless already changed) + if ($submission->getCall() === $this) { + $submission->setCall(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getManagers(): Collection + { + return $this->managers; + } + + public function addManager(User $manager): static + { + if (!$this->managers->contains($manager)) { + $this->managers->add($manager); + } + + return $this; + } + + public function removeManager(User $manager): static + { + $this->managers->removeElement($manager); + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + + return $this; + } + + public function getTerritory(): ?Territory + { + return $this->territory; + } + + public function setTerritory(Territory $territory): static + { + $this->territory = $territory; + + return $this; + } + + public function getStatus(): ?MatchCallStatus + { + return $this->status; + } + + public function setStatus(MatchCallStatus $status): static + { + $this->status = $status; + + return $this; + } +} diff --git a/src/Entity/Matchfunding/MatchCallStatus.php b/src/Entity/Matchfunding/MatchCallStatus.php new file mode 100644 index 0000000..6d29c74 --- /dev/null +++ b/src/Entity/Matchfunding/MatchCallStatus.php @@ -0,0 +1,31 @@ +id; + } + + public function getCall(): ?MatchCall + { + return $this->call; + } + + public function setCall(?MatchCall $call): static + { + $this->call = $call; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): static + { + $this->project = $project; + + return $this; + } + + public function getStatus(): ?MatchCallSubmissionStatus + { + return $this->status; + } + + public function setStatus(MatchCallSubmissionStatus $status): static + { + $this->status = $status; + + return $this; + } +} diff --git a/src/Entity/Matchfunding/MatchCallSubmissionStatus.php b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php new file mode 100644 index 0000000..cd5c800 --- /dev/null +++ b/src/Entity/Matchfunding/MatchCallSubmissionStatus.php @@ -0,0 +1,28 @@ +setCall($call); + $strategy->setFormulaName($formulaName ?? MultiplicationFormula::getName()); + $strategy->setLimit(new Money( + 2147483647, + $call->getAccounting()->getCurrency() + )); + + return $strategy; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCall(): ?MatchCall + { + return $this->call; + } + + public function setCall(?MatchCall $call): static + { + $this->call = $call; + + return $this; + } + + public function getRanking(): ?int + { + return $this->ranking; + } + + public function setRanking(int $ranking): static + { + $this->ranking = $ranking; + + return $this; + } + + public function getRuleNames(): array + { + return $this->ruleNames; + } + + public function setRuleNames(array $ruleNames): static + { + $this->ruleNames = $ruleNames; + + return $this; + } + + public function getFormulaName(): ?string + { + return $this->formulaName; + } + + public function setFormulaName(string $formulaName): static + { + $this->formulaName = $formulaName; + + return $this; + } + + public function getLimit(): ?Money + { + return $this->limit; + } + + public function setLimit(Money $limit): static + { + $this->limit = $limit; + + return $this; + } + + public function getFactor(): ?float + { + return $this->factor; + } + + public function setFactor(float $factor): static + { + $this->factor = $factor; + + return $this; + } + + public function getAgainst(): ?MatchAgainst + { + return $this->against; + } + + public function setAgainst(MatchAgainst $against): static + { + $this->against = $against; + + return $this; + } +} diff --git a/src/Entity/Money.php b/src/Entity/Money.php deleted file mode 100644 index c3625d5..0000000 --- a/src/Entity/Money.php +++ /dev/null @@ -1,36 +0,0 @@ -amount = $amount; - $this->currency = $currency; - } -} diff --git a/src/Entity/Project/BudgetItem.php b/src/Entity/Project/BudgetItem.php index 73ab6b9..0d752f8 100644 --- a/src/Entity/Project/BudgetItem.php +++ b/src/Entity/Project/BudgetItem.php @@ -2,9 +2,10 @@ namespace App\Entity\Project; +use App\Entity\EmbeddableMoney as Money; use App\Entity\Interface\LocalizedEntityInterface; -use App\Entity\Money; use App\Entity\Trait\LocalizedEntityTrait; +use App\Entity\Trait\MigratedEntity; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\BudgetItemRepository; use AutoMapper\Attribute\MapProvider; @@ -17,6 +18,7 @@ use Gedmo\Mapping\Annotation as Gedmo; #[ORM\Entity(repositoryClass: BudgetItemRepository::class)] class BudgetItem implements LocalizedEntityInterface { + use MigratedEntity; use LocalizedEntityTrait; #[ORM\Id] diff --git a/src/Entity/Project/Project.php b/src/Entity/Project/Project.php index ce9dec2..a34b5dc 100644 --- a/src/Entity/Project/Project.php +++ b/src/Entity/Project/Project.php @@ -6,6 +6,9 @@ use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Interface\LocalizedEntityInterface; use App\Entity\Interface\UserOwnedInterface; +use App\Entity\Matchfunding\MatchCallSubmission; +use App\Entity\Matchfunding\MatchCallSubmissionStatus; +use App\Entity\Territory; use App\Entity\Trait\LocalizedEntityTrait; use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; @@ -22,6 +25,7 @@ use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; #[MapProvider(EntityMapProvider::class)] +#[ORM\Index(fields: ['migratedId'])] #[ORM\Entity(repositoryClass: ProjectRepository::class)] class Project implements UserOwnedInterface, AccountingOwnerInterface, LocalizedEntityInterface { @@ -66,8 +70,8 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized /** * Project's territory of interest. */ - #[ORM\Embedded(class: ProjectTerritory::class)] - private ?ProjectTerritory $territory; + #[ORM\Embedded(class: Territory::class)] + private ?Territory $territory; /** * The description body for the Project. @@ -109,6 +113,12 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized #[ORM\OneToMany(targetEntity: Reward::class, mappedBy: 'project')] private Collection $rewards; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'project', targetEntity: MatchCallSubmission::class)] + private Collection $matchCallSubmissions; + /** * @var Collection */ @@ -131,6 +141,7 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized { $this->accounting = Accounting::of($this); $this->rewards = new ArrayCollection(); + $this->matchCallSubmissions = new ArrayCollection(); $this->budgetItems = new ArrayCollection(); $this->updates = new ArrayCollection(); $this->supports = new ArrayCollection(); @@ -220,12 +231,12 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized return $this; } - public function getTerritory(): ?ProjectTerritory + public function getTerritory(): ?Territory { return $this->territory; } - public function setTerritory(ProjectTerritory $territory): static + public function setTerritory(Territory $territory): static { $this->territory = $territory; @@ -310,6 +321,44 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized return $this; } + /** + * @return Collection + */ + public function getMatchCallSubmissions(): Collection + { + return $this->matchCallSubmissions; + } + + /** + * @return Collection + */ + public function getMatchCallSubmissionsBy(MatchCallSubmissionStatus $status): Collection + { + return $this->matchCallSubmissions->filter(fn($s) => $s->getStatus() === $status); + } + + public function addMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static + { + if (!$this->matchCallSubmissions->contains($MatchCallSubmission)) { + $this->matchCallSubmissions->add($MatchCallSubmission); + $MatchCallSubmission->setProject($this); + } + + return $this; + } + + public function removeMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static + { + if ($this->matchCallSubmissions->removeElement($MatchCallSubmission)) { + // set the owning side to null (unless already changed) + if ($MatchCallSubmission->getProject() === $this) { + $MatchCallSubmission->setProject(null); + } + } + + return $this; + } + /** * @return Collection */ @@ -348,6 +397,16 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized return $this->updates; } + /** + * @param Collection $updates + */ + public function setUpdates(Collection $updates): static + { + $this->updates = $updates; + + return $this; + } + public function addUpdate(Update $update): static { if (!$this->updates->contains($update)) { diff --git a/src/Entity/Project/ProjectStatus.php b/src/Entity/Project/ProjectStatus.php index 388fa95..ba151fe 100644 --- a/src/Entity/Project/ProjectStatus.php +++ b/src/Entity/Project/ProjectStatus.php @@ -32,6 +32,11 @@ enum ProjectStatus: string */ case Rejected = 'rejected'; + /** + * An admin reviewed it and deemed worthy to campaign. + */ + case ToCampaign = 'to_campaign'; + /** * Project was reviewed and is in campaign for funding. */ diff --git a/src/Entity/Project/Reward.php b/src/Entity/Project/Reward.php index 27cbad5..13169e1 100644 --- a/src/Entity/Project/Reward.php +++ b/src/Entity/Project/Reward.php @@ -2,8 +2,10 @@ namespace App\Entity\Project; -use App\Entity\Money; +use App\Entity\EmbeddableMoney as Money; +use App\Entity\Interface\LocalizedEntityInterface; use App\Entity\Trait\LocalizedEntityTrait; +use App\Entity\Trait\MigratedEntity; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\RewardRepository; use AutoMapper\Attribute\MapProvider; @@ -19,8 +21,9 @@ use Gedmo\Mapping\Annotation as Gedmo; #[MapProvider(EntityMapProvider::class)] #[ORM\Table(name: 'project_reward')] #[ORM\Entity(repositoryClass: RewardRepository::class)] -class Reward +class Reward implements LocalizedEntityInterface { + use MigratedEntity; use LocalizedEntityTrait; #[ORM\Id] @@ -50,24 +53,30 @@ class Reward * Rewards might be finite, i.e: has a limited amount of existing unitsTotal. */ #[ORM\Column] - private ?bool $hasUnits = null; + private ?bool $isFinite = null; /** * For finite rewards, the total amount of existing unitsTotal. */ - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $unitsTotal = null; + /** + * The total amount of claims on this Reward. + */ + #[ORM\Column(nullable: true)] + private ?int $unitsClaimed = null; + /** * For finite rewards, the currently available amount of unitsTotal that can be claimed. */ - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $unitsAvailable = null; /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'reward', targetEntity: RewardClaim::class)] + #[ORM\OneToMany(mappedBy: 'reward', targetEntity: RewardClaim::class, cascade: ['persist'])] private Collection $claims; public function __construct() @@ -128,14 +137,14 @@ class Reward return $this; } - public function hasUnits(): bool + public function isFinite(): bool { - return $this->hasUnits; + return $this->isFinite; } - public function setHasUnits(bool $hasUnits): static + public function setIsFinite(bool $isFinite): static { - $this->hasUnits = $hasUnits; + $this->isFinite = $isFinite; return $this; } @@ -152,6 +161,18 @@ class Reward return $this; } + public function getUnitsClaimed(): ?int + { + return $this->unitsClaimed; + } + + public function setUnitsClaimed(int $unitsClaimed): static + { + $this->unitsClaimed = $unitsClaimed; + + return $this; + } + public function getUnitsAvailable(): ?int { return $this->unitsAvailable; @@ -177,6 +198,8 @@ class Reward if (!$this->claims->contains($claim)) { $this->claims->add($claim); $claim->setReward($this); + + $this->calcUnits(); } return $this; @@ -189,8 +212,19 @@ class Reward if ($claim->getReward() === $this) { $claim->setReward(null); } + + $this->calcUnits(); } return $this; } + + private function calcUnits() + { + $this->unitsClaimed = \count($this->claims); + + if ($this->isFinite()) { + $this->unitsAvailable = $this->unitsTotal - $this->unitsClaimed; + } + } } diff --git a/src/Entity/Project/RewardClaim.php b/src/Entity/Project/RewardClaim.php index 1707ea5..a68c4f6 100644 --- a/src/Entity/Project/RewardClaim.php +++ b/src/Entity/Project/RewardClaim.php @@ -23,35 +23,23 @@ class RewardClaim implements UserOwnedInterface #[ORM\Column] private ?int $id = null; - #[ORM\OneToOne(cascade: ['persist', 'remove'])] + #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] - private ?Charge $charge = null; + private ?User $owner = null; - #[ORM\ManyToOne(inversedBy: 'claims')] + #[ORM\ManyToOne(inversedBy: 'claims', cascade: ['persist'])] #[ORM\JoinColumn(nullable: false)] private ?Reward $reward = null; #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] - private ?User $owner = null; + private ?Charge $charge = null; public function getId(): ?int { return $this->id; } - public function getCharge(): ?Charge - { - return $this->charge; - } - - public function setCharge(Charge $charge): static - { - $this->charge = $charge; - - return $this; - } - public function getReward(): ?Reward { return $this->reward; @@ -63,4 +51,16 @@ class RewardClaim implements UserOwnedInterface return $this; } + + public function getCharge(): ?Charge + { + return $this->charge; + } + + public function setCharge(?Charge $charge): static + { + $this->charge = $charge; + + return $this; + } } diff --git a/src/Entity/Project/Support.php b/src/Entity/Project/Support.php index efa8d18..4a67613 100644 --- a/src/Entity/Project/Support.php +++ b/src/Entity/Project/Support.php @@ -2,17 +2,21 @@ namespace App\Entity\Project; -use App\Entity\Gateway\Charge; -use App\Entity\User\User; +use App\Entity\Accounting\Accounting; +use App\Entity\Accounting\Transaction; +use App\Entity\EmbeddableMoney; use App\Mapping\Provider\EntityMapProvider; use App\Repository\Project\SupportRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Doctrine\ORM\Mapping\Embedded; #[MapProvider(EntityMapProvider::class)] #[ORM\Table(name: 'project_support')] +#[ORM\UniqueConstraint(fields: ['project', 'origin'], name: 'project_origin_unique')] #[ORM\Entity(repositoryClass: SupportRepository::class)] class Support { @@ -21,32 +25,35 @@ class Support #[ORM\Column] private ?int $id = null; - #[ORM\ManyToOne(inversedBy: 'supports')] - #[ORM\JoinColumn(nullable: false)] - private ?User $owner = null; - #[ORM\ManyToOne(inversedBy: 'supports')] #[ORM\JoinColumn(nullable: false)] private ?Project $project = null; + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: false)] + private ?Accounting $origin = null; + /** - * @var Collection + * @var Collection */ - #[ORM\JoinTable(name: 'project_support_charges')] + #[ORM\JoinTable(name: 'project_support_trxs')] #[ORM\JoinColumn(name: 'support_id', referencedColumnName: 'id')] - #[ORM\InverseJoinColumn(name: 'charge_id', referencedColumnName: 'id')] - #[ORM\ManyToMany(targetEntity: Charge::class)] - private Collection $charges; + #[ORM\InverseJoinColumn(name: 'transaction_id', referencedColumnName: 'id')] + #[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])] + private Collection $transactions; + + #[Embedded(class: EmbeddableMoney::class)] + private ?EmbeddableMoney $money = null; #[ORM\Column] - private ?bool $anonymous = null; + private bool $anonymous = false; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $message = null; public function __construct() { - $this->charges = new ArrayCollection(); + $this->transactions = new ArrayCollection(); } public function getId(): ?int @@ -54,14 +61,14 @@ class Support return $this->id; } - public function getOwner(): ?User + public function getOrigin(): ?Accounting { - return $this->owner; + return $this->origin; } - public function setOwner(?User $owner): static + public function setOrigin(?Accounting $origin): static { - $this->owner = $owner; + $this->origin = $origin; return $this; } @@ -79,30 +86,42 @@ class Support } /** - * @return Collection + * @return Collection */ - public function getCharges(): Collection + public function getTransactions(): Collection { - return $this->charges; + return $this->transactions; } - public function addCharge(Charge $charge): static + public function addTransaction(Transaction $transaction): static { - if (!$this->charges->contains($charge)) { - $this->charges->add($charge); + if (!$this->transactions->contains($transaction)) { + $this->transactions->add($transaction); } return $this; } - public function removeCharge(Charge $charge): static + public function removeTransaction(Transaction $transaction): static { - $this->charges->removeElement($charge); + $this->transactions->removeElement($transaction); return $this; } - public function isAnonymous(): ?bool + public function getMoney(): ?EmbeddableMoney + { + return $this->money; + } + + public function setMoney(EmbeddableMoney $money): static + { + $this->money = $money; + + return $this; + } + + public function isAnonymous(): bool { return $this->anonymous; } diff --git a/src/Entity/Project/ProjectTerritory.php b/src/Entity/Territory.php similarity index 53% rename from src/Entity/Project/ProjectTerritory.php rename to src/Entity/Territory.php index 274a744..d8bc3ae 100644 --- a/src/Entity/Project/ProjectTerritory.php +++ b/src/Entity/Territory.php @@ -1,30 +1,39 @@ subLvl2 = $subLvl2 ?? null; } - public static function unknown(): ProjectTerritory + public static function unknown(): Territory { - return new ProjectTerritory(self::COUNTRY_UNKNOWN_CODE); + return new Territory(self::COUNTRY_UNKNOWN_CODE); } } diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 34c03f9..3e12ccc 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -5,7 +5,6 @@ namespace App\Entity\User; use App\Entity\Accounting\Accounting; use App\Entity\Interface\AccountingOwnerInterface; use App\Entity\Project\Project; -use App\Entity\Project\Support; use App\Entity\Trait\MigratedEntity; use App\Entity\Trait\TimestampedCreationEntity; use App\Entity\Trait\TimestampedUpdationEntity; @@ -112,12 +111,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $avatar = null; - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: Support::class, mappedBy: 'owner')] - private Collection $supports; - public function __construct() { $this->accounting = Accounting::of($this); @@ -128,7 +121,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account $this->emailConfirmed = false; $this->active = false; - $this->supports = new ArrayCollection(); } public function getId(): ?int @@ -377,34 +369,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - - /** - * @return Collection - */ - public function getSupports(): Collection - { - return $this->supports; - } - - public function addSupport(Support $support): static - { - if (!$this->supports->contains($support)) { - $this->supports->add($support); - $support->setOwner($this); - } - - return $this; - } - - public function removeSupport(Support $support): static - { - if ($this->supports->removeElement($support)) { - // set the owning side to null (unless already changed) - if ($support->getOwner() === $this) { - $support->setOwner(null); - } - } - - return $this; - } } diff --git a/src/Entity/WalletFinancement.php b/src/Entity/Wallet/WalletFinancement.php similarity index 82% rename from src/Entity/WalletFinancement.php rename to src/Entity/Wallet/WalletFinancement.php index 34f7ddc..de8f454 100644 --- a/src/Entity/WalletFinancement.php +++ b/src/Entity/Wallet/WalletFinancement.php @@ -1,8 +1,9 @@ id; } - public function getMoney(): ?Money + public function getMoney(): ?EmbeddableMoney { return $this->money; } - public function setMoney(Money $money): static + public function setMoney(EmbeddableMoney $money): static { $this->money = $money; diff --git a/src/Entity/WalletStatement.php b/src/Entity/Wallet/WalletStatement.php similarity index 93% rename from src/Entity/WalletStatement.php rename to src/Entity/Wallet/WalletStatement.php index be15c70..1a91524 100644 --- a/src/Entity/WalletStatement.php +++ b/src/Entity/Wallet/WalletStatement.php @@ -1,10 +1,11 @@ balance; } - public function setBalance(Money $balance): static + public function setBalance(EmbeddableMoney $balance): static { $this->balance = $balance; diff --git a/src/EventListener/AccountingDefaultsListener.php b/src/EventListener/AccountingDefaultsListener.php new file mode 100644 index 0000000..cfbec9a --- /dev/null +++ b/src/EventListener/AccountingDefaultsListener.php @@ -0,0 +1,27 @@ +getCurrency() === null) { + $accounting->setCurrency($this->moneyService->getDefaultCurrency()); + } + } +} diff --git a/src/EventListener/GatewayCheckoutSupportsListener.php b/src/EventListener/GatewayCheckoutSupportsListener.php index f47e16e..860685c 100644 --- a/src/EventListener/GatewayCheckoutSupportsListener.php +++ b/src/EventListener/GatewayCheckoutSupportsListener.php @@ -2,74 +2,57 @@ namespace App\EventListener; +use App\Entity\Accounting\Accounting; +use App\Entity\Accounting\Transaction; +use App\Entity\EmbeddableMoney; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; use App\Entity\Project\Project; use App\Entity\Project\Support; -use App\Entity\User\User; +use App\Money\Money; +use App\Money\MoneyService; +use App\Repository\Project\SupportRepository; use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; -use Doctrine\ORM\Event\PostUpdateEventArgs; -use Doctrine\ORM\Event\PreUpdateEventArgs; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\PreFlushEventArgs; use Doctrine\ORM\Events; #[AsEntityListener( - event: Events::preUpdate, - method: 'preUpdate', - entity: Checkout::class -)] -#[AsEntityListener( - event: Events::postUpdate, - method: 'postUpdate', + event: Events::preFlush, + method: 'preFlush', entity: Checkout::class )] class GatewayCheckoutSupportsListener { - /** @var array */ - private array $supports; + public function __construct( + private SupportRepository $supportRepository, + private MoneyService $moneyService, + ) {} - public function preUpdate(Checkout $checkout, PreUpdateEventArgs $args): void + public function preFlush(Checkout $checkout, PreFlushEventArgs $args): void { - if (!$args->hasChangedField('status')) { - return; + /** @var EntityManagerInterface */ + $em = $args->getObjectManager(); + $uow = $em->getUnitOfWork(); + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$entity instanceof Checkout) { + continue; + } + + $changes = $uow->getEntityChangeSet($entity); + + if (!isset($changes['status']) || !$entity->isCharged()) { + continue; + } + + $supports = $this->prepareSupports($entity); + + foreach ($supports as $support) { + $em->persist($support); + $uow->computeChangeSet($em->getClassMetadata(Support::class), $support); + } } - - if ($checkout->isCharged()) { - $this->supports = $this->prepareSupports($checkout, $args->getObjectManager()); - } - } - - public function postUpdate(Checkout $checkout, PostUpdateEventArgs $args): void - { - if (empty($this->supports)) { - return; - } - - foreach ($this->supports as $key => $support) { - $args->getObjectManager()->persist($support); - - unset($this->supports[$key]); - } - - $args->getObjectManager()->flush(); - } - - /** - * Create ProjectSupport for the given data. - * - * @param Charge[] $charges - */ - private function createSupport(Project $project, User $owner, array $charges): Support - { - $projectSupport = new Support(); - $projectSupport->setProject($project); - $projectSupport->setOwner($owner); - $projectSupport->setAnonymous(false); - - foreach ($charges as $charge) { - $projectSupport->addCharge($charge); - } - - return $projectSupport; } /** @@ -77,26 +60,66 @@ class GatewayCheckoutSupportsListener */ public function prepareSupports(Checkout $checkout): array { + /** @var Charge[] */ $charges = $checkout->getCharges()->toArray(); - $owner = $checkout->getOrigin()->getUser(); + $origin = $checkout->getOrigin(); - // Group charges for projects - $chargesInProjectMap = []; + $projects = []; + $transactionsByProject = []; foreach ($charges as $charge) { $project = $charge->getTarget()->getProject(); - if ($project === null) { + + if (!$project) { continue; } - $chargesInProjectMap[$project->getId()][] = $charge; + $projectId = $project->getId(); + + $projects[$projectId] = $project; + $transactionsByProject[$projectId] = [ + ...$transactionsByProject[$projectId] ?? [], + ...$charge->getTransactions(), + ]; } $supports = []; - foreach ($chargesInProjectMap as $chargesInProject) { - $project = $chargesInProject[0]->getTarget()->getProject(); - $supports[] = $this->createSupport($project, $owner, $chargesInProject); + foreach ($transactionsByProject as $projectId => $transactions) { + $supports[] = $this->getSupport($projects[$projectId], $origin, $transactions); } return $supports; } + + /** + * Create ProjectSupport for the given data. + * + * @param Transaction[] $transactions + */ + private function getSupport(Project $project, Accounting $origin, array $transactions): Support + { + /** @var Support|null */ + $projectSupport = $this->supportRepository->findOneBy([ + 'project' => $project, + 'origin' => $origin, + ]); + + if (!$projectSupport) { + $projectSupport = new Support(); + } + + $projectSupport->setProject($project); + $projectSupport->setOrigin($origin); + $projectSupport->setAnonymous(false); + + $money = new Money(0, $project->getAccounting()->getCurrency()); + foreach ($transactions as $transaction) { + $money = $this->moneyService->add($transaction->getMoney(), $money); + + $projectSupport->addTransaction($transaction); + } + + $projectSupport->setMoney(EmbeddableMoney::of($money)); + + return $projectSupport; + } } diff --git a/src/EventListener/MatchfundingListener.php b/src/EventListener/MatchfundingListener.php new file mode 100644 index 0000000..711b773 --- /dev/null +++ b/src/EventListener/MatchfundingListener.php @@ -0,0 +1,66 @@ +charge = null; + + if (count($event->getEntityChangeSet()) === 0) { + return; + } + + if ($charge->getStatus() !== ChargeStatus::Charged) { + return; + } + + $transactions = $this->matchfunding->match($charge); + + if (\count($transactions) > 0) { + foreach ($transactions as $transaction) { + $charge->addTransaction($transaction); + } + + $this->charge = $charge; + } + } + + public function postUpdate() + { + if ($this->charge === null) { + return; + } + + $this->entityManager->persist($this->charge); + $this->entityManager->flush(); + + $this->charge = null; + } +} diff --git a/src/EventListener/TransactionCurrencyListener.php b/src/EventListener/TransactionCurrencyListener.php new file mode 100644 index 0000000..e3de416 --- /dev/null +++ b/src/EventListener/TransactionCurrencyListener.php @@ -0,0 +1,28 @@ +getMoney(); + $target = $transaction->getTarget(); + + if ($money->getCurrency() !== $target->getCurrency()) { + throw new \LogicException(\sprintf( + 'The Accounting with ID %s can only receive Transactions in %s currency. Please do a conversion operation before targeting an Accounting in a different currency.', + $target->getId(), + $target->getCurrency() + )); + } + } +} diff --git a/src/EventSubscriber/AuthorizationTokenSubscriber.php b/src/EventSubscriber/AuthorizationTokenSubscriber.php new file mode 100644 index 0000000..e9a11a4 --- /dev/null +++ b/src/EventSubscriber/AuthorizationTokenSubscriber.php @@ -0,0 +1,63 @@ + ['onView', EventPriorities::POST_WRITE], + KernelEvents::RESPONSE => ['onResponse', EventPriorities::POST_RESPOND], + ]; + } + + public function onView(ViewEvent $event): void + { + $resourceClass = $event->getRequest()->attributes->get('_api_resource_class'); + + if ($resourceClass !== UserTokenApiResource::class) { + return; + } + + $event->getRequest()->attributes->set('authTokenMethod', $event->getRequest()->getMethod()); + + if ($event->getRequest()->getMethod() === Request::METHOD_POST) { + $token = $event->getControllerResult(); + + $event->getRequest()->attributes->set('authTokenValue', $token->token); + } + } + + public function onResponse(ResponseEvent $event): void + { + $token = $event->getRequest()->attributes->get('authTokenValue'); + $method = $event->getRequest()->attributes->get('authTokenMethod'); + + if (!$method) { + return; + } + + switch ($method) { + case Request::METHOD_DELETE: + $event->getResponse()->headers->clearCookie(AuthService::AUTH_COOKIE_NAME); + break; + case Request::METHOD_POST: + $event->getResponse()->headers->setCookie($this->authService->generateCookie($token)); + break; + } + } +} diff --git a/src/Factory/Project/ProjectFactory.php b/src/Factory/Project/ProjectFactory.php index e15adef..d347141 100644 --- a/src/Factory/Project/ProjectFactory.php +++ b/src/Factory/Project/ProjectFactory.php @@ -6,7 +6,7 @@ use App\Entity\Project\Project; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Entity\User\User; use App\Factory\User\UserFactory; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; @@ -44,7 +44,7 @@ final class ProjectFactory extends PersistentProxyObjectFactory 'category' => ProjectCategory::Design, 'deadline' => ProjectDeadline::Minimum, 'description' => '', - 'territory' => new ProjectTerritory('ES'), + 'territory' => new Territory('ES'), 'owner' => new User(), 'status' => ProjectStatus::InEditing, 'subtitle' => 'Subtitle', @@ -58,7 +58,7 @@ final class ProjectFactory extends PersistentProxyObjectFactory 'category' => self::faker()->randomElement(ProjectCategory::cases()), 'deadline' => self::faker()->randomElement(ProjectDeadline::cases()), 'description' => self::faker()->text(), - 'territory' => new ProjectTerritory('ES'), + 'territory' => new Territory('ES'), 'owner' => UserFactory::createWithMode(), 'status' => self::faker()->randomElement(ProjectStatus::cases()), 'subtitle' => self::faker()->text(255), diff --git a/src/Filter/GatewayFilter.php b/src/Filter/GatewayFilter.php new file mode 100644 index 0000000..4a62d8d --- /dev/null +++ b/src/Filter/GatewayFilter.php @@ -0,0 +1,99 @@ +isPropertyEnabled($property, $resourceClass) + ) { + return; + } + + $values = (array) $value; + $rootAlias = $queryBuilder->getRootAliases()[0]; + $parameterName = ':'.$queryNameGenerator->generateParameterName($property); + + if ($this->isPropertyNested($property, $resourceClass)) { + [$alias] = $this->addJoinsForNestedProperty($property, $rootAlias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN); + } + + $aliasedField = \sprintf('%s.gatewayName', $alias); + + if (\count($values) === 1) { + $queryBuilder + ->andWhere($queryBuilder->expr()->eq($aliasedField, $parameterName)) + ->setParameter($parameterName, $this->getGatewayName($values[0])); + + return; + } + + $queryBuilder + ->andWhere($queryBuilder->expr()->in($aliasedField, $parameterName)) + ->setParameter($parameterName, array_map(fn($v) => $this->getGatewayName($v), $values)); + + return; + } + + private function getGatewayName(mixed $value): string + { + /** @var GatewayApiResource */ + $gateway = $this->iriConverter->getResourceFromIri($value); + + return $gateway->name; + } + + public function getDescription(string $resourceClass): array + { + $description = []; + + foreach ($this->properties as $property => $strategy) { + $description[$property] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + 'is_collection' => false, + ]; + + $description[$property.'[]'] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_ARRAY, + 'required' => false, + 'is_collection' => true, + ]; + } + + return $description; + } +} diff --git a/src/Gateway/Gateway/CashGateway.php b/src/Gateway/Gateway/CashGateway.php index 7718b17..31ac495 100644 --- a/src/Gateway/Gateway/CashGateway.php +++ b/src/Gateway/Gateway/CashGateway.php @@ -2,12 +2,42 @@ namespace App\Gateway\Gateway; +use App\Entity\Gateway\Charge; +use App\Entity\Gateway\Checkout; use App\Gateway\GatewayInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; -abstract class CashGateway implements GatewayInterface +class CashGateway implements GatewayInterface { public static function getName(): string { return 'cash'; } + + public static function getSupportedChargeTypes(): array + { + return []; + } + + public function process(Checkout $checkout): Checkout + { + return $checkout; + } + + public function handleRedirect(Request $request): RedirectResponse + { + return new Response(); + } + + public function handleWebhook(Request $request): Response + { + return new Response(); + } + + public function processRefund(Charge $charge): void + { + return; + } } diff --git a/src/Gateway/Gateway/CecaGateway.php b/src/Gateway/Gateway/CecaGateway.php index 6f0771e..63d7d00 100644 --- a/src/Gateway/Gateway/CecaGateway.php +++ b/src/Gateway/Gateway/CecaGateway.php @@ -2,12 +2,47 @@ namespace App\Gateway\Gateway; +use App\Entity\Gateway\Charge; +use App\Entity\Gateway\Checkout; +use App\Gateway\ChargeType; use App\Gateway\GatewayInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; -abstract class CecaGateway implements GatewayInterface +class CecaGateway implements GatewayInterface { + public const LEGACY_MESSAGE = 'This Gateway implementation is only meant to give support to legacy-records. DO NOT USE. DO NOT IMPLEMENT.'; + public static function getName(): string { return 'tpv'; } + + public static function getSupportedChargeTypes(): array + { + return [ + ChargeType::Single, + ]; + } + + public function process(Checkout $checkout): Checkout + { + throw new \Exception(self::LEGACY_MESSAGE); + } + + public function handleRedirect(Request $request): RedirectResponse + { + throw new \Exception(self::LEGACY_MESSAGE); + } + + public function handleWebhook(Request $request): Response + { + throw new \Exception(self::LEGACY_MESSAGE); + } + + public function processRefund(Charge $charge): void + { + throw new \Exception(self::LEGACY_MESSAGE); + } } diff --git a/src/Gateway/Paypal/PaypalGateway.php b/src/Gateway/Paypal/PaypalGateway.php index 4c07538..3d44132 100644 --- a/src/Gateway/Paypal/PaypalGateway.php +++ b/src/Gateway/Paypal/PaypalGateway.php @@ -4,12 +4,12 @@ namespace App\Gateway\Paypal; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\Tracking; use App\Gateway\AbstractGateway; use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\LinkType; -use App\Gateway\Tracking; use Brick\Money\Money as BrickMoney; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -64,9 +64,7 @@ class PaypalGateway extends AbstractGateway 'payment_source' => $this->getPaypalPaymentSource($checkout), ]); - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_ORDER; - $tracking->value = $order['id']; + $tracking = new Tracking(self::TRACKING_TITLE_ORDER, $order['id']); $checkout->addTracking($tracking); @@ -111,9 +109,10 @@ class PaypalGateway extends AbstractGateway } foreach ($capture['purchase_units'] as $purchaseUnit) { - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_TRANSACTION; - $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; + $tracking = new Tracking( + self::TRACKING_TITLE_TRANSACTION, + $purchaseUnit['payments']['captures'][0]['id'] + ); $checkout->addTracking($tracking); } @@ -189,9 +188,10 @@ class PaypalGateway extends AbstractGateway } foreach ($event['resource']['purchase_units'] as $purchaseUnit) { - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_TRANSACTION; - $tracking->value = $purchaseUnit['payments']['captures'][0]['id']; + $tracking = new Tracking( + self::TRACKING_TITLE_TRANSACTION, + $purchaseUnit['payments']['captures'][0]['id'] + ); $checkout->addTracking($tracking); } @@ -263,16 +263,15 @@ class PaypalGateway extends AbstractGateway public function processRefund(Charge $charge): void { $trackings = $charge->getCheckout()->getTrackings(); - $captureTracking = current(array_filter( - $trackings, - fn(Tracking $tracking) => $tracking->title === self::TRACKING_TITLE_TRANSACTION && $tracking->value - )); + $captureTracking = $trackings->filter(function (Tracking $t) { + return $t->getTitle() === self::TRACKING_TITLE_TRANSACTION && $t->getValue(); + })->current(); if (!$captureTracking) { - throw new \Exception('PayPal capture ID not found in tracking.'); + throw new \Exception('Tracking for PayPal capture ID not found.'); } - $captureId = $captureTracking->value; + $captureId = $captureTracking->getValue(); $response = $this->paypal->refundCapture($captureId, [ 'amount' => $this->getPaypalMoney($charge), diff --git a/src/Gateway/Stripe/StripeGateway.php b/src/Gateway/Stripe/StripeGateway.php index 31b0d4f..96a7198 100644 --- a/src/Gateway/Stripe/StripeGateway.php +++ b/src/Gateway/Stripe/StripeGateway.php @@ -4,6 +4,7 @@ namespace App\Gateway\Stripe; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; +use App\Entity\Gateway\Tracking; use App\Entity\Project\Project; use App\Entity\User\User; use App\Gateway\AbstractGateway; @@ -11,7 +12,6 @@ use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\Link; use App\Gateway\LinkType; -use App\Gateway\Tracking; use App\Service\Gateway\CheckoutService; use Stripe\Checkout\Session as StripeSession; use Stripe\StripeClient; @@ -71,12 +71,7 @@ class StripeGateway extends AbstractGateway $link->type = LinkType::Payment; $checkout->addLink($link); - - $tracking = new Tracking(); - $tracking->title = self::TRACKING_TITLE_CHECKOUT; - $tracking->value = $session->id; - - $checkout->addTracking($tracking); + $checkout->addTracking(new Tracking(self::TRACKING_TITLE_CHECKOUT, $session->id)); return $checkout; } @@ -209,22 +204,15 @@ class StripeGateway extends AbstractGateway public function processRefund(Charge $charge): void { $trackings = $charge->getCheckout()->getTrackings(); - $filteredTrackings = array_filter( - $trackings, - fn(Tracking $tracking) => $tracking->title === self::TRACKING_TITLE_CHECKOUT - ); + $sessionTracking = $trackings->filter(function (Tracking $t) { + return $t->getTitle() === self::TRACKING_TITLE_CHECKOUT; + })->current(); - $firstTracking = $filteredTrackings[0] ?? null; - - if (!$firstTracking || $firstTracking->title !== self::TRACKING_TITLE_CHECKOUT) { - throw new \Exception('Tracking not found or not related to Stripe checkout.'); + if (!$sessionTracking) { + throw new \Exception('Tracking for Stripe Session ID not found.'); } - $sessionId = $firstTracking->value; - - if (!$sessionId) { - throw new \Exception('Session ID not found for the charge.'); - } + $sessionId = $sessionTracking->getValue(); $session = $this->stripe->checkout->sessions->retrieve($sessionId); diff --git a/src/Gateway/Tracking.php b/src/Gateway/Tracking.php index 0f48007..b611781 100644 --- a/src/Gateway/Tracking.php +++ b/src/Gateway/Tracking.php @@ -2,32 +2,15 @@ namespace App\Gateway; -use Symfony\Component\Validator\Constraints as Assert; - class Tracking { /** * A descriptive title for the tracking number. */ - #[Assert\NotBlank()] public string $title; /** * The tracking number given by the Gateway. */ - #[Assert\NotBlank()] public string $value; - - public static function tryFrom($value): Tracking - { - if ($value instanceof Tracking) { - return $value; - } - - $tracking = new Tracking(); - $tracking->title = $value['title']; - $tracking->value = $value['value']; - - return $tracking; - } } diff --git a/src/Gateway/Wallet/WalletGateway.php b/src/Gateway/Wallet/WalletGateway.php index e380239..d0bcbde 100644 --- a/src/Gateway/Wallet/WalletGateway.php +++ b/src/Gateway/Wallet/WalletGateway.php @@ -5,12 +5,12 @@ namespace App\Gateway\Wallet; use App\Entity\Accounting\Transaction; use App\Entity\Gateway\Charge; use App\Entity\Gateway\Checkout; -use App\Entity\Money; use App\Gateway\ChargeStatus; use App\Gateway\ChargeType; use App\Gateway\CheckoutStatus; use App\Gateway\GatewayInterface; -use App\Library\Economy\MoneyService; +use App\Money\Money; +use App\Money\MoneyService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Gateway/Wallet/WalletService.php b/src/Gateway/Wallet/WalletService.php index 3ef2724..66f14a2 100644 --- a/src/Gateway/Wallet/WalletService.php +++ b/src/Gateway/Wallet/WalletService.php @@ -4,11 +4,12 @@ namespace App\Gateway\Wallet; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; -use App\Entity\Money; -use App\Entity\WalletFinancement; -use App\Entity\WalletStatement; -use App\Library\Economy\MoneyService; -use App\Repository\WalletStatementRepository; +use App\Entity\EmbeddableMoney; +use App\Entity\Wallet\WalletFinancement; +use App\Entity\Wallet\WalletStatement; +use App\Money\Money; +use App\Money\MoneyService; +use App\Repository\Wallet\WalletStatementRepository; use Brick\Money as Brick; use Doctrine\ORM\EntityManagerInterface; @@ -39,7 +40,10 @@ class WalletService $total = $total->plus($balance); } - return $this->money->toMoney($total); + return new Money( + $total->getMinorAmount()->toInt(), + $total->getCurrency()->getCurrencyCode() + ); } /** @@ -77,7 +81,7 @@ class WalletService $origin = $transaction->getOrigin(); $spendGoal = $transaction->getMoney(); - $spentTotal = new Money(0, $spendGoal->currency); + $spentTotal = new Money(0, $spendGoal->getCurrency()); $outgoing = new WalletStatement(); $outgoing->setTransaction($transaction); @@ -85,7 +89,7 @@ class WalletService $statements = $this->getStatements($origin); foreach ($statements as $statement) { - if ($spendGoal->amount === 0) { + if ($spendGoal->getAmount() === 0) { break; } @@ -94,9 +98,9 @@ class WalletService } $incoming = $statement; - $balance = $incoming->getBalance(); - if ($balance->amount === 0) { + $balance = $incoming->getBalance(); + if ($balance->getAmount() === 0) { continue; } @@ -107,12 +111,14 @@ class WalletService } $financement = new WalletFinancement(); - $financement->setMoney($balanceSpent); + $financement->setMoney(EmbeddableMoney::of($balanceSpent)); - $incoming->setBalance($this->money->substract($balanceSpent, $balance)); + $substracted = $this->money->substract($balanceSpent, $balance); + $incoming->setBalance(EmbeddableMoney::of($substracted)); $incoming->addFinancesTo($financement); - $outgoing->setBalance($this->money->add($balanceSpent, $spentTotal)); + $added = $this->money->add($balanceSpent, $spentTotal); + $outgoing->setBalance(EmbeddableMoney::of($added)); $outgoing->addFinancedBy($financement); $this->entityManager->persist($incoming); diff --git a/src/Mapping/AutoMapper.php b/src/Mapping/AutoMapper.php index 1e20c33..d63aedd 100644 --- a/src/Mapping/AutoMapper.php +++ b/src/Mapping/AutoMapper.php @@ -4,13 +4,15 @@ namespace App\Mapping; use AutoMapper\AutoMapper as InnerMapper; use AutoMapper\AutoMapperInterface; +use AutoMapper\MapperContext; class AutoMapper implements AutoMapperInterface { public const CACHE_DIR = 'automapper'; public const DEFAULT_CONTEXT = [ - 'skip_null_values' => true, + MapperContext::DEPTH => 1, + MapperContext::SKIP_NULL_VALUES => true, ]; private AutoMapperInterface $innerMapper; diff --git a/src/Mapping/Provider/TrackingMapProvider.php b/src/Mapping/Provider/TrackingMapProvider.php new file mode 100644 index 0000000..34b4548 --- /dev/null +++ b/src/Mapping/Provider/TrackingMapProvider.php @@ -0,0 +1,25 @@ +trackingRepository->findOneBy([ + 'title' => $source->title, + 'value' => $source->value, + ]); + } +} diff --git a/src/Mapping/Transformer/AccountingBalanceMapTransformer.php b/src/Mapping/Transformer/AccountingBalanceMapTransformer.php deleted file mode 100644 index c829b35..0000000 --- a/src/Mapping/Transformer/AccountingBalanceMapTransformer.php +++ /dev/null @@ -1,22 +0,0 @@ -accountingService->calcBalance($source); - } -} diff --git a/src/Mapping/Transformer/BudgetMapTransformer.php b/src/Mapping/Transformer/BudgetMapTransformer.php index 9c5c019..c9bdd15 100644 --- a/src/Mapping/Transformer/BudgetMapTransformer.php +++ b/src/Mapping/Transformer/BudgetMapTransformer.php @@ -2,20 +2,20 @@ namespace App\Mapping\Transformer; +use App\ApiResource\ApiMoney; use App\ApiResource\Project\Budget; use App\ApiResource\Project\BudgetSummary; -use App\Entity\Money; use App\Entity\Project\BudgetItem; use App\Entity\Project\BudgetItemType; use App\Entity\Project\Project; use App\Entity\Project\ProjectDeadline; -use App\Library\Economy\MoneyService; +use App\Service\Project\BudgetService; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; class BudgetMapTransformer implements PropertyTransformerInterface { public function __construct( - private MoneyService $moneyService, + private BudgetService $budgetService, ) {} /** @@ -40,64 +40,31 @@ class BudgetMapTransformer implements PropertyTransformerInterface }); } - /** - * @param BudgetItem[] $items - * - * @return array{minimum: Money, optimum: Money} - */ - private function calcItemsTotal(array $items, string $currency): array - { - $totalMinimum = new Money(0, $currency); - $totalOptimum = new Money(0, $currency); - - foreach ($items as $item) { - $itemMoney = $item->getMoney(); - $itemDeadline = $item->getDeadline(); - - if ($itemDeadline == ProjectDeadline::Minimum) { - $totalMinimum = $this->moneyService->add($itemMoney, $totalMinimum); - } - - $totalOptimum = $this->moneyService->add($itemMoney, $totalOptimum); - } - - return [ - ProjectDeadline::Minimum->value => $totalMinimum, - ProjectDeadline::Optimum->value => $totalOptimum, - ]; - } - private function getItemsSummary(array $items, string $currency, ProjectDeadline $deadline) { - $deadlineValue = $deadline->value; + $deadline = $deadline->value; $summary = new BudgetSummary(); - $summary->money = $this->calcItemsTotal($items, $currency)[$deadlineValue]; - - $summary->task = $this->calcItemsTotal( - $this->filterItemsByType( - $items, - BudgetItemType::Task - ), + $summary->money = ApiMoney::of($this->budgetService->calcBudget( + $items, $currency - )[$deadlineValue]; + )[$deadline]); - $summary->material = $this->calcItemsTotal( - $this->filterItemsByType( - $items, - BudgetItemType::Material - ), + $summary->task = ApiMoney::of($this->budgetService->calcBudget( + $this->filterItemsByType($items, BudgetItemType::Task), $currency - )[$deadlineValue]; + )[$deadline]); - $summary->infra = $this->calcItemsTotal( - $this->filterItemsByType( - $items, - BudgetItemType::Infrastructure - ), + $summary->material = ApiMoney::of($this->budgetService->calcBudget( + $this->filterItemsByType($items, BudgetItemType::Material), $currency - )[$deadlineValue]; + )[$deadline]); + + $summary->infra = ApiMoney::of($this->budgetService->calcBudget( + $this->filterItemsByType($items, BudgetItemType::Infrastructure), + $currency + )[$deadline]); return $summary; } diff --git a/src/Mapping/Transformer/MatchFormulaMapTransformer.php b/src/Mapping/Transformer/MatchFormulaMapTransformer.php new file mode 100644 index 0000000..647123b --- /dev/null +++ b/src/Mapping/Transformer/MatchFormulaMapTransformer.php @@ -0,0 +1,21 @@ +formulaLocator->get($value); + + return FormulaApiResource::from($interface); + } +} diff --git a/src/Mapping/Transformer/MatchRulesMapTransformer.php b/src/Mapping/Transformer/MatchRulesMapTransformer.php new file mode 100644 index 0000000..804c374 --- /dev/null +++ b/src/Mapping/Transformer/MatchRulesMapTransformer.php @@ -0,0 +1,21 @@ +ruleLocator->getFrom($source); + + return \array_values(\array_map(fn($r) => RuleApiResource::from($r), $rules)); + } +} diff --git a/src/Mapping/Transformer/SupportMoneyMapTransformer.php b/src/Mapping/Transformer/SupportMoneyMapTransformer.php deleted file mode 100644 index 0aac7b7..0000000 --- a/src/Mapping/Transformer/SupportMoneyMapTransformer.php +++ /dev/null @@ -1,31 +0,0 @@ -getCharges(); - foreach ($charges as $charge) { - $money = $this->moneyService->add( - $charge->getMoney(), - $money ?? new Money(0, $charge->getMoney()->currency) - ); - } - - return $money; - } -} diff --git a/src/Matchfunding/Exception/FormulaDuplicatedException.php b/src/Matchfunding/Exception/FormulaDuplicatedException.php new file mode 100644 index 0000000..d5126e5 --- /dev/null +++ b/src/Matchfunding/Exception/FormulaDuplicatedException.php @@ -0,0 +1,22 @@ + */ + private array $formulasByName = []; + + public function __construct( + iterable $formulas, + ) { + foreach ($formulas as $formula) { + $formulaName = $formula::getName(); + + if (\array_key_exists($formulaName, $this->formulasByName)) { + throw new FormulaDuplicatedException( + $formulaName, + $formula::class, + $this->formulasByName[$formulaName]::class + ); + } + + $this->formulasByName[$formulaName] = $formula; + } + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->formulasByName; + } + + public function get(string $formulaName): ?FormulaInterface + { + if (!array_key_exists($formulaName, $this->formulasByName)) { + throw new FormulaNotFoundException($formulaName); + } + + return $this->formulasByName[$formulaName]; + } +} diff --git a/src/Matchfunding/Formula/MultiplicationFormula.php b/src/Matchfunding/Formula/MultiplicationFormula.php new file mode 100644 index 0000000..a989a8a --- /dev/null +++ b/src/Matchfunding/Formula/MultiplicationFormula.php @@ -0,0 +1,30 @@ +multipliedBy($factor) + ); + + return MoneyService::toMoney($delta); + } +} diff --git a/src/Matchfunding/Formula/PercentageFormula.php b/src/Matchfunding/Formula/PercentageFormula.php new file mode 100644 index 0000000..50328a5 --- /dev/null +++ b/src/Matchfunding/Formula/PercentageFormula.php @@ -0,0 +1,30 @@ +multipliedBy($factor)->dividedBy(100) + ); + + return MoneyService::toMoney($delta); + } +} diff --git a/src/Matchfunding/Rule/RuleInterface.php b/src/Matchfunding/Rule/RuleInterface.php new file mode 100644 index 0000000..326e8c2 --- /dev/null +++ b/src/Matchfunding/Rule/RuleInterface.php @@ -0,0 +1,25 @@ + */ + private array $rulesByName = []; + + public function __construct( + iterable $rules, + ) { + foreach ($rules as $rule) { + $name = RuleApiResource::from($rule)->name; + + $this->rulesByName[$name] = $rule; + } + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->rulesByName; + } + + /** + * @return array + */ + public function getFrom(MatchStrategy $strategy): array + { + $rules = $strategy->getRuleNames(); + + return \array_filter( + $this->rulesByName, + fn($v, $k) => \in_array($k, $rules), + ARRAY_FILTER_USE_BOTH + ); + } + + public function getOne(string $className): ?RuleInterface + { + if (!array_key_exists($className, $this->rulesByName)) { + throw new ClassNotFoundException($className); + } + + return $this->rulesByName[$className]; + } +} diff --git a/src/Matchfunding/Rule/SingleUserPerProjectRule.php b/src/Matchfunding/Rule/SingleUserPerProjectRule.php new file mode 100644 index 0000000..d1e44fb --- /dev/null +++ b/src/Matchfunding/Rule/SingleUserPerProjectRule.php @@ -0,0 +1,33 @@ +chargeRepository->findByOriginAndTarget( + $charge->getCheckout()->getOrigin(), + $submission->getProject()->getAccounting() + ); + + if (\count($charges) === 0) { + return true; + } + + return false; + } +} diff --git a/src/Money/Conversion/Conversion.php b/src/Money/Conversion/Conversion.php new file mode 100644 index 0000000..8d2107d --- /dev/null +++ b/src/Money/Conversion/Conversion.php @@ -0,0 +1,138 @@ +rounding = $rounding; + } + + if ($roundingMode) { + $this->rounding = self::getRoundingName($roundingMode); + } + + if ($this->rounding === null) { + throw new \Exception('Conversion rounding cannot be null'); + } + } + + /** + * @return Money the original Money that was converted from + */ + public function getFrom(): Money + { + return $this->from; + } + + /** + * @return Money the conversion result Money + */ + public function getTo(): Money + { + return $this->to; + } + + /** + * @return float the conversion rate given by the exchange rate provider + */ + public function getRate(): float + { + return $this->rate; + } + + /** + * @return string the date of the rate as given by the exchange rate provider + */ + public function getDate(): string + { + return $this->date; + } + + /** + * @return string the ExchangeInterface implementation name + */ + public function getProvider(): string + { + return $this->provider; + } + + /** + * @return string the rounding mode used during conversion + */ + public function getRounding(): string + { + return $this->rounding; + } + + public static function getRoundingName(RoundingMode $rounding): string + { + return match ($rounding) { + RoundingMode::UP => 'up', + RoundingMode::DOWN => 'down', + RoundingMode::FLOOR => 'floor', + RoundingMode::CEILING => 'ceiling', + RoundingMode::HALF_UP => 'half_up', + RoundingMode::HALF_DOWN => 'half_down', + RoundingMode::HALF_FLOOR => 'half_floor', + RoundingMode::HALF_CEILING => 'half_ceiling', + RoundingMode::HALF_EVEN => 'half_even', + }; + } + + public function toArray(): array + { + return [ + 'from' => [ + 'amount' => $this->from->getAmount(), + 'currency' => $this->from->getCurrency(), + ], + 'to' => [ + 'amount' => $this->to->getAmount(), + 'currency' => $this->to->getCurrency(), + ], + 'rate' => $this->rate, + 'date' => $this->date, + 'provider' => $this->provider, + 'rounding' => $this->rounding, + ]; + } + + public static function fromArray(array $data): self + { + return new self( + from: new Money( + $data['from']['amount'], + $data['from']['currency'] + ), + to: new Money( + $data['to']['amount'], + $data['to']['currency'] + ), + rate: (float) $data['rate'], + date: $data['date'], + provider: $data['provider'], + rounding: $data['rounding'] ?? null, + ); + } +} diff --git a/src/Money/Conversion/Exchange/AbstractExchange.php b/src/Money/Conversion/Exchange/AbstractExchange.php new file mode 100644 index 0000000..047a06e --- /dev/null +++ b/src/Money/Conversion/Exchange/AbstractExchange.php @@ -0,0 +1,58 @@ +converter->convert( + MoneyService::toBrick($from), + $toCurrency, + $context, + $roundingMode + ); + + return new Money( + $converted->getMinorAmount()->toInt(), + $converted->getCurrency()->getCurrencyCode(), + new Conversion( + $from, + MoneyService::toMoney($converted), + rate: $this->getConversionRate($from->getCurrency(), $toCurrency), + date: $this->getConversionDate($from->getCurrency(), $toCurrency), + provider: $this->getName(), + roundingMode: $roundingMode + ) + ); + } + + public function getConversionRate(string $fromCurrency, string $toCurrency): float + { + return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat(); + } + + public function getConversionDate(string $fromCurrency, string $toCurrency): string + { + return $this->date; + } +} diff --git a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php similarity index 72% rename from src/Library/Economy/Currency/EuropeanCentralBankExchange.php rename to src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php index 7fc7508..103c026 100644 --- a/src/Library/Economy/Currency/EuropeanCentralBankExchange.php +++ b/src/Money/Conversion/Exchange/EuropeanCentralBankExchange.php @@ -1,12 +1,8 @@ cache = new FilesystemAdapter(); @@ -56,7 +46,7 @@ class EuropeanCentralBankExchange implements ExchangeInterface $provider->setExchangeRate(self::ISO_4217, $rate['currency'], $rate['rate']); } - $this->date = $this->parseECBTime($data['@attributes']['time']); + $this->date = $data['@attributes']['time']; $this->provider = new BaseCurrencyProvider($provider, self::ISO_4217); $this->converter = new CurrencyConverter($this->provider); } @@ -71,33 +61,6 @@ class EuropeanCentralBankExchange implements ExchangeInterface return self::WEIGHT; } - public function convert(Money $money, string $toCurrency): Money - { - $converted = $this->converter->convert( - MoneyService::toBrick($money), - $toCurrency, - null, - RoundingMode::HALF_EVEN - ); - - return new Money( - $converted->getMinorAmount()->toInt(), - $converted->getCurrency()->getCurrencyCode() - ); - } - - public function getConversionRate(string $fromCurrency, string $toCurrency): float - { - return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat(); - } - - public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface - { - $this->provider->getExchangeRate($fromCurrency, $toCurrency); - - return $this->date; - } - public function getData(): array { try { @@ -137,7 +100,13 @@ class EuropeanCentralBankExchange implements ExchangeInterface throw new \Exception('Could not retrieve XML data'); } - return \json_decode(\json_encode($data), true)['Cube']['Cube']; + $data = \json_decode(\json_encode($data), true)['Cube']['Cube']; + + if (empty($data['@attributes']['time'])) { + throw new \Exception('XML data comes empty from ECB'); + } + + return $data; } private function getDataCached(): array @@ -148,7 +117,7 @@ class EuropeanCentralBankExchange implements ExchangeInterface return $this->getDataLatest(); }); - if (!$data) { + if (!$data || empty($data['@attributes']['time'])) { throw new \Exception('Could not retrieve cached data'); } diff --git a/src/Money/Conversion/Exchange/FrankfurterExchange.php b/src/Money/Conversion/Exchange/FrankfurterExchange.php new file mode 100644 index 0000000..e302c75 --- /dev/null +++ b/src/Money/Conversion/Exchange/FrankfurterExchange.php @@ -0,0 +1,43 @@ +request('GET', self::ENDPOINT); + $data = $response->toArray(); + + $provider = new ConfigurableProvider(); + foreach ($data['rates'] as $currency => $rate) { + $provider->setExchangeRate(self::ISO_4217, $currency, $rate); + } + + $this->date = $data['date']; + $this->provider = new BaseCurrencyProvider($provider, self::ISO_4217); + $this->converter = new CurrencyConverter($this->provider); + } +} diff --git a/src/Library/Economy/Currency/ExchangeInterface.php b/src/Money/Conversion/ExchangeInterface.php similarity index 64% rename from src/Library/Economy/Currency/ExchangeInterface.php rename to src/Money/Conversion/ExchangeInterface.php index a4c2aee..3ac73e6 100644 --- a/src/Library/Economy/Currency/ExchangeInterface.php +++ b/src/Money/Conversion/ExchangeInterface.php @@ -1,8 +1,10 @@ getWeight() < $b->getWeight(); + return $a->getWeight() <=> $b->getWeight(); }); foreach ($exchanges as $exchange) { diff --git a/src/Money/Money.php b/src/Money/Money.php new file mode 100644 index 0000000..917ddab --- /dev/null +++ b/src/Money/Money.php @@ -0,0 +1,29 @@ +amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function getConversion(): ?Conversion + { + return $this->conversion; + } +} diff --git a/src/Money/MoneyInterface.php b/src/Money/MoneyInterface.php new file mode 100644 index 0000000..5eaae53 --- /dev/null +++ b/src/Money/MoneyInterface.php @@ -0,0 +1,23 @@ +defaultCurrency; + } + + public static function toMoney(BrickMoney $brick): Money { return new Money( $brick->getMinorAmount()->toInt(), @@ -20,17 +25,17 @@ class MoneyService ); } - public static function toBrick(Money $money): Brick\Money + public static function toBrick(MoneyInterface $money): BrickMoney { - return Brick\Money::ofMinor($money->amount, $money->currency); + return BrickMoney::ofMinor($money->getAmount(), $money->getCurrency()); } /** * Adds `a` to `b`. */ - public function add(Money $a, Money $b): Money + public function add(MoneyInterface $a, MoneyInterface $b): Money { - $a = $this->convert($a, $b->currency); + $a = $this->convert($a, $b->getCurrency()); $ab = self::toBrick($b)->plus($a); return self::toMoney($ab); @@ -39,9 +44,9 @@ class MoneyService /** * Substracts `a` from `b`. */ - public function substract(Money $a, Money $b): Money + public function substract(MoneyInterface $a, MoneyInterface $b): Money { - $a = $this->convert($a, $b->currency); + $a = $this->convert($a, $b->getCurrency()); $ab = self::toBrick($b)->minus($a); return self::toMoney($ab); @@ -52,9 +57,9 @@ class MoneyService * * @return bool `true` if `$money` is less than `$than` */ - public function isLess(Money $money, Money $than): bool + public function isLess(MoneyInterface $money, MoneyInterface $than): bool { - $money = $this->convert($money, $than->currency); + $money = $this->convert($money, $than->getCurrency()); return $money->isLessThan(self::toBrick($than)); } @@ -64,16 +69,16 @@ class MoneyService * * @return bool `true` if `$money` is more than or same as `$than` */ - public function isMoreOrSame(Money $money, Money $than): bool + public function isMoreOrSame(MoneyInterface $money, MoneyInterface $than): bool { - $money = $this->convert($money, $than->currency); + $money = $this->convert($money, $than->getCurrency()); return $money->isGreaterThanOrEqualTo(self::toBrick($than)); } - private function convert(Money $money, string $toCurrency): Brick\Money + private function convert(MoneyInterface $money, string $toCurrency): BrickMoney { - $fromCurrency = $money->currency; + $fromCurrency = $money->getCurrency(); if ($fromCurrency === $toCurrency) { return self::toBrick($money); } diff --git a/src/Money/Totalization/TotalizedMoney.php b/src/Money/Totalization/TotalizedMoney.php new file mode 100644 index 0000000..4479a21 --- /dev/null +++ b/src/Money/Totalization/TotalizedMoney.php @@ -0,0 +1,30 @@ +amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + /** + * @return int the total number of items that were totalized to produce the Money + */ + public function getLength(): int + { + return $this->length; + } +} diff --git a/src/Money/Totalization/Totalizer/SupportTotalizer.php b/src/Money/Totalization/Totalizer/SupportTotalizer.php new file mode 100644 index 0000000..287168a --- /dev/null +++ b/src/Money/Totalization/Totalizer/SupportTotalizer.php @@ -0,0 +1,40 @@ + $items + */ + public function totalize(iterable $items): TotalizedMoney + { + $length = 0; + $money = null; + + foreach ($items as $support) { + ++$length; + $money = $this->moneyService->add( + $support->getMoney(), + $money ?? new Money(0, 'EUR') + ); + } + + return new TotalizedMoney($money->getAmount(), 'EUR', $length); + } +} diff --git a/src/Money/Totalization/TotalizerInterface.php b/src/Money/Totalization/TotalizerInterface.php new file mode 100644 index 0000000..eba570c --- /dev/null +++ b/src/Money/Totalization/TotalizerInterface.php @@ -0,0 +1,16 @@ + */ + private array $totalizers = []; + + public function __construct( + iterable $interfaces, + ) { + /** @var TotalizerInterface[] */ + $totalizers = \iterator_to_array($interfaces); + + foreach ($totalizers as $totalizer) { + $resource = $totalizer::getSupportedResource(); + + if (\array_key_exists($resource, $this->totalizers)) { + throw new \Exception(\sprintf( + "Duplicated TotalizerInterface. The resource '%s' is already supported by %s", + $resource, + $totalizers[$resource]::class + )); + } + + $this->totalizers[$resource] = $totalizer; + } + } + + public function get(string $resource): ?TotalizerInterface + { + return $this->totalizers[$resource] ?? null; + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->totalizers; + } +} diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index c285bee..a76323c 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -39,6 +39,16 @@ trait RedocExtensionsTrait 'ProjectSupport', ], ], + [ + 'name' => 'Matchfunding', + 'tags' => [ + 'MatchCall', + 'MatchCallSubmission', + 'MatchStrategy', + 'MatchFormula', + 'MatchRule', + ], + ], [ 'name' => 'Gateways', 'tags' => [ @@ -51,6 +61,7 @@ trait RedocExtensionsTrait 'name' => 'Accounting', 'tags' => [ 'Accounting', + 'AccountingBalance', 'AccountingBalancePoint', 'AccountingTransaction', ], diff --git a/src/Repository/Accounting/TransactionRepository.php b/src/Repository/Accounting/TransactionRepository.php index a160df0..d2c70f9 100644 --- a/src/Repository/Accounting/TransactionRepository.php +++ b/src/Repository/Accounting/TransactionRepository.php @@ -34,19 +34,15 @@ class TransactionRepository extends ServiceEntityRepository ?\DateTimeInterface $dateEnd = null, ): array { $queryBuilder = $this->createQueryBuilder('t'); - $accountingExpr = $queryBuilder->expr(); - - if ($dateEnd === null) { - $dateEnd = new \DateTime(); - } $query = $queryBuilder - ->andWhere($accountingExpr->orX( - $accountingExpr->eq('t.origin', $accounting->getId()), - $accountingExpr->eq('t.target', $accounting->getId()), + ->andWhere($queryBuilder->expr()->orX( + 't.origin = :accountingId', + 't.target = :accountingId' )) + ->setParameter('accountingId', $accounting->getId()) ->andWhere('t.dateCreated <= :dateEnd') - ->setParameter('dateEnd', $dateEnd); + ->setParameter('dateEnd', $dateEnd ?? new \DateTime()); if ($dateStart !== null) { $query->andWhere('t.dateCreated >= :dateStart') @@ -58,6 +54,45 @@ class TransactionRepository extends ServiceEntityRepository return $query->getQuery()->getResult(); } + /** + * @param \DateTimeInterface|null $dateStart a date to act as lower bound of the selection + * @param \DateTimeInterface|null $dateEnd A date to act as upper bound of the selection. Current date will be used if null. + * + * @return array Simplified array data of Transactions originated from or targeting the Accounting + */ + public function findForBalanceCalc( + Accounting $accounting, + ?\DateTimeInterface $dateStart = null, + ?\DateTimeInterface $dateEnd = null, + ): array { + $queryBuilder = $this->createQueryBuilder('t'); + + $query = $queryBuilder + ->select(' + t.id, + t.money.amount, + t.money.currency, + IDENTITY(t.origin) AS origin_id, + IDENTITY(t.target) AS target_id + ') + ->andWhere($queryBuilder->expr()->orX( + 't.origin = :accountingId', + 't.target = :accountingId' + )) + ->setParameter('accountingId', $accounting->getId()) + ->andWhere('t.dateCreated <= :dateEnd') + ->setParameter('dateEnd', $dateEnd ?? new \DateTime()); + + if ($dateStart !== null) { + $query->andWhere('t.dateCreated >= :dateStart') + ->setParameter('dateStart', $dateStart); + } + + $query->orderBy('t.id', 'ASC'); + + return $query->getQuery()->getArrayResult(); + } + /** * @return Transaction[] Transactions originated from the Accounting */ diff --git a/src/Repository/Gateway/ChargeRepository.php b/src/Repository/Gateway/ChargeRepository.php index c8a196c..16e410c 100644 --- a/src/Repository/Gateway/ChargeRepository.php +++ b/src/Repository/Gateway/ChargeRepository.php @@ -2,8 +2,10 @@ namespace App\Repository\Gateway; +use App\Entity\Accounting\Accounting; use App\Entity\Gateway\Charge; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\Persistence\ManagerRegistry; /** @@ -21,28 +23,20 @@ class ChargeRepository extends ServiceEntityRepository parent::__construct($registry, Charge::class); } - // /** - // * @return Charge[] Returns an array of Charge objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('g.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?Charge - // { - // return $this->createQueryBuilder('g') - // ->andWhere('g.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + /** + * @return Charge[] Returns an array of Charge objects + */ + public function findByOriginAndTarget(Accounting $origin, Accounting $target): array + { + return $this->createQueryBuilder('g') + ->join('g.checkout', 'c', Join::WITH, 'c.id = g.checkout') + ->where('c.origin = :origin') + ->andWhere('g.target = :target') + ->setParameter('origin', $origin) + ->setParameter('target', $target) + ->orderBy('g.id', 'ASC') + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/Gateway/TrackingRepository.php b/src/Repository/Gateway/TrackingRepository.php new file mode 100644 index 0000000..89af268 --- /dev/null +++ b/src/Repository/Gateway/TrackingRepository.php @@ -0,0 +1,43 @@ + + */ +class TrackingRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Tracking::class); + } + + // /** + // * @return Tracking[] Returns an array of Tracking objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Tracking + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/Matchfunding/MatchCallRepository.php b/src/Repository/Matchfunding/MatchCallRepository.php new file mode 100644 index 0000000..a7fb3cd --- /dev/null +++ b/src/Repository/Matchfunding/MatchCallRepository.php @@ -0,0 +1,43 @@ + + */ +class MatchCallRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MatchCall::class); + } + + // /** + // * @return MatchCall[] Returns an array of MatchCall objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('m.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?MatchCall + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/Matchfunding/MatchCallSubmissionRepository.php b/src/Repository/Matchfunding/MatchCallSubmissionRepository.php new file mode 100644 index 0000000..54b8646 --- /dev/null +++ b/src/Repository/Matchfunding/MatchCallSubmissionRepository.php @@ -0,0 +1,43 @@ + + */ +class MatchCallSubmissionRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MatchCallSubmission::class); + } + + // /** + // * @return MatchCallSubmission[] Returns an array of MatchCallSubmission objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('m.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?MatchCallSubmission + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/Matchfunding/MatchStrategyRepository.php b/src/Repository/Matchfunding/MatchStrategyRepository.php new file mode 100644 index 0000000..d4e9cbe --- /dev/null +++ b/src/Repository/Matchfunding/MatchStrategyRepository.php @@ -0,0 +1,43 @@ + + */ +class MatchStrategyRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MatchStrategy::class); + } + + /** + * @return MatchStrategy[] Returns an array of ranked MatchStrategy objects + */ + public function findByCall(MatchCall $call): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.call = :val') + ->setParameter('val', $call) + ->orderBy('m.ranking', 'ASC') + ->getQuery() + ->getResult() + ; + } + + // public function findOneBySomeField($value): ?MatchStrategy + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/WalletFinancementRepository.php b/src/Repository/Wallet/WalletFinancementRepository.php similarity index 94% rename from src/Repository/WalletFinancementRepository.php rename to src/Repository/Wallet/WalletFinancementRepository.php index 0704d8c..aad9e74 100644 --- a/src/Repository/WalletFinancementRepository.php +++ b/src/Repository/Wallet/WalletFinancementRepository.php @@ -1,8 +1,8 @@ cookies->has(AuthService::AUTH_COOKIE_NAME); + } + + public function authenticate(Request $request): Passport + { + $token = $request->cookies->get(AuthService::AUTH_COOKIE_NAME); + + if (!$token) { + throw new AuthenticationException(); + } + + return new SelfValidatingPassport($this->accessTokenHandler->getUserBadgeFrom($token)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new JsonResponse([ + 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()), + ], Response::HTTP_UNAUTHORIZED); + } +} diff --git a/src/Security/Voter/AccountingVoter.php b/src/Security/Voter/AccountingVoter.php index a681126..a41cce7 100644 --- a/src/Security/Voter/AccountingVoter.php +++ b/src/Security/Voter/AccountingVoter.php @@ -3,7 +3,6 @@ namespace App\Security\Voter; use App\ApiResource\Accounting\AccountingApiResource; -use App\ApiResource\Project\ProjectApiResource; use App\ApiResource\User\UserApiResource; use App\Entity\User\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -31,21 +30,21 @@ class AccountingVoter extends Voter $owner = $subject->getOwner(); switch ($owner::class) { - case ProjectApiResource::class: - return $this->voteOnProject($attribute, $owner, $user); case UserApiResource::class: return $this->voteOnUser($attribute, $owner, $user); + default: + return $this->voteOn($attribute, $subject, $user); } return false; } - private function voteOnProject(string $attribute, ProjectApiResource $project, ?User $user): bool + private function voteOn(string $attribute, mixed $subject, ?User $user): bool { switch ($attribute) { case self::EDIT: return $user->hasRoles(['ROLE_ADMIN']) - || $this->isOwnerOf($project, $user); + || $this->isOwnerOf($subject, $user); case self::VIEW: return true; } diff --git a/src/Security/Voter/MatchCallVoter.php b/src/Security/Voter/MatchCallVoter.php new file mode 100644 index 0000000..aaa172c --- /dev/null +++ b/src/Security/Voter/MatchCallVoter.php @@ -0,0 +1,54 @@ +getUser(); + + switch ($attribute) { + case self::EDIT: + return $this->canEdit($subject, $user); + case self::VIEW: + return true; + } + + return false; + } + + private function canEdit(MatchCallApiResource $call, ?User $user): bool + { + if (!$user) { + return false; + } + + if ($user->hasRoles(['ROLE_ADMIN'])) { + return true; + } + + if (\in_array($user->getId(), \array_map(fn($m) => $m->id, $call->managers))) { + return true; + } + + return false; + } +} diff --git a/src/Security/Voter/ProjectVoter.php b/src/Security/Voter/ProjectVoter.php index 61c3889..3aaa5d6 100644 --- a/src/Security/Voter/ProjectVoter.php +++ b/src/Security/Voter/ProjectVoter.php @@ -37,7 +37,7 @@ final class ProjectVoter extends Voter return false; } - private function canEdit(ProjectApiResource $project, User $user): bool + private function canEdit(ProjectApiResource $project, ?User $user): bool { if (!$user instanceof User) { return false; diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index 6be9760..09f7289 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -3,12 +3,14 @@ namespace App\Service; use App\ApiResource\Accounting\AccountingBalancePoint; +use App\ApiResource\ApiMoney; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; -use App\Entity\Money; use App\Entity\User\User; use App\Gateway\Wallet\WalletService; -use App\Library\Economy\MoneyService; +use App\Money\Money; +use App\Money\MoneyInterface; +use App\Money\MoneyService; use App\Repository\Accounting\TransactionRepository; class AccountingService @@ -27,16 +29,19 @@ class AccountingService return $this->wallet->getBalance($accounting); } - $balance = new Money(0, $accounting->getCurrency()); - $trxs = $this->transactionRepository->findByAccounting($accounting); + $trxs = $this->transactionRepository->findForBalanceCalc($accounting); - foreach ($trxs as $transaction) { - if ($transaction->getTarget() === $accounting) { - $balance = $this->money->add($transaction->getMoney(), $balance); + $accountingId = $accounting->getId(); + $balance = new Money(0, $accounting->getCurrency()); + foreach ($trxs as $trx) { + $transaction = new Money($trx['money.amount'], $trx['money.currency']); + + if ($trx['target_id'] === $accountingId) { + $balance = $this->money->add($transaction, $balance); } - if ($transaction->getOrigin() === $accounting) { - $balance = $this->money->substract($transaction->getMoney(), $balance); + if ($trx['origin_id'] === $accountingId) { + $balance = $this->money->substract($transaction, $balance); } } @@ -50,7 +55,7 @@ class AccountingService * * @return Transaction[] */ - private function getTransactionsInPeriod( + private function filterTransactions( array $transactions, \DateTimeInterface $start, \DateTimeInterface $end, @@ -61,13 +66,13 @@ class AccountingService private function createPoint( \DateTimeInterface $lowerBound, \DateTimeInterface $upperBound, - Money $balance, + MoneyInterface $balance, array $transactions, ): AccountingBalancePoint { $point = new AccountingBalancePoint(); $point->start = $lowerBound; $point->end = $upperBound; - $point->balance = $balance; + $point->balance = ApiMoney::of($balance); $point->length = count($transactions); return $point; @@ -92,22 +97,24 @@ class AccountingService $period->getStartDate(), $period->getEndDate(), ); + $points = []; $totalBalance = new Money(0, $accounting->getCurrency()); + $accountingId = $accounting->getId(); foreach ($period as $start) { $end = \DateTime::createFromInterface($start)->add($period->getDateInterval()); - $periodTrxs = $this->getTransactionsInPeriod($trxs, $start, $end); + $periodTrxs = $this->filterTransactions($trxs, $start, $end); $balance = $aggregate ? $totalBalance : new Money(0, $accounting->getCurrency()); foreach ($periodTrxs as $trx) { $trxMoney = $trx->getMoney(); - if ($trx->getTarget() === $accounting) { + if ($trx->getTarget()->getId() === $accountingId) { $balance = $this->money->add($trxMoney, $balance); } - if ($trx->getOrigin() === $accounting) { + if ($trx->getOrigin()->getId() === $accountingId) { $balance = $this->money->substract($trxMoney, $balance); } } diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index 1bd1d47..0a17a9a 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -6,9 +6,13 @@ use App\Entity\User\User; use App\Entity\User\UserToken; use App\Repository\User\UserRepository; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Cookie; class AuthService { + public const AUTH_COOKIE_NAME = 'authToken'; + public const AUTH_COOKIE_TTL = 86400; + private const TOKEN_HASH_ALGO = 'sha256'; private array $config; @@ -27,6 +31,24 @@ class AuthService return $this->config; } + private function getCookieTtl(): \DateTimeInterface + { + return (new \DateTime())->add(new \DateInterval(\sprintf('PT%dS', AuthService::AUTH_COOKIE_TTL))); + } + + public function generateCookie(string $token): Cookie + { + return new Cookie( + name: AuthService::AUTH_COOKIE_NAME, + value: $token, + expire: $this->getCookieTtl(), + path: '/', + secure: true, + httpOnly: true, + sameSite: 'Strict' + ); + } + public function generateUserToken(User $user, AuthTokenType $type): UserToken { $token = new UserToken(); diff --git a/src/Service/Embed/EmbedService.php b/src/Service/Embed/EmbedService.php index 49eab8f..bbd11bd 100644 --- a/src/Service/Embed/EmbedService.php +++ b/src/Service/Embed/EmbedService.php @@ -103,7 +103,11 @@ class EmbedService throw new \Exception(\sprintf('Could not extract embed data from %s', $url)); } - $data = $data[$url]; + $data = $data[\array_key_first($data)]; + + if (!\array_key_exists('html', $data)) { + throw new \Exception(\sprintf('Could not extract embed data from %s', $url)); + } return new EmbedVideo( $this->getIframeSrc($data['html']), diff --git a/src/Service/Matchfunding/MatchfundingService.php b/src/Service/Matchfunding/MatchfundingService.php new file mode 100644 index 0000000..5424906 --- /dev/null +++ b/src/Service/Matchfunding/MatchfundingService.php @@ -0,0 +1,84 @@ +getTarget()->getOwner(); + + if (!$target instanceof Project) { + return []; + } + + $transactions = []; + + foreach ($target->getMatchCallSubmissionsBy(self::SUBMISSION_ACCEPTED) as $submission) { + $rankedStrategies = $this->matchStrategyRepository->findByCall($submission->getCall()); + + foreach ($rankedStrategies as $strategy) { + foreach ($this->ruleLocator->getFrom($strategy) as $rule) { + if (!$rule->validate($charge, $submission)) { + // If an strategy's rule fails, skip to the next strategy + continue 2; + } + } + + $toBeMatched = match ($strategy->getAgainst()) { + MatchAgainst::Charge => $charge->getMoney(), + MatchAgainst::BudgetMin => $this->getBudget($target)[ProjectDeadline::Minimum->value], + MatchAgainst::BudgetOpt => $this->getBudget($target)[ProjectDeadline::Optimum->value], + }; + + $matched = $this->formulaLocator + ->get($strategy->getFormulaName()) + ->match($strategy->getFactor(), $toBeMatched, $strategy->getLimit()); + + $transaction = new Transaction(); + $transaction->setMoney($matched); + $transaction->setOrigin($submission->getCall()->getAccounting()); + $transaction->setTarget($submission->getProject()->getAccounting()); + + $transactions[] = $transaction; + + continue; + } + } + + return $transactions; + } + + private function getBudget(Project $project) + { + return $this->budgetService->calcBudget( + $project->getBudgetItems()->toArray(), + $project->getAccounting()->getCurrency() + ); + } +} diff --git a/src/Service/Project/BudgetService.php b/src/Service/Project/BudgetService.php new file mode 100644 index 0000000..fb9f24f --- /dev/null +++ b/src/Service/Project/BudgetService.php @@ -0,0 +1,42 @@ +getMoney(); + $itemDeadline = $item->getDeadline(); + + if ($itemDeadline == ProjectDeadline::Minimum) { + $totalMinimum = $this->moneyService->add($itemMoney, $totalMinimum); + } + + $totalOptimum = $this->moneyService->add($itemMoney, $totalOptimum); + } + + return [ + ProjectDeadline::Minimum->value => $totalMinimum, + ProjectDeadline::Optimum->value => $totalOptimum, + ]; + } +} diff --git a/src/Service/Project/TerritoryService.php b/src/Service/Project/TerritoryService.php index c2f3b0f..f36fdcb 100644 --- a/src/Service/Project/TerritoryService.php +++ b/src/Service/Project/TerritoryService.php @@ -2,7 +2,7 @@ namespace App\Service\Project; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Service\Nominatim\NominatimService; class TerritoryService @@ -11,12 +11,12 @@ class TerritoryService private NominatimService $nominatimService, ) {} - public function search(string $query): ProjectTerritory + public function search(string $query): Territory { $search = $this->nominatimService->search($query); if (empty($search)) { - return ProjectTerritory::unknown(); + return Territory::unknown(); } $result = $search[0]; @@ -28,13 +28,13 @@ class TerritoryService $address = $result['address']; if (!\array_key_exists('country_code', $address)) { - return ProjectTerritory::unknown(); + return Territory::unknown(); } return $this->processResultAddress($address); } - private function processResultAddress(array $address): ProjectTerritory + private function processResultAddress(array $address): Territory { $country = \strtoupper($address['country_code']); @@ -56,6 +56,6 @@ class TerritoryService } } - return new ProjectTerritory($country, $subLvl1, $subLvl2); + return new Territory($country, $subLvl1, $subLvl2); } } diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 84633f7..f9f2044 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -18,8 +18,8 @@ class UserService $value = $matches[0]; } - // Only lowercase a-z, numbers and underscore in user handles - $value = \preg_replace('/[^a-z0-9_]/', '_', \strtolower($value)); + // Only lowercase a-z, numbers, underscore and middle dots in user handles + $value = \preg_replace('/[^a-z0-9_.]|^\.|\.$/', '_', \strtolower($value)); // Min length 4 $value = \str_pad($value, 4, '_'); diff --git a/src/State/Accounting/AccountingBalanceStateProvider.php b/src/State/Accounting/AccountingBalanceStateProvider.php new file mode 100644 index 0000000..ca4b0f8 --- /dev/null +++ b/src/State/Accounting/AccountingBalanceStateProvider.php @@ -0,0 +1,43 @@ +accountingRepository->find($uriVariables['id']); + + if ($accounting === null) { + return null; + } + + $balance = new AccountingBalance(); + + /** @var AccountingApiResource */ + $resource = $this->autoMapper->map($accounting, AccountingApiResource::class); + + $balance->accounting = $resource; + $balance->balance = ApiMoney::of($this->accountingService->calcBalance($accounting)); + + return $balance; + } +} diff --git a/src/State/Accounting/AccountingStateProvider.php b/src/State/Accounting/AccountingStateProvider.php index 9543437..d8260d0 100644 --- a/src/State/Accounting/AccountingStateProvider.php +++ b/src/State/Accounting/AccountingStateProvider.php @@ -10,7 +10,6 @@ use ApiPlatform\State\Pagination\TraversablePaginator; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Accounting\AccountingApiResource; use App\Mapping\AutoMapper; -use App\Service\AccountingService; use Symfony\Component\DependencyInjection\Attribute\Autowire; class AccountingStateProvider implements ProviderInterface @@ -21,7 +20,6 @@ class AccountingStateProvider implements ProviderInterface #[Autowire(service: CollectionProvider::class)] private ProviderInterface $collectionProvider, private AutoMapper $autoMapper, - private AccountingService $accountingService, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null diff --git a/src/State/Gateway/CheckoutStateProcessor.php b/src/State/Gateway/CheckoutStateProcessor.php index 7e09f56..c36c2b9 100644 --- a/src/State/Gateway/CheckoutStateProcessor.php +++ b/src/State/Gateway/CheckoutStateProcessor.php @@ -6,10 +6,13 @@ use ApiPlatform\Doctrine\Common\State\PersistProcessor; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\Gateway\CheckoutApiResource; -use App\Dto\CheckoutUpdationDto; +use App\Dto\Gateway\CheckoutUpdationDto; +use App\Entity\EmbeddableMoney; use App\Entity\Gateway\Checkout; use App\Gateway\GatewayLocator; use App\Mapping\AutoMapper; +use App\Money\Conversion\ExchangeLocator; +use Brick\Money\Context\CustomContext; use Symfony\Component\DependencyInjection\Attribute\Autowire; class CheckoutStateProcessor implements ProcessorInterface @@ -17,8 +20,9 @@ class CheckoutStateProcessor implements ProcessorInterface public function __construct( #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $innerProcessor, - private GatewayLocator $gatewayLocator, private AutoMapper $autoMapper, + private GatewayLocator $gatewayLocator, + private ExchangeLocator $exchangeLocator, ) {} /** @@ -33,13 +37,31 @@ class CheckoutStateProcessor implements ProcessorInterface } $entity = $this->autoMapper->map($data, Checkout::class); - $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); - if ($data instanceof CheckoutApiResource) { - $entity = $this->gatewayLocator->get($data->gateway->name)->process($entity); - $entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); + /** @var Checkout */ + $checkout = $this->innerProcessor->process($entity, $operation, $uriVariables, $context); + + if ($data instanceof CheckoutUpdationDto) { + return $this->autoMapper->map($checkout, CheckoutApiResource::class); } - return $this->autoMapper->map($entity, CheckoutApiResource::class); + foreach ($checkout->getCharges() as $charge) { + $fromCurrency = $charge->getMoney()->getCurrency(); + $toCurrency = $charge->getTarget()->getCurrency(); + + if ($fromCurrency === $toCurrency) { + continue; + } + + $exchange = $this->exchangeLocator->get($fromCurrency, $toCurrency); + $exchanged = $exchange->convert($charge->getMoney(), $toCurrency, new CustomContext(0, 1)); + + $charge->setMoney(EmbeddableMoney::of($exchanged)); + } + + $checkout = $this->gatewayLocator->get($data->gateway->name)->process($checkout); + $checkout = $this->innerProcessor->process($checkout, $operation, $uriVariables, $context); + + return $this->autoMapper->map($checkout, CheckoutApiResource::class); } } diff --git a/src/State/Matchfunding/FormulaStateProvider.php b/src/State/Matchfunding/FormulaStateProvider.php new file mode 100644 index 0000000..a26556c --- /dev/null +++ b/src/State/Matchfunding/FormulaStateProvider.php @@ -0,0 +1,38 @@ +formulaLocator->get($uriVariables['name']); + + return FormulaApiResource::from($formula); + } catch (FormulaNotFoundException $e) { + return null; + } + } + + $strategies = $this->formulaLocator->getAll(); + + $resources = []; + foreach ($strategies as $formula) { + $resources[] = FormulaApiResource::from($formula); + } + + return $resources; + } +} diff --git a/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php b/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php new file mode 100644 index 0000000..b7a96a6 --- /dev/null +++ b/src/State/Matchfunding/MatchCallSubmissionStateProcessor.php @@ -0,0 +1,33 @@ +autoMapper->map($data, MatchCallSubmission::class); + $submission = $this->entityProcessor->process($submission, $operation, $uriVariables, $context); + + return $this->autoMapper->map($submission, MatchCallSubmissionApiResource::class); + } +} diff --git a/src/State/Matchfunding/MatchStrategyStateProcessor.php b/src/State/Matchfunding/MatchStrategyStateProcessor.php new file mode 100644 index 0000000..da7a94d --- /dev/null +++ b/src/State/Matchfunding/MatchStrategyStateProcessor.php @@ -0,0 +1,51 @@ +autoMapper->map($data, MatchStrategy::class); + + $call = $strategy->getCall(); + + if ($operation instanceof DeleteOperationInterface) { + $call->removeStrategy($strategy); + } else { + $call->addStrategy($strategy); + } + + $this->entityManager->persist($call); + $this->entityManager->flush(); + + $strategy = $this->entityProcessor->process($strategy, $operation, $uriVariables, $context); + + if ($strategy === null) { + return null; + } + + return $this->autoMapper->map($strategy, MatchStrategyApiResource::class); + } +} diff --git a/src/State/Matchfunding/RuleStateProvider.php b/src/State/Matchfunding/RuleStateProvider.php new file mode 100644 index 0000000..029daf1 --- /dev/null +++ b/src/State/Matchfunding/RuleStateProvider.php @@ -0,0 +1,38 @@ +ruleLocator->getOne($uriVariables['name']); + + return RuleApiResource::from($rule); + } catch (ClassNotFoundException $e) { + return null; + } + } + + $strategies = $this->ruleLocator->getAll(); + + $resources = []; + foreach ($strategies as $rule) { + $resources[] = RuleApiResource::from($rule); + } + + return $resources; + } +} diff --git a/src/State/MoneyTotalStateProvider.php b/src/State/MoneyTotalStateProvider.php new file mode 100644 index 0000000..5638201 --- /dev/null +++ b/src/State/MoneyTotalStateProvider.php @@ -0,0 +1,88 @@ +getClass(); + if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { + $entityClass = $options->getEntityClass(); + } + + $totalizer = $this->totalizerLocator->get($entityClass); + + return $totalizer->totalize($this->getItems($entityClass, $operation, $uriVariables, $context)); + } + + private function getItems( + string $entityClass, + Operation $operation, + array $uriVariables = [], + array $context = [], + ): iterable { + /** @var EntityManagerInterface $manager */ + $manager = $this->managerRegistry->getManagerForClass($entityClass); + + $repository = $manager->getRepository($entityClass); + if (!method_exists($repository, 'createQueryBuilder')) { + throw new RuntimeException('The repository class must have a "createQueryBuilder" method.'); + } + + $queryBuilder = $repository->createQueryBuilder('o'); + $queryNameGenerator = new QueryNameGenerator(); + + $this->handleLinks($queryBuilder, $uriVariables, $queryNameGenerator, $context, $entityClass, $operation); + + foreach ($this->collectionExtensions as $extension) { + $extension->applyToCollection($queryBuilder, $queryNameGenerator, $entityClass, $operation, $context); + + if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($entityClass, $operation, $context)) { + foreach ($extension->getResult($queryBuilder, $entityClass, $operation, $context) as $item) { + $manager->detach($item); + + yield $item; + } + + return; + } + } + + foreach ($queryBuilder->getQuery()->toIterable() as $item) { + $manager->detach($item); + + yield $item; + } + } +} diff --git a/src/State/Project/ProjectStateProvider.php b/src/State/Project/ProjectStateProvider.php index 4aa8552..418548e 100644 --- a/src/State/Project/ProjectStateProvider.php +++ b/src/State/Project/ProjectStateProvider.php @@ -5,11 +5,14 @@ namespace App\State\Project; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\Project\ProjectApiResource; +use App\Doctrine\LocalizedExtensionTrait; use App\Mapping\AutoMapper; use App\Repository\Project\ProjectRepository; class ProjectStateProvider implements ProviderInterface { + use LocalizedExtensionTrait; + public function __construct( private ProjectRepository $projectRepository, private AutoMapper $autoMapper, @@ -17,7 +20,15 @@ class ProjectStateProvider implements ProviderInterface public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { - $project = $this->projectRepository->findOneByIdOrSlug($uriVariables['idOrSlug']); + $idOrSlug = $uriVariables['idOrSlug']; + + $queryBuilder = $this->projectRepository->createQueryBuilder('p'); + $queryBuilder->where(\is_numeric($idOrSlug) ? 'p.id = :value' : 'p.slug = :value'); + $queryBuilder->setParameter('value', $idOrSlug); + + $query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context)); + + $project = $query->getOneOrNullResult(); if ($project === null) { return null; diff --git a/src/State/Project/RewardClaimStateProcessor.php b/src/State/Project/RewardClaimStateProcessor.php index ea47d14..15ce788 100644 --- a/src/State/Project/RewardClaimStateProcessor.php +++ b/src/State/Project/RewardClaimStateProcessor.php @@ -5,6 +5,7 @@ namespace App\State\Project; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\Project\RewardClaimApiResource; +use App\Dto\RewardClaimCreationDto; use App\Entity\Project\RewardClaim; use App\Mapping\AutoMapper; use App\Service\Auth\AuthService; @@ -20,7 +21,7 @@ class RewardClaimStateProcessor implements ProcessorInterface ) {} /** - * @param RewardClaimApiResource $data + * @param RewardClaimApiResource|RewardClaimCreationDto $data * * @return RewardClaimApiResource|null */ @@ -29,7 +30,7 @@ class RewardClaimStateProcessor implements ProcessorInterface /** @var RewardClaim */ $claim = $this->autoMapper->map($data, RewardClaim::class); - if (!$claim->getId()) { + if ($data instanceof RewardClaimCreationDto) { $owner = $this->authService->getUser(); if (!$owner) { @@ -37,6 +38,11 @@ class RewardClaimStateProcessor implements ProcessorInterface } $claim->setOwner($owner); + + $reward = $claim->getReward(); + $reward->addClaim($claim); + + $claim->setReward($reward); } $claim = $this->entityStateProcessor->process($claim, $operation, $uriVariables, $context); diff --git a/src/Validator/AvailableRewardUnitsValidator.php b/src/Validator/AvailableRewardUnitsValidator.php index bf51d40..e3949e8 100644 --- a/src/Validator/AvailableRewardUnitsValidator.php +++ b/src/Validator/AvailableRewardUnitsValidator.php @@ -13,11 +13,11 @@ final class AvailableRewardUnitsValidator extends ConstraintValidator */ public function validate(mixed $value, Constraint $constraint): void { - if (!$value->hasUnits) { + if (!$value->isFinite) { return; } - if ($value->hasUnits && $value->unitsAvailable > 0) { + if ($value->isFinite && $value->unitsAvailable > 0) { return; } diff --git a/src/Validator/EnoughRewardChargeValidator.php b/src/Validator/EnoughRewardChargeValidator.php index bbf4017..6880951 100644 --- a/src/Validator/EnoughRewardChargeValidator.php +++ b/src/Validator/EnoughRewardChargeValidator.php @@ -3,7 +3,7 @@ namespace App\Validator; use App\ApiResource\Project\RewardClaimApiResource; -use App\Library\Economy\MoneyService; +use App\Money\MoneyService; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; diff --git a/src/Validator/SameProjectRewardCharge.php b/src/Validator/SameProjectRewardCharge.php new file mode 100644 index 0000000..38cf6dd --- /dev/null +++ b/src/Validator/SameProjectRewardCharge.php @@ -0,0 +1,16 @@ +charge; + $reward = $value->reward; + + if ($charge->target->project?->id === $reward->project->id) { + return; + } + + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + } +} diff --git a/src/Validator/SingleProjectSubmissionPerCall.php b/src/Validator/SingleProjectSubmissionPerCall.php new file mode 100644 index 0000000..bcc72ca --- /dev/null +++ b/src/Validator/SingleProjectSubmissionPerCall.php @@ -0,0 +1,24 @@ +matchCallSubmissionRepository->findOneBy([ + 'call' => $value->call->id, + 'project' => $value->project->id, + ]); + + if ($submission === null) { + return; + } + + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + } +} diff --git a/src/Validator/SingleRewardClaimPerCharge.php b/src/Validator/SingleRewardClaimPerCharge.php new file mode 100644 index 0000000..4b19130 --- /dev/null +++ b/src/Validator/SingleRewardClaimPerCharge.php @@ -0,0 +1,24 @@ +rewardClaimRepository->findOneBy([ + 'charge' => $value->charge->id, + ]); + + if ($claim === null) { + return; + } + + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + } +} diff --git a/symfony.lock b/symfony.lock index 13d0289..0c70376 100644 --- a/symfony.lock +++ b/symfony.lock @@ -36,6 +36,15 @@ "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" } }, + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "87424683adc81d7dc305eefec1fced883084aab9" + } + }, "doctrine/doctrine-bundle": { "version": "2.12", "recipe": { diff --git a/tests/Entity/ProjectApi/CreateTest.php b/tests/Entity/ProjectApi/CreateTest.php index 183c347..a6aff83 100644 --- a/tests/Entity/ProjectApi/CreateTest.php +++ b/tests/Entity/ProjectApi/CreateTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use Symfony\Component\HttpFoundation\Response; -class CreateTest extends BaseTest +class CreateTest extends ProjectTestCase { // Auxiliary functions diff --git a/tests/Entity/ProjectApi/DeleteTest.php b/tests/Entity/ProjectApi/DeleteTest.php index f0b13af..f016fa9 100644 --- a/tests/Entity/ProjectApi/DeleteTest.php +++ b/tests/Entity/ProjectApi/DeleteTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use Symfony\Component\HttpFoundation\Response; -class DeleteTest extends BaseTest +class DeleteTest extends ProjectTestCase { protected function getMethod(): string { @@ -29,7 +29,7 @@ class DeleteTest extends BaseTest static::createClient()->request('DELETE', $this->getUri(1)); - $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR); + $this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); } public function testDeleteWithInvalidToken(): void diff --git a/tests/Entity/ProjectApi/GetAllTest.php b/tests/Entity/ProjectApi/GetAllTest.php index c913adc..ade088c 100644 --- a/tests/Entity/ProjectApi/GetAllTest.php +++ b/tests/Entity/ProjectApi/GetAllTest.php @@ -4,11 +4,11 @@ namespace App\Tests\Entity\ProjectApi; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectStatus; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Factory\Project\ProjectFactory; use Symfony\Component\HttpFoundation\Response; -class GetAllTest extends BaseTest +class GetAllTest extends ProjectTestCase { private const PAGE_SIZE = 30; @@ -52,7 +52,7 @@ class GetAllTest extends BaseTest int $responseCode = Response::HTTP_OK, ) { $owner = $this->createTestUser(); - $territory = new ProjectTerritory('ES'); + $territory = new Territory('ES'); $baseAttributes = [ 'owner' => $owner, 'territory' => $territory, @@ -81,7 +81,7 @@ class GetAllTest extends BaseTest string|ProjectCategory|ProjectStatus $otherValue, ) { $owner = $this->createTestUser(); - $territory = new ProjectTerritory('ES'); + $territory = new Territory('ES'); $baseAttributes = [ 'owner' => $owner, 'territory' => $territory, @@ -241,7 +241,7 @@ class GetAllTest extends BaseTest { ProjectFactory::createOne([ 'owner' => $this->createTestUser(), - 'territory' => new ProjectTerritory('ES'), + 'territory' => new Territory('ES'), 'title' => 'Lorem ipsum title', ]); diff --git a/tests/Entity/ProjectApi/GetOneTest.php b/tests/Entity/ProjectApi/GetOneTest.php index 1130ae3..afcf93c 100644 --- a/tests/Entity/ProjectApi/GetOneTest.php +++ b/tests/Entity/ProjectApi/GetOneTest.php @@ -5,7 +5,7 @@ namespace App\Tests\Entity\ProjectApi; use App\Entity\Project\Project; use App\Entity\Project\ProjectStatus; -class GetOneTest extends BaseTest +class GetOneTest extends ProjectTestCase { // Auxiliary functions diff --git a/tests/Entity/ProjectApi/BaseTest.php b/tests/Entity/ProjectApi/ProjectTestCase.php similarity index 96% rename from tests/Entity/ProjectApi/BaseTest.php rename to tests/Entity/ProjectApi/ProjectTestCase.php index a87d059..b76f2ab 100644 --- a/tests/Entity/ProjectApi/BaseTest.php +++ b/tests/Entity/ProjectApi/ProjectTestCase.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Symfony\Bundle\Test\Client; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Factory\Project\ProjectFactory; use App\Factory\User\UserFactory; use App\Tests\Traits\TestHelperTrait; @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; -abstract class BaseTest extends ApiTestCase +abstract class ProjectTestCase extends ApiTestCase { use ResetDatabase; use Factories; @@ -79,7 +79,7 @@ abstract class BaseTest extends ApiTestCase protected function createTestProjectOptimized(int $count = 1, array $attributes = []): array { $owner = $this->createTestUser(); - $territory = new ProjectTerritory('ES'); + $territory = new Territory('ES'); $mergedAttributes = array_merge([ 'owner' => $owner, diff --git a/tests/Entity/ProjectApi/UpdateTest.php b/tests/Entity/ProjectApi/UpdateTest.php index 4943a5a..4304fee 100644 --- a/tests/Entity/ProjectApi/UpdateTest.php +++ b/tests/Entity/ProjectApi/UpdateTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Entity\ProjectApi; use Symfony\Component\HttpFoundation\Response; -class UpdateTest extends BaseTest +class UpdateTest extends ProjectTestCase { // Auxiliary functions @@ -33,7 +33,7 @@ class UpdateTest extends BaseTest static::createClient()->request($this->getMethod(), $this->getUri(1)); - $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR); + $this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); } public function testUpdateWithInvalidToken(): void diff --git a/tests/Entity/ProjectCalendarTest.php b/tests/Entity/ProjectCalendarTest.php index da45649..b680940 100644 --- a/tests/Entity/ProjectCalendarTest.php +++ b/tests/Entity/ProjectCalendarTest.php @@ -7,7 +7,7 @@ use App\Entity\Project\Project; use App\Entity\Project\ProjectCategory; use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; -use App\Entity\Project\ProjectTerritory; +use App\Entity\Territory; use App\Entity\User\User; use Doctrine\ORM\EntityManagerInterface; use Zenstruck\Foundry\Test\ResetDatabase; @@ -47,7 +47,7 @@ class ProjectCalendarTest extends ApiTestCase $project->setDeadline($deadline); $project->setCategory(ProjectCategory::LibreSoftware); $project->setDescription('Test Project Description'); - $project->setTerritory(new ProjectTerritory('ES')); + $project->setTerritory(new Territory('ES')); $project->setOwner($this->owner); $project->setStatus(ProjectStatus::InReview); diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php index d7c45c7..65ba9af 100644 --- a/tests/Gateway/Wallet/WalletServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -3,7 +3,7 @@ namespace App\Tests\Gateway\Wallet; use App\Entity\Accounting\Transaction; -use App\Entity\Money; +use App\Entity\EmbeddableMoney; use App\Entity\Tipjar; use App\Entity\User\User; use App\Gateway\Wallet\StatementDirection; @@ -62,19 +62,19 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(0, $balance->amount); - $this->assertEquals($user->getCurrency(), $balance->currency); + $this->assertEquals(0, $balance->getAmount()); + $this->assertEquals($user->getCurrency(), $balance->getCurrency()); $incoming = new Transaction(); - $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); $income = $this->walletService->save($incoming); - $this->assertEquals(100, $income->getBalance()->amount); + $this->assertEquals(100, $income->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $income->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $income->getFinancesTo()); $this->assertCount(0, $income->getFinancedBy()); @@ -83,17 +83,17 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(100, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(100, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(1, $statements); $statement = $statements[0]; - $this->assertEquals(100, $statement->getBalance()->amount); + $this->assertEquals(100, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); } @@ -109,11 +109,11 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(0, $balance->amount); - $this->assertEquals($user->getCurrency(), $balance->currency); + $this->assertEquals(0, $balance->getAmount()); + $this->assertEquals($user->getCurrency(), $balance->getCurrency()); $incoming = new Transaction(); - $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -122,17 +122,17 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(100, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(100, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(1, $statements); $statement = $statements[0]; - $this->assertEquals(100, $statement->getBalance()->amount); + $this->assertEquals(100, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); } @@ -143,7 +143,7 @@ class WalletServiceTest extends KernelTestCase $user = $this->getUser()->getAccounting(); $incoming = new Transaction(); - $incoming->setMoney(new Money(100, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(100, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -151,7 +151,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $outgoing = new Transaction(); - $outgoing->setMoney(new Money(20, 'EUR')); + $outgoing->setMoney(new EmbeddableMoney(20, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -162,29 +162,29 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(80, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(80, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(2, $statements); $statement = $statements[0]; - $this->assertEquals(80, $statement->getBalance()->amount); + $this->assertEquals(80, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; - $this->assertEquals(20, $statement->getBalance()->amount); + $this->assertEquals(20, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); $outgoing = new Transaction(); - $outgoing->setMoney(new Money(30, 'EUR')); + $outgoing->setMoney(new EmbeddableMoney(30, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -195,43 +195,43 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(50, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(50, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(3, $statements); $statement = $statements[0]; - $this->assertEquals(50, $statement->getBalance()->amount); + $this->assertEquals(50, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(2, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; - $this->assertEquals(20, $statement->getBalance()->amount); + $this->assertEquals(20, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); $financement = $statements[0]->getFinancesTo()[0]; - $this->assertEquals(20, $financement->getMoney()->amount); - $this->assertEquals('EUR', $financement->getMoney()->currency); + $this->assertEquals(20, $financement->getMoney()->getAmount()); + $this->assertEquals('EUR', $financement->getMoney()->getCurrency()); $this->assertSame($financement, $statement->getFinancedBy()[0]); $this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId()); $statement = $statements[2]; - $this->assertEquals(30, $statement->getBalance()->amount); + $this->assertEquals(30, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(1, $statement->getFinancedBy()); $financement = $statements[0]->getFinancesTo()[1]; - $this->assertEquals(30, $financement->getMoney()->amount); - $this->assertEquals('EUR', $financement->getMoney()->currency); + $this->assertEquals(30, $financement->getMoney()->getAmount()); + $this->assertEquals('EUR', $financement->getMoney()->getCurrency()); $this->assertSame($financement, $statement->getFinancedBy()[0]); $this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId()); } @@ -242,7 +242,7 @@ class WalletServiceTest extends KernelTestCase $user = $this->getUser()->getAccounting(); $incoming = new Transaction(); - $incoming->setMoney(new Money(10, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(10, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -250,7 +250,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $incoming = new Transaction(); - $incoming->setMoney(new Money(11, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(11, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -258,7 +258,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $outgoing = new Transaction(); - $outgoing->setMoney(new Money(12, 'EUR')); + $outgoing->setMoney(new EmbeddableMoney(12, 'EUR')); $outgoing->setOrigin($user); $outgoing->setTarget($tipjar); @@ -268,7 +268,7 @@ class WalletServiceTest extends KernelTestCase $this->entityManager->flush(); $incoming = new Transaction(); - $incoming->setMoney(new Money(13, 'EUR')); + $incoming->setMoney(new EmbeddableMoney(13, 'EUR')); $incoming->setOrigin($tipjar); $incoming->setTarget($user); @@ -277,38 +277,38 @@ class WalletServiceTest extends KernelTestCase $balance = $this->walletService->getBalance($user); - $this->assertEquals(22, $balance->amount); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals(22, $balance->getAmount()); + $this->assertEquals('EUR', $balance->getCurrency()); $statements = $this->walletService->getStatements($user); $this->assertCount(4, $statements); $statement = $statements[0]; - $this->assertEquals(0, $statement->getBalance()->amount); + $this->assertEquals(0, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[1]; - $this->assertEquals(9, $statement->getBalance()->amount); + $this->assertEquals(9, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(1, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); $statement = $statements[2]; - $this->assertEquals(12, $statement->getBalance()->amount); + $this->assertEquals(12, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(2, $statement->getFinancedBy()); $statement = $statements[3]; - $this->assertEquals(13, $statement->getBalance()->amount); + $this->assertEquals(13, $statement->getBalance()->getAmount()); $this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value); - $this->assertEquals('EUR', $balance->currency); + $this->assertEquals('EUR', $balance->getCurrency()); $this->assertCount(0, $statement->getFinancesTo()); $this->assertCount(0, $statement->getFinancedBy()); } diff --git a/tests/Library/Economy/Currency/EuropeanCentralBankExchangeTest.php b/tests/Money/Conversion/EuropeanCentralBankExchangeTest.php similarity index 59% rename from tests/Library/Economy/Currency/EuropeanCentralBankExchangeTest.php rename to tests/Money/Conversion/EuropeanCentralBankExchangeTest.php index bebbe84..dae5bdd 100644 --- a/tests/Library/Economy/Currency/EuropeanCentralBankExchangeTest.php +++ b/tests/Money/Conversion/EuropeanCentralBankExchangeTest.php @@ -1,8 +1,10 @@ assertArrayHasKey('Cube', $exchangeData); $this->assertArrayHasKey('@attributes', $exchangeData); } + + public function testConvertsWholeEuros() + { + $exchange = new EuropeanCentralBankExchange(); + $context = new CustomContext(scale: 0, step: 1); + + $this->assertEquals(100, $exchange->convert(new Money(100, 'JPY'), 'EUR', $context)->getAmount()); + $this->assertEquals(100, $exchange->convert(new Money(150, 'JPY'), 'EUR', $context)->getAmount()); + $this->assertEquals(100, $exchange->convert(new Money(700, 'CNY'), 'EUR', $context)->getAmount()); + } } diff --git a/tests/Library/Economy/MoneyServiceTest.php b/tests/Money/MoneyServiceTest.php similarity index 77% rename from tests/Library/Economy/MoneyServiceTest.php rename to tests/Money/MoneyServiceTest.php index 2ad6b97..63ac2fe 100644 --- a/tests/Library/Economy/MoneyServiceTest.php +++ b/tests/Money/MoneyServiceTest.php @@ -1,9 +1,9 @@ moneyService->add($a, $b); - $this->assertEquals(100, $a->amount); - $this->assertEquals(110, $b->amount); - $this->assertEquals(210, $c->amount); + $this->assertEquals(100, $a->getAmount()); + $this->assertEquals(110, $b->getAmount()); + $this->assertEquals(210, $c->getAmount()); $d = new Money(100, 'USD'); $e = $this->moneyService->add($a, $d); - $this->assertEquals($d->amount, $a->amount); - $this->assertEquals($d->currency, $e->currency); + $this->assertEquals($d->getAmount(), $a->getAmount()); + $this->assertEquals($d->getCurrency(), $e->getCurrency()); - $this->assertNotEquals($d->amount, $e->amount); + $this->assertNotEquals($d->getAmount(), $e->getAmount()); $this->assertTrue($this->moneyService->isMoreOrSame($e, $d)); $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); } @@ -86,16 +86,16 @@ class MoneyServiceTest extends KernelTestCase $c = $this->moneyService->substract($a, $b); - $this->assertEquals(101, $a->amount); - $this->assertEquals(210, $b->amount); - $this->assertEquals(109, $c->amount); + $this->assertEquals(101, $a->getAmount()); + $this->assertEquals(210, $b->getAmount()); + $this->assertEquals(109, $c->getAmount()); $d = new Money(500, 'USD'); $e = $this->moneyService->substract($a, $d); - $this->assertEquals($d->currency, $e->currency); + $this->assertEquals($d->getCurrency(), $e->getCurrency()); - $this->assertNotEquals($d->amount, $e->amount); + $this->assertNotEquals($d->getAmount(), $e->getAmount()); $this->assertTrue($this->moneyService->isLess($e, $d)); $this->assertTrue($this->moneyService->isMoreOrSame($e, $a)); } diff --git a/tests/Service/AccountingServiceTest.php b/tests/Service/AccountingServiceTest.php index 097952b..8585a54 100644 --- a/tests/Service/AccountingServiceTest.php +++ b/tests/Service/AccountingServiceTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Service; use App\ApiResource\Accounting\AccountingBalancePoint; use App\Entity\Accounting\Transaction; -use App\Entity\Money; +use App\Entity\EmbeddableMoney; use App\Entity\Tipjar; use App\Service\AccountingService; use Doctrine\ORM\EntityManagerInterface; @@ -46,7 +46,7 @@ class AccountingServiceTest extends KernelTestCase $target = $i % 2 ? $tipjars[\random_int(5, 9)] : $tipjars[\random_int(0, 5)]; $trx = new Transaction(); - $trx->setMoney(new Money(random_int(100, 999), 'EUR')); + $trx->setMoney(new EmbeddableMoney(random_int(100, 999), 'EUR')); $trx->setOrigin($origin->getAccounting()); $trx->setTarget($target->getAccounting());