Merge pull request #30 from GoteoFoundation/feat/add-wallet
[feat] Add Wallet Gateway
This commit is contained in:
@@ -39,15 +39,15 @@ DOCTRINE_ENCRYPT_KEY=""
|
||||
CRYPTO_SECRET=""
|
||||
###< doctrineencryptbundle/doctrine-encrypt-bundle ###
|
||||
|
||||
###> App\Library\Economy\Payment\StripeGateway ###
|
||||
###> App\Gateway\Stripe\StripeGateway ###
|
||||
STRIPE_API_KEY=""
|
||||
STRIPE_WEBHOOK_SECRET=""
|
||||
###< App\Library\Economy\Payment\StripeGateway ###
|
||||
###< App\Gateway\Stripe\StripeGateway ###
|
||||
|
||||
###> App\Library\Economy\Payment\PaypalGateway ###
|
||||
###> App\Gateway\Paypal\PaypalGateway ###
|
||||
# PAYPAL_API_ADDRESS="https://api-m.paypal.com"
|
||||
PAYPAL_API_ADDRESS="https://api-m.sandbox.paypal.com"
|
||||
PAYPAL_CLIENT_ID=""
|
||||
PAYPAL_CLIENT_SECRET=""
|
||||
PAYPAL_WEBHOOK_ID="WEBHOOK_ID"
|
||||
###< App\Library\Economy\Payment\PaypalGateway ###
|
||||
###< App\Gateway\Paypal\PaypalGateway ###
|
||||
|
||||
@@ -8,6 +8,8 @@ return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||
'function_declaration' => ['closure_fn_spacing' => 'none'],
|
||||
'single_line_empty_body' => true,
|
||||
'yoda_style' => [
|
||||
'equal' => false,
|
||||
'identical' => false,
|
||||
|
||||
Generated
+79
-73
@@ -4,20 +4,20 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "20e503cd4b7c41eca5ee6fcf83fa4ae8",
|
||||
"content-hash": "0d0e2940f9aaecab4ba3c50bc5822de6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/core",
|
||||
"version": "v3.4.1",
|
||||
"version": "v3.4.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/api-platform/core.git",
|
||||
"reference": "05f7d795252d32424b49336e5184557569809796"
|
||||
"reference": "77d3ff3e8ddf88546eaf288a399d11f130553606"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/api-platform/core/zipball/05f7d795252d32424b49336e5184557569809796",
|
||||
"reference": "05f7d795252d32424b49336e5184557569809796",
|
||||
"url": "https://api.github.com/repos/api-platform/core/zipball/77d3ff3e8ddf88546eaf288a399d11f130553606",
|
||||
"reference": "77d3ff3e8ddf88546eaf288a399d11f130553606",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -223,9 +223,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/api-platform/core/issues",
|
||||
"source": "https://github.com/api-platform/core/tree/v3.4.1"
|
||||
"source": "https://github.com/api-platform/core/tree/v3.4.4"
|
||||
},
|
||||
"time": "2024-09-23T07:27:56+00:00"
|
||||
"time": "2024-10-19T10:05:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "beberlei/doctrineextensions",
|
||||
@@ -708,16 +708,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/common",
|
||||
"version": "3.4.4",
|
||||
"version": "3.4.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/common.git",
|
||||
"reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a"
|
||||
"reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/common/zipball/0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a",
|
||||
"reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a",
|
||||
"url": "https://api.github.com/repos/doctrine/common/zipball/6c8fef961f67b8bc802ce3e32e3ebd1022907286",
|
||||
"reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -779,7 +779,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/common/issues",
|
||||
"source": "https://github.com/doctrine/common/tree/3.4.4"
|
||||
"source": "https://github.com/doctrine/common/tree/3.4.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -795,20 +795,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-16T13:35:33+00:00"
|
||||
"time": "2024-10-08T15:53:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/dbal",
|
||||
"version": "3.9.1",
|
||||
"version": "3.9.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/dbal.git",
|
||||
"reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7"
|
||||
"reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7",
|
||||
"reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7",
|
||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba",
|
||||
"reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -824,7 +824,7 @@
|
||||
"doctrine/coding-standard": "12.0.0",
|
||||
"fig/log-test": "^1",
|
||||
"jetbrains/phpstorm-stubs": "2023.1",
|
||||
"phpstan/phpstan": "1.12.0",
|
||||
"phpstan/phpstan": "1.12.6",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"phpunit/phpunit": "9.6.20",
|
||||
"psalm/plugin-phpunit": "0.18.4",
|
||||
@@ -892,7 +892,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/dbal/issues",
|
||||
"source": "https://github.com/doctrine/dbal/tree/3.9.1"
|
||||
"source": "https://github.com/doctrine/dbal/tree/3.9.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -908,7 +908,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-01T13:49:23+00:00"
|
||||
"time": "2024-10-10T17:56:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
@@ -1500,16 +1500,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/migrations",
|
||||
"version": "3.8.1",
|
||||
"version": "3.8.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/migrations.git",
|
||||
"reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877"
|
||||
"reference": "5007eb1168691225ac305fe16856755c20860842"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/migrations/zipball/7760fbd0b7cb58bfb50415505a7bab821adf0877",
|
||||
"reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877",
|
||||
"url": "https://api.github.com/repos/doctrine/migrations/zipball/5007eb1168691225ac305fe16856755c20860842",
|
||||
"reference": "5007eb1168691225ac305fe16856755c20860842",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1583,7 +1583,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/migrations/issues",
|
||||
"source": "https://github.com/doctrine/migrations/tree/3.8.1"
|
||||
"source": "https://github.com/doctrine/migrations/tree/3.8.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1599,20 +1599,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-28T13:17:28+00:00"
|
||||
"time": "2024-10-10T21:35:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/orm",
|
||||
"version": "2.19.7",
|
||||
"version": "2.20.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/orm.git",
|
||||
"reference": "168ac31084226f94d42e7461a40ff5607a56bd35"
|
||||
"reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/orm/zipball/168ac31084226f94d42e7461a40ff5607a56bd35",
|
||||
"reference": "168ac31084226f94d42e7461a40ff5607a56bd35",
|
||||
"url": "https://api.github.com/repos/doctrine/orm/zipball/8ed6c2234aba019f9737a6bcc9516438e62da27c",
|
||||
"reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1641,7 +1641,9 @@
|
||||
"doctrine/annotations": "^1.13 || ^2",
|
||||
"doctrine/coding-standard": "^9.0.2 || ^12.0",
|
||||
"phpbench/phpbench": "^0.16.10 || ^1.0",
|
||||
"phpstan/phpstan": "~1.4.10 || 1.11.1",
|
||||
"phpstan/extension-installer": "~1.1.0 || ^1.4",
|
||||
"phpstan/phpstan": "~1.4.10 || 1.12.6",
|
||||
"phpstan/phpstan-deprecation-rules": "^1",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
"squizlabs/php_codesniffer": "3.7.2",
|
||||
@@ -1698,9 +1700,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/orm/issues",
|
||||
"source": "https://github.com/doctrine/orm/tree/2.19.7"
|
||||
"source": "https://github.com/doctrine/orm/tree/2.20.0"
|
||||
},
|
||||
"time": "2024-08-23T06:54:57+00:00"
|
||||
"time": "2024-10-11T11:47:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/persistence",
|
||||
@@ -1801,16 +1803,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/sql-formatter",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/sql-formatter.git",
|
||||
"reference": "7f83911cc5eba870de7ebb11283972483f7e2891"
|
||||
"reference": "b784cbde727cf806721451dde40eff4fec3bbe86"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/7f83911cc5eba870de7ebb11283972483f7e2891",
|
||||
"reference": "7f83911cc5eba870de7ebb11283972483f7e2891",
|
||||
"url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/b784cbde727cf806721451dde40eff4fec3bbe86",
|
||||
"reference": "b784cbde727cf806721451dde40eff4fec3bbe86",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1818,6 +1820,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^12",
|
||||
"ergebnis/phpunit-slow-test-detector": "^2.14",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"vimeo/psalm": "^5.24"
|
||||
@@ -1850,22 +1853,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/sql-formatter/issues",
|
||||
"source": "https://github.com/doctrine/sql-formatter/tree/1.4.1"
|
||||
"source": "https://github.com/doctrine/sql-formatter/tree/1.5.1"
|
||||
},
|
||||
"time": "2024-08-05T20:32:22+00:00"
|
||||
"time": "2024-10-21T18:21:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrineencryptbundle/doctrine-encrypt-bundle",
|
||||
"version": "5.4.0",
|
||||
"version": "5.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle.git",
|
||||
"reference": "aa60745e1548213f11467a1d41308d6c03f23faf"
|
||||
"reference": "556c2ec0156d519378e6d62a61c2e924e4583e13"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/DoctrineEncryptBundle/DoctrineEncryptBundle/zipball/aa60745e1548213f11467a1d41308d6c03f23faf",
|
||||
"reference": "aa60745e1548213f11467a1d41308d6c03f23faf",
|
||||
"url": "https://api.github.com/repos/DoctrineEncryptBundle/DoctrineEncryptBundle/zipball/556c2ec0156d519378e6d62a61c2e924e4583e13",
|
||||
"reference": "556c2ec0156d519378e6d62a61c2e924e4583e13",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1915,22 +1918,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/issues",
|
||||
"source": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/tree/5.4.0"
|
||||
"source": "https://github.com/DoctrineEncryptBundle/DoctrineEncryptBundle/tree/5.4.1"
|
||||
},
|
||||
"time": "2024-03-02T18:14:49+00:00"
|
||||
"time": "2024-10-13T10:08:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "gedmo/doctrine-extensions",
|
||||
"version": "v3.16.1",
|
||||
"version": "v3.17.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine-extensions/DoctrineExtensions.git",
|
||||
"reference": "e85560ed96f977b8c29428a99222cb2ef2f0e80d"
|
||||
"reference": "eabb45018c5a4362b46c5beae3881261da89f900"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/e85560ed96f977b8c29428a99222cb2ef2f0e80d",
|
||||
"reference": "e85560ed96f977b8c29428a99222cb2ef2f0e80d",
|
||||
"url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eabb45018c5a4362b46c5beae3881261da89f900",
|
||||
"reference": "eabb45018c5a4362b46c5beae3881261da89f900",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1947,14 +1950,14 @@
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/annotations": "<1.13 || >=3.0",
|
||||
"doctrine/dbal": "<3.2 || >=4.0",
|
||||
"doctrine/dbal": "<3.7 || >=5.0",
|
||||
"doctrine/mongodb-odm": "<2.3 || >=3.0",
|
||||
"doctrine/orm": "<2.14.0 || 2.16.0 || 2.16.1 || >=4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/annotations": "^1.13 || ^2.0",
|
||||
"doctrine/cache": "^1.11 || ^2.0",
|
||||
"doctrine/dbal": "^3.2",
|
||||
"doctrine/dbal": "^3.7 || ^4.0",
|
||||
"doctrine/doctrine-bundle": "^2.3",
|
||||
"doctrine/mongodb-odm": "^2.3",
|
||||
"doctrine/orm": "^2.14.0 || ^3.0",
|
||||
@@ -2026,7 +2029,7 @@
|
||||
"support": {
|
||||
"email": "gediminas.morkevicius@gmail.com",
|
||||
"issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues",
|
||||
"source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.16.1",
|
||||
"source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.17.1",
|
||||
"wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc"
|
||||
},
|
||||
"funding": [
|
||||
@@ -2047,7 +2050,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-25T16:22:14+00:00"
|
||||
"time": "2024-10-07T22:30:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "godruoyi/php-snowflake",
|
||||
@@ -2737,16 +2740,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "1.31.0",
|
||||
"version": "1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "249f15fb843bf240cf058372dad29e100cee6c17"
|
||||
"reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17",
|
||||
"reference": "249f15fb843bf240cf058372dad29e100cee6c17",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140",
|
||||
"reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2778,9 +2781,9 @@
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0"
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0"
|
||||
},
|
||||
"time": "2024-09-22T11:32:18+00:00"
|
||||
"time": "2024-10-13T11:25:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
@@ -4512,22 +4515,25 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/flex",
|
||||
"version": "v2.4.6",
|
||||
"version": "v2.4.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/flex.git",
|
||||
"reference": "4dc11919791f81d087a12db2ab4c7e044431ef6b"
|
||||
"reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/flex/zipball/4dc11919791f81d087a12db2ab4c7e044431ef6b",
|
||||
"reference": "4dc11919791f81d087a12db2ab4c7e044431ef6b",
|
||||
"url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402",
|
||||
"reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^2.1",
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"composer/semver": "<1.7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^2.1",
|
||||
"symfony/dotenv": "^5.4|^6.0",
|
||||
@@ -4557,7 +4563,7 @@
|
||||
"description": "Composer plugin for Symfony",
|
||||
"support": {
|
||||
"issues": "https://github.com/symfony/flex/issues",
|
||||
"source": "https://github.com/symfony/flex/tree/v2.4.6"
|
||||
"source": "https://github.com/symfony/flex/tree/v2.4.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4573,7 +4579,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-27T10:22:22+00:00"
|
||||
"time": "2024-10-07T08:51:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/framework-bundle",
|
||||
@@ -8293,16 +8299,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.2.0",
|
||||
"version": "v5.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb"
|
||||
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
|
||||
"reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
|
||||
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8345,9 +8351,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0"
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
|
||||
},
|
||||
"time": "2024-09-15T16:40:33+00:00"
|
||||
"time": "2024-10-08T18:51:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
@@ -11146,7 +11152,7 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@@ -11154,6 +11160,6 @@
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
+18
-18
@@ -12,12 +12,12 @@ services:
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
_instanceof:
|
||||
App\Gateway\GatewayInterface:
|
||||
tags: ['app.gateway.gateway']
|
||||
App\Library\Benzina\Pump\PumpInterface:
|
||||
tags: ['app.lib.benzina.pump.pump']
|
||||
App\Library\Economy\Currency\ExchangeInterface:
|
||||
tags: ['app.lib.economy.currency.exchange']
|
||||
App\Library\Economy\Payment\GatewayInterface:
|
||||
tags: ['app.lib.economy.payment.gateway']
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
@@ -37,6 +37,22 @@ services:
|
||||
arguments:
|
||||
- '@App\OpenApi\OpenApiFactory.inner'
|
||||
|
||||
App\Gateway\GatewayLocator:
|
||||
arguments:
|
||||
- !tagged 'app.gateway.gateway'
|
||||
|
||||
App\Gateway\Paypal\PaypalService:
|
||||
arguments:
|
||||
- '%env(PAYPAL_API_ADDRESS)%'
|
||||
- '%env(PAYPAL_CLIENT_ID)%'
|
||||
- '%env(PAYPAL_CLIENT_SECRET)%'
|
||||
- '%env(PAYPAL_WEBHOOK_ID)%'
|
||||
|
||||
App\Gateway\Stripe\StripeGateway:
|
||||
arguments:
|
||||
- '%env(STRIPE_API_KEY)%'
|
||||
- '%env(STRIPE_WEBHOOK_SECRET)%'
|
||||
|
||||
App\Service\Auth\AuthService:
|
||||
arguments:
|
||||
- '%env(APP_SECRET)%'
|
||||
@@ -49,21 +65,5 @@ services:
|
||||
arguments:
|
||||
- !tagged 'app.lib.economy.currency.exchange'
|
||||
|
||||
App\Library\Economy\Payment\GatewayLocator:
|
||||
arguments:
|
||||
- !tagged 'app.lib.economy.payment.gateway'
|
||||
|
||||
App\Library\Economy\Payment\StripeGateway:
|
||||
arguments:
|
||||
- '%env(STRIPE_API_KEY)%'
|
||||
- '%env(STRIPE_WEBHOOK_SECRET)%'
|
||||
|
||||
App\Library\Economy\Payment\PaypalGatewayService:
|
||||
arguments:
|
||||
- '%env(PAYPAL_API_ADDRESS)%'
|
||||
- '%env(PAYPAL_CLIENT_ID)%'
|
||||
- '%env(PAYPAL_CLIENT_SECRET)%'
|
||||
- '%env(PAYPAL_WEBHOOK_ID)%'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
[opcache]
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=1
|
||||
opcache.revalidate_freq=0
|
||||
opcache.validate_timestamps=1
|
||||
opcache.preload=/app/config/preload.php
|
||||
opcache.memory_consumption=256
|
||||
opcache.max_accelerated_files=20000
|
||||
opcache.max_wasted_percentage=10
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[opcache]
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=1
|
||||
opcache.preload=/app/config/preload.php
|
||||
opcache.preload_user=www-data
|
||||
opcache.memory_consumption=256
|
||||
opcache.max_accelerated_files=20000
|
||||
opcache.validate_timestamps=0
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Accounting;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Accounting as Entity;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\State\Accounting\AccountingStateProcessor;
|
||||
use App\State\Accounting\AccountingStateProvider;
|
||||
|
||||
/**
|
||||
* Accountings represent payment services used to perform the necessary payments for Transactions between Accounts.\
|
||||
* \
|
||||
* For each Accounting there is an internal implementation that handles the creation and validation of Transactions between Accounts.
|
||||
* These implementations make use of external or internal services to gather the funds that are inside a Transaction,
|
||||
* perform corroboration of funds and store the Transactions into the system.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'Accounting',
|
||||
stateOptions: new Options(entityClass: Entity\Accounting::class),
|
||||
provider: AccountingStateProvider::class,
|
||||
processor: AccountingStateProcessor::class,
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Get()]
|
||||
#[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')]
|
||||
class Accounting
|
||||
{
|
||||
public ?int $id = null;
|
||||
|
||||
public ?string $currency = null;
|
||||
|
||||
public ?AccountingOwnerInterface $owner = null;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Gateway;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\Accounting;
|
||||
use App\Entity\Gateway\Charge as EntityCharge;
|
||||
use App\Entity\Money;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\State\Gateway\ChargeStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A Charge represents a payment item to be included in a Checkout for payment at a Gateway.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'GatewayCharge',
|
||||
stateOptions: new Options(entityClass: EntityCharge::class),
|
||||
provider: ChargeStateProvider::class
|
||||
)]
|
||||
#[API\Get()]
|
||||
class Charge
|
||||
{
|
||||
#[API\ApiProperty(writable: false, identifier: true)]
|
||||
public ?int $id = null;
|
||||
|
||||
/**
|
||||
* How this item should be processed by the Gateway.\
|
||||
* \
|
||||
* `single` is for one time payments.\
|
||||
* `recurring` is for payments repeated over time.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public ChargeType $type = ChargeType::Single;
|
||||
|
||||
/**
|
||||
* A short, descriptive string for this charge item.\
|
||||
* May be displayed to the payer.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public string $title;
|
||||
|
||||
/**
|
||||
* Detailed information about the charge item.\
|
||||
* May be displayed to the payer.
|
||||
*/
|
||||
public ?string $description = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the money after a successful payment.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Accounting $target;
|
||||
|
||||
/**
|
||||
* The money to-be-paid for this item at the Gateway.
|
||||
*
|
||||
* It is money before fees and taxes, not accountable.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Money $money;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Gateway;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\Accounting;
|
||||
use App\Entity\Gateway as Entity;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\Tracking;
|
||||
use App\State\Gateway\CheckoutStateProcessor;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCheckout represents a payment session with a Gateway.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'GatewayCheckout',
|
||||
stateOptions: new Options(entityClass: Entity\Checkout::class),
|
||||
processor: CheckoutStateProcessor::class
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post()]
|
||||
#[API\Get()]
|
||||
class Checkout
|
||||
{
|
||||
#[API\ApiProperty(writable: false, identifier: true)]
|
||||
public ?int $id = null;
|
||||
|
||||
/**
|
||||
* The desired Gateway to checkout with.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Gateway $gateway;
|
||||
|
||||
/**
|
||||
* The Accounting paying for the charges.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Accounting $origin;
|
||||
|
||||
/**
|
||||
* A list of the payment items to be charged to the origin.
|
||||
*
|
||||
* @var Charge[]
|
||||
*/
|
||||
#[API\ApiProperty(readableLink: true, writableLink: true)]
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Count(min: 1)]
|
||||
public array $charges = [];
|
||||
|
||||
/**
|
||||
* The status of this Checkout, as confirmed by the Gateway.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public CheckoutStatus $status = CheckoutStatus::Pending;
|
||||
|
||||
/**
|
||||
* A list of related hyperlinks, as provided by the Gateway.
|
||||
*
|
||||
* @var Link[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $links = [];
|
||||
|
||||
/**
|
||||
* A list of related tracking codes and numbers, as provided by the Gateway.
|
||||
*
|
||||
* @var Tracking[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $trackings = [];
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource;
|
||||
namespace App\ApiResource\Gateway;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Library\Economy\Payment\GatewayInterface;
|
||||
use App\State\GatewayStateProvider;
|
||||
use App\State\Gateway\GatewayStateProvider;
|
||||
|
||||
/**
|
||||
* Gateways represent payment services used to perform the necessary payments for Transactions between Accounts.\
|
||||
@@ -13,17 +12,16 @@ use App\State\GatewayStateProvider;
|
||||
* These implementations make use of external or internal services to gather the funds that are inside a Transaction,
|
||||
* perform corroboration of funds and store the Transactions into the system.
|
||||
*/
|
||||
#[API\ApiResource(shortName: 'Gateway')]
|
||||
#[API\GetCollection(provider: GatewayStateProvider::class)]
|
||||
#[API\Get(provider: GatewayStateProvider::class)]
|
||||
class Gateway
|
||||
{
|
||||
public function __construct(private readonly GatewayInterface $gateway)
|
||||
{
|
||||
}
|
||||
|
||||
#[API\ApiProperty(identifier: true)]
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->gateway->getName();
|
||||
}
|
||||
public ?string $name = null;
|
||||
|
||||
/**
|
||||
* @var array<int, \App\Gateway\ChargeType>
|
||||
*/
|
||||
public ?array $supports = null;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace App\ApiResource;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Filter\ResourceVersionResourceFilter;
|
||||
use App\Filter\ResourceVersionResourceIdFilter;
|
||||
use App\Service\ApiResourceNormalizer;
|
||||
use App\Service\ApiService;
|
||||
use App\State\ResourceVersionStateProvider;
|
||||
use Gedmo\Loggable\Entity\LogEntry;
|
||||
|
||||
@@ -24,8 +24,7 @@ class Version
|
||||
public function __construct(
|
||||
private readonly LogEntry $log,
|
||||
private readonly object $entity,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The ID of the version record.
|
||||
@@ -56,7 +55,7 @@ class Version
|
||||
*/
|
||||
public function getResource(): string
|
||||
{
|
||||
return ApiResourceNormalizer::toResource($this->log->getObjectClass());
|
||||
return ApiService::toResource($this->log->getObjectClass());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,9 +50,9 @@ class BenzinaPumpCommand extends Command
|
||||
$this->addOption(
|
||||
'skip-pumped',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
InputOption::VALUE_NEGATABLE,
|
||||
'Skips feeding already pumped records in a batch',
|
||||
false
|
||||
true
|
||||
);
|
||||
|
||||
$this->addUsage('app:benzina:pump --no-debug user');
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\DependencyInjection\Compiler\GatewaysCompilerPass;
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:gateways:setup',
|
||||
description: 'Post install setup for Gateways.',
|
||||
)]
|
||||
class GatewaysSetupCommand extends Command
|
||||
{
|
||||
private const SUCCESS_MESSAGE = 'Gateways setup completed successfully!';
|
||||
private const FAILURE_MESSAGE = 'Could not setup Gateways. Please review the error.';
|
||||
|
||||
public function __construct(
|
||||
private GatewayLocator $gatewayLocator,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$classes = $this->gatewayLocator->getClasses();
|
||||
GatewaysCompilerPass::validateGatewayNames($classes);
|
||||
|
||||
$names = $this->gatewayLocator->getNames();
|
||||
GatewaysCompilerPass::compileGatewayNames($names);
|
||||
|
||||
$io->success(self::SUCCESS_MESSAGE);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$io->error(self::FAILURE_MESSAGE);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,7 @@ class SystemVarsListCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
}
|
||||
protected function configure(): void {}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use App\Gateway\GatewayLocator;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -19,8 +19,7 @@ class GatewaysController extends AbstractController
|
||||
|
||||
public function __construct(
|
||||
private GatewayLocator $gatewayLocator,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
#[Route('/gateway_redirects', name: self::REDIRECT)]
|
||||
public function handleRedirect(Request $request): Response
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Library\Economy\Payment\GatewayInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class GatewaysCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public const GATEWAYS_DIR = 'gateways';
|
||||
public const GATEWAY_NAMES_LOCK = 'gateway_names_compiled.lock';
|
||||
|
||||
/**
|
||||
* @return string The path to the gateways dir inside the project's var dir
|
||||
*/
|
||||
public static function getCompileDir(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%svar%s%s',
|
||||
\dirname(__DIR__, 3),
|
||||
DIRECTORY_SEPARATOR,
|
||||
DIRECTORY_SEPARATOR,
|
||||
self::GATEWAYS_DIR,
|
||||
);
|
||||
}
|
||||
|
||||
private static function makeCompileDir()
|
||||
{
|
||||
$compileDir = self::getCompileDir();
|
||||
|
||||
if (!\is_dir($compileDir)) {
|
||||
\mkdir($compileDir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The path to the gateway names lock file
|
||||
*/
|
||||
public static function getLockFile(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%s%s',
|
||||
self::getCompileDir(),
|
||||
DIRECTORY_SEPARATOR,
|
||||
self::GATEWAY_NAMES_LOCK
|
||||
);
|
||||
}
|
||||
|
||||
private static function writeLockFile(array $lines)
|
||||
{
|
||||
self::makeCompileDir();
|
||||
|
||||
\file_put_contents(
|
||||
self::getLockFile(),
|
||||
implode(PHP_EOL, $lines)
|
||||
);
|
||||
}
|
||||
|
||||
private static function getGatewaysNamespace(): string
|
||||
{
|
||||
return join('\\', \array_slice(explode('\\', GatewayInterface::class), 0, -1));
|
||||
}
|
||||
|
||||
private function getGatewayNames(array $gatewayClasses): array
|
||||
{
|
||||
$names = [];
|
||||
foreach ($gatewayClasses as $class) {
|
||||
$names[] = $class::getName();
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
private static function getGatewayClasses(string $classesDir): array
|
||||
{
|
||||
$namespace = self::getGatewaysNamespace();
|
||||
$economyDirPaths = \scandir($classesDir);
|
||||
|
||||
$classes = [];
|
||||
foreach ($economyDirPaths as $path) {
|
||||
if ($path === '.' || $path === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = \rtrim($path, '.php');
|
||||
if (!\str_ends_with($className, 'Gateway')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass(sprintf("%s\%s", $namespace, $className));
|
||||
if ($reflection->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classes[] = $reflection->getName();
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the gateway names are unique for each gateway.
|
||||
*
|
||||
* @param array $gatewayClasses Fully-qualified Gateway class names
|
||||
*
|
||||
* @throws \Exception If there are two different Gateway classes with the same name
|
||||
*
|
||||
* @see \App\Library\Economy\Payment\GatewayInterface::getName() To see the names returned by the implementations
|
||||
*/
|
||||
public static function validateGatewayNames(array $gatewayClasses): void
|
||||
{
|
||||
$gatewaysValidated = [];
|
||||
foreach ($gatewayClasses as $gatewayClass) {
|
||||
$gatewayName = $gatewayClass::getName();
|
||||
|
||||
if (\array_key_exists($gatewayName, $gatewaysValidated)) {
|
||||
$exceptionMessage = sprintf(
|
||||
"Duplicate Gateway name '%s' from class %s, name is already in use by class %s",
|
||||
$gatewayName,
|
||||
$gatewayClass,
|
||||
$gatewaysValidated[$gatewayName]
|
||||
);
|
||||
|
||||
throw new \Exception($exceptionMessage);
|
||||
}
|
||||
|
||||
$gatewaysValidated[$gatewayName] = $gatewayClass;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the gateway names in disk.
|
||||
*
|
||||
* @param array $names The names returned by the gateways
|
||||
*
|
||||
* @see \App\Library\Economy\Payment\GatewayInterface::getName() To see the names returned by the implementations
|
||||
*/
|
||||
public static function compileGatewayNames(array $names): void
|
||||
{
|
||||
self::writeLockFile($names);
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$classesDir = join(DIRECTORY_SEPARATOR, [
|
||||
$container->getParameter('kernel.project_dir'),
|
||||
'src',
|
||||
...\array_slice(explode('\\', self::getGatewaysNamespace()), 1),
|
||||
]);
|
||||
|
||||
$gatewayClasses = self::getGatewayClasses($classesDir);
|
||||
self::validateGatewayNames($gatewayClasses);
|
||||
|
||||
$gatewayNames = $this->getGatewayNames($gatewayClasses);
|
||||
self::compileGatewayNames($gatewayNames);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Service\ApiResourceNormalizer;
|
||||
use App\Service\ApiService;
|
||||
use App\Service\VersionedResourceService;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
@@ -28,7 +28,7 @@ class VersionedResourcePass implements CompilerPassInterface
|
||||
|
||||
$versionedResourceNames = [
|
||||
...$versionedResourceNames,
|
||||
ApiResourceNormalizer::toResource($entityClass),
|
||||
ApiService::toResource($entityClass),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ final class SystemVariablesLoader implements EnvVarLoaderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private SystemVariableRepository $systemVarRepository,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function loadEnvVars(): array
|
||||
{
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Repository\AccountingRepository;
|
||||
use App\Service\ApiResourceNormalizer;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Accountings are the receivers and the issuers of Transactions.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: AccountingRepository::class)]
|
||||
#[API\Get()]
|
||||
#[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')]
|
||||
class Accounting
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The preferred currency for monetary operations.\
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[Assert\Currency()]
|
||||
#[ORM\Column(length: 3)]
|
||||
private ?string $currency = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, AccountingTransaction>
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)]
|
||||
private Collection $transactionsIssued;
|
||||
|
||||
/**
|
||||
* @var Collection<int, AccountingTransaction>
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)]
|
||||
private Collection $transactionsReceived;
|
||||
|
||||
#[API\ApiProperty(writable: false, readable: false)]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $ownerClass = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])]
|
||||
private ?User $user = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])]
|
||||
private ?Project $project = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])]
|
||||
private ?Tipjar $tipjar = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
/*
|
||||
* TO-DO: This property must be loaded from App's configuration,
|
||||
* ideally a configuration that can be updated via a frontend, not env var only
|
||||
*/
|
||||
$this->currency = 'EUR';
|
||||
$this->transactionsIssued = new ArrayCollection();
|
||||
$this->transactionsReceived = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCurrency(): ?string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function setCurrency(string $currency): static
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, AccountingTransaction>
|
||||
*/
|
||||
public function getTransactionsIssued(): Collection
|
||||
{
|
||||
return $this->transactionsIssued;
|
||||
}
|
||||
|
||||
public function addTransactionsIssued(AccountingTransaction $transactionsIssued): static
|
||||
{
|
||||
if (!$this->transactionsIssued->contains($transactionsIssued)) {
|
||||
$this->transactionsIssued->add($transactionsIssued);
|
||||
$transactionsIssued->setOrigin($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTransactionsIssued(AccountingTransaction $transactionsIssued): static
|
||||
{
|
||||
if ($this->transactionsIssued->removeElement($transactionsIssued)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($transactionsIssued->getOrigin() === $this) {
|
||||
$transactionsIssued->setOrigin(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, AccountingTransaction>
|
||||
*/
|
||||
public function getTransactionsReceived(): Collection
|
||||
{
|
||||
return $this->transactionsReceived;
|
||||
}
|
||||
|
||||
public function addTransactionsReceived(AccountingTransaction $transactionsReceived): static
|
||||
{
|
||||
if (!$this->transactionsReceived->contains($transactionsReceived)) {
|
||||
$this->transactionsReceived->add($transactionsReceived);
|
||||
$transactionsReceived->setTarget($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTransactionsReceived(AccountingTransaction $transactionsReceived): static
|
||||
{
|
||||
if ($this->transactionsReceived->removeElement($transactionsReceived)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($transactionsReceived->getTarget() === $this) {
|
||||
$transactionsReceived->setTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwnerClass(): ?string
|
||||
{
|
||||
return $this->ownerClass;
|
||||
}
|
||||
|
||||
public function setOwnerClass(string $ownerClass): static
|
||||
{
|
||||
// ensure ownership does not change
|
||||
if (
|
||||
$this->ownerClass !== null
|
||||
&& $this->ownerClass !== $ownerClass
|
||||
) {
|
||||
throw new \Exception('Are you trying to commit fraud? Cannot change ownership of an Accounting.');
|
||||
}
|
||||
|
||||
$this->ownerClass = $ownerClass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwnerResource(): string
|
||||
{
|
||||
return ApiResourceNormalizer::toResource($this->ownerClass);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($user->getAccounting() !== $this) {
|
||||
$user->setAccounting($this);
|
||||
}
|
||||
|
||||
// set the owner class
|
||||
$this->setOwnerClass($user::class);
|
||||
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(Project $project): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($project->getAccounting() !== $this) {
|
||||
$project->setAccounting($this);
|
||||
}
|
||||
|
||||
// set the owner class
|
||||
$this->setOwnerClass($project::class);
|
||||
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTipjar(): ?Tipjar
|
||||
{
|
||||
return $this->tipjar;
|
||||
}
|
||||
|
||||
public function setTipjar(Tipjar $tipjar): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($tipjar->getAccounting() !== $this) {
|
||||
$tipjar->setAccounting($this);
|
||||
}
|
||||
|
||||
// set the owner class
|
||||
$this->setOwnerClass($tipjar::class);
|
||||
|
||||
$this->tipjar = $tipjar;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Accounting;
|
||||
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Accounting\AccountingRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Accountings are the receivers and the issuers of Transactions.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: AccountingRepository::class)]
|
||||
class Accounting
|
||||
{
|
||||
public const OWNER_CHANGE_NOT_ALLOWED = 'Are you trying to commit fraud? Cannot change ownership of an Accounting.';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The preferred currency for monetary operations.\
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[Assert\Currency()]
|
||||
#[ORM\Column(length: 3)]
|
||||
private ?string $currency = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $owner = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?Project $project = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?Tipjar $tipjar = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
/*
|
||||
* TO-DO: This property must be loaded from App's configuration,
|
||||
* ideally a configuration that can be updated via a frontend, not env var only
|
||||
*/
|
||||
$this->currency = 'EUR';
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCurrency(): ?string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function setCurrency(string $currency): static
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwner(): AccountingOwnerInterface
|
||||
{
|
||||
$owner = $this->owner;
|
||||
|
||||
return $this->$owner;
|
||||
}
|
||||
|
||||
public function setOwner(AccountingOwnerInterface $owner): static
|
||||
{
|
||||
if ($owner instanceof User) {
|
||||
$this->owner = 'user';
|
||||
|
||||
return $this->setUser($owner);
|
||||
}
|
||||
|
||||
if ($owner instanceof Project) {
|
||||
$this->owner = 'project';
|
||||
|
||||
return $this->setProject($owner);
|
||||
}
|
||||
|
||||
if ($owner instanceof Tipjar) {
|
||||
$this->owner = 'tipjar';
|
||||
|
||||
return $this->setTipjar($owner);
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('%s is not a recognized AccountingOwnerInterface', $owner::class));
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
// unset the owning side of the relation if necessary
|
||||
if ($user === null && $this->user !== null) {
|
||||
$this->user->setAccounting(null);
|
||||
}
|
||||
|
||||
// set the owning side of the relation if necessary
|
||||
if ($user !== null && $user->getAccounting() !== $this) {
|
||||
$user->setAccounting($this);
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(?Project $project): static
|
||||
{
|
||||
// unset the owning side of the relation if necessary
|
||||
if ($project === null && $this->project !== null) {
|
||||
$this->project->setAccounting(null);
|
||||
}
|
||||
|
||||
// set the owning side of the relation if necessary
|
||||
if ($project !== null && $project->getAccounting() !== $this) {
|
||||
$project->setAccounting($this);
|
||||
}
|
||||
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTipjar(): ?Tipjar
|
||||
{
|
||||
return $this->tipjar;
|
||||
}
|
||||
|
||||
public function setTipjar(?Tipjar $tipjar): static
|
||||
{
|
||||
// unset the owning side of the relation if necessary
|
||||
if ($tipjar === null && $this->tipjar !== null) {
|
||||
$this->tipjar->setAccounting(null);
|
||||
}
|
||||
|
||||
// set the owning side of the relation if necessary
|
||||
if ($tipjar !== null && $tipjar->getAccounting() !== $this) {
|
||||
$tipjar->setAccounting($this);
|
||||
}
|
||||
|
||||
$this->tipjar = $tipjar;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Entity\Accounting;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Repository\AccountingTransactionRepository;
|
||||
use App\Entity\Money;
|
||||
use App\Repository\Accounting\TransactionRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -16,8 +17,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* AccountingTransactions are generated for each GatewayCharge in a GatewayCheckout once it becomes successful.
|
||||
*/
|
||||
#[API\Get()]
|
||||
#[ORM\Entity(repositoryClass: AccountingTransactionRepository::class)]
|
||||
class AccountingTransaction
|
||||
#[ORM\Entity(repositoryClass: TransactionRepository::class)]
|
||||
class Transaction
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -34,16 +35,16 @@ class AccountingTransaction
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* The Accounting issuing the money of this Transaction.
|
||||
* The Accounting from which the Transaction comes from.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'transactionsIssued', cascade: ['persist'])]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $origin = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the money of this Transaction.
|
||||
* The Accounting where the Transaction goes to.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'transactionsReceived', cascade: ['persist'])]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $target = null;
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Gateway;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Money;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Repository\Gateway\ChargeRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCharge represents a monetary payment that can be done by an issuer at checkout with the Gateway.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: ChargeRepository::class)]
|
||||
class Charge
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'charges')]
|
||||
private ?Checkout $checkout = null;
|
||||
|
||||
/**
|
||||
* The type represents the kind of payment for the charged money.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column()]
|
||||
private ?ChargeType $type = null;
|
||||
|
||||
/**
|
||||
* A short, descriptive text for this charge operation.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
/**
|
||||
* Detailed message about this charge operation.
|
||||
*/
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the consequent Transaction for this GatewayCharge.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $target = null;
|
||||
|
||||
/**
|
||||
* The charged monetary sum.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Embedded(Money::class)]
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Transaction>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])]
|
||||
private Collection $transactions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->transactions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCheckout(): ?Checkout
|
||||
{
|
||||
return $this->checkout;
|
||||
}
|
||||
|
||||
public function setCheckout(?Checkout $checkout): static
|
||||
{
|
||||
$this->checkout = $checkout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): ?ChargeType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(ChargeType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTarget(): ?Accounting
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function setTarget(?Accounting $target): static
|
||||
{
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMoney(): ?Money
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(Money $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Transaction>
|
||||
*/
|
||||
public function getTransactions(): Collection
|
||||
{
|
||||
return $this->transactions;
|
||||
}
|
||||
|
||||
public function addTransaction(Transaction $transaction): static
|
||||
{
|
||||
if (!$this->transactions->contains($transaction)) {
|
||||
$this->transactions->add($transaction);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTransaction(Transaction $transaction): static
|
||||
{
|
||||
$this->transactions->removeElement($transaction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Gateway;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use App\Validator\SupportedChargeTypes;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCheckout bundles the data to perform a payment operation with a Gateway.\
|
||||
* \
|
||||
* Once the Gateway validates the payment as successful the GatewayCheckout will be updated
|
||||
* and respective AccountingTransactions will be generated for each Charge.
|
||||
*/
|
||||
#[Gedmo\Loggable()]
|
||||
#[ORM\Entity(repositoryClass: CheckoutRepository::class)]
|
||||
#[ORM\Index(fields: ['migratedId'])]
|
||||
class Checkout
|
||||
{
|
||||
use MigratedEntity;
|
||||
use TimestampedCreationEntity;
|
||||
use TimestampedUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The Accounting that will issue the Transactions of the GatewayCharges after a successful checkout.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $origin = null;
|
||||
|
||||
/**
|
||||
* The status of the checkout with the Gateway.
|
||||
*/
|
||||
#[Gedmo\Versioned]
|
||||
#[ORM\Column()]
|
||||
private ?CheckoutStatus $status = null;
|
||||
|
||||
/**
|
||||
* The GatewayCharges to be charged at checkout with the gateway.
|
||||
*
|
||||
* @var Collection<int, Charge>
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Count(min: 1)]
|
||||
#[SupportedChargeTypes()]
|
||||
#[ORM\OneToMany(mappedBy: 'checkout', targetEntity: Charge::class, cascade: ['persist'])]
|
||||
private Collection $charges;
|
||||
|
||||
/**
|
||||
* The name of the Gateway implementation to checkout with.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $gateway = null;
|
||||
|
||||
/**
|
||||
* A list of URLs provided by the Gateway for this checkout.\
|
||||
* e.g: Fulfill payment, API resource address.
|
||||
*
|
||||
* @var Link[]
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $links = [];
|
||||
|
||||
/**
|
||||
* A list of tracking codes provided by the Gateway for this checkout.\
|
||||
* e.g: Order ID, Payment Capture ID, Checkout Session Token.
|
||||
*
|
||||
* @var Tracking[]
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $trackings = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->status = CheckoutStatus::Pending;
|
||||
$this->charges = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOrigin(): ?Accounting
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function setOrigin(?Accounting $origin): static
|
||||
{
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?CheckoutStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(CheckoutStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Charge>
|
||||
*/
|
||||
public function getCharges(): Collection
|
||||
{
|
||||
return $this->charges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Charge> $charges
|
||||
*/
|
||||
public function setCharges(Collection $charges): static
|
||||
{
|
||||
$this->charges = new ArrayCollection();
|
||||
|
||||
foreach ($charges as $charge) {
|
||||
$this->charges->add($charge);
|
||||
$charge->setCheckout($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addCharge(Charge $charge): static
|
||||
{
|
||||
if (!$this->charges->contains($charge)) {
|
||||
$this->charges->add($charge);
|
||||
$charge->setCheckout($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCharge(Charge $charge): static
|
||||
{
|
||||
if ($this->charges->removeElement($charge)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($charge->getCheckout() === $this) {
|
||||
$charge->setCheckout(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGateway(): ?string
|
||||
{
|
||||
return $this->gateway;
|
||||
}
|
||||
|
||||
public function setGateway(string $gateway): static
|
||||
{
|
||||
$this->gateway = $gateway;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Link[]
|
||||
*/
|
||||
public function getLinks(): array
|
||||
{
|
||||
return $this->links;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Link[] $links
|
||||
*/
|
||||
public function setLinks(array $links): static
|
||||
{
|
||||
$this->links = $links;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addLink(Link $link): static
|
||||
{
|
||||
$this->links = [...$this->links, $link];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeLink(Link $link): static
|
||||
{
|
||||
$this->links = \array_filter(
|
||||
$this->links,
|
||||
function (Link $existingLink) use ($link) {
|
||||
return $existingLink->href !== $link->href;
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tracking[]
|
||||
*/
|
||||
public function getTrackings(): array
|
||||
{
|
||||
return $this->trackings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tracking[] $trackings
|
||||
*/
|
||||
public function setTrackings(array $trackings): static
|
||||
{
|
||||
$this->trackings = $trackings;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTracking(Tracking $tracking): static
|
||||
{
|
||||
$this->trackings = [...$this->trackings, $tracking];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTracking(Tracking $tracking): static
|
||||
{
|
||||
$this->trackings = \array_filter(
|
||||
$this->trackings,
|
||||
function (Tracking $existingTracking) use ($tracking) {
|
||||
return $existingTracking->title !== $tracking->title
|
||||
&& $existingTracking->value !== $tracking->value;
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Repository\GatewayChargeRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCharge represents a monetary payment that can be done by an issuer at checkout with the Gateway.
|
||||
*/
|
||||
#[API\ApiResource()]
|
||||
#[ORM\Entity(repositoryClass: GatewayChargeRepository::class)]
|
||||
class GatewayCharge
|
||||
{
|
||||
// TO-DO: This message should be translatable to the User's language
|
||||
public const MESSAGE_STATEMENT = 'PAGO EN GOTEO.ORG';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The type of a GatewayCharge represents the kind of payment.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column()]
|
||||
private ?GatewayChargeType $type = null;
|
||||
|
||||
/**
|
||||
* The charged monetary sum.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Embedded(Money::class)]
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the consequent Transaction for this GatewayCharge.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $target = null;
|
||||
|
||||
/**
|
||||
* The AccountingTransaction generated for this charge after the checkout with the Gateway.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
|
||||
private ?AccountingTransaction $transaction = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): ?GatewayChargeType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(GatewayChargeType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMoney(): ?Money
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(Money $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTarget(): ?Accounting
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function setTarget(?Accounting $target): static
|
||||
{
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTransaction(): ?AccountingTransaction
|
||||
{
|
||||
return $this->transaction;
|
||||
}
|
||||
|
||||
public function setTransaction(?AccountingTransaction $transaction): static
|
||||
{
|
||||
$this->transaction = $transaction;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampableUpdationEntity;
|
||||
use App\Repository\GatewayCheckoutRepository;
|
||||
use App\State\GatewayCheckoutProcessor;
|
||||
use App\Validator\GatewayName;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCheckout bundles the data to perform a payment operation with a Gateway.\
|
||||
* \
|
||||
* Once the Gateway validates the payment as successful the GatewayCheckout will be updated
|
||||
* and respective AccountingTransactions will be generated for each GatewayCharge.
|
||||
*/
|
||||
#[Gedmo\Loggable()]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post(processor: GatewayCheckoutProcessor::class)]
|
||||
#[API\Get()]
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, properties: ['origin' => 'exact', 'charges.target' => 'exact'])]
|
||||
#[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)]
|
||||
#[ORM\Index(fields: ['migratedId'])]
|
||||
class GatewayCheckout
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampableUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The Accounting that will issue the Transactions of the GatewayCharges after a successful checkout.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $origin = null;
|
||||
|
||||
/**
|
||||
* The GatewayCharges to be charged at checkout with the Gateway.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Count(min: 1)]
|
||||
#[ORM\ManyToMany(targetEntity: GatewayCharge::class, cascade: ['persist'])]
|
||||
private Collection $charges;
|
||||
|
||||
/**
|
||||
* The status of the checkout with the Gateway.
|
||||
*/
|
||||
#[Gedmo\Versioned]
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(SearchFilter::class)]
|
||||
#[ORM\Column()]
|
||||
private ?GatewayCheckoutStatus $status = null;
|
||||
|
||||
/**
|
||||
* The name of the Gateway implementation to checkout with.
|
||||
*/
|
||||
#[GatewayName]
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiFilter(SearchFilter::class)]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $gateway = null;
|
||||
|
||||
/**
|
||||
* A list of URLs provided by the Gateway for this checkout.\
|
||||
* e.g: Fulfill payment, API resource address.
|
||||
*
|
||||
* @var GatewayLink[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private array $gatewayLinks = [];
|
||||
|
||||
/**
|
||||
* A list of tracking codes provided by the Gateway for this checkout.\
|
||||
* e.g: Order ID, Payment Capture ID, Checkout Session Token.
|
||||
*
|
||||
* @var GatewayTracking[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private array $gatewayTrackings = [];
|
||||
|
||||
/**
|
||||
* GatewayCheckout was migrated from an invest record in Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private ?bool $migrated = false;
|
||||
|
||||
/**
|
||||
* The id of the original invest record in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $migratedId = null;
|
||||
|
||||
/**
|
||||
* A free-form collection of additional data associated with this checkout operation.
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?array $metadata = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->charges = new ArrayCollection();
|
||||
$this->status = GatewayCheckoutStatus::Pending;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOrigin(): ?Accounting
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function setOrigin(?Accounting $origin): static
|
||||
{
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, GatewayCharge>
|
||||
*/
|
||||
public function getCharges(): Collection
|
||||
{
|
||||
return $this->charges;
|
||||
}
|
||||
|
||||
public function addCharge(GatewayCharge $charge): static
|
||||
{
|
||||
if (!$this->charges->contains($charge)) {
|
||||
$this->charges->add($charge);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCharge(GatewayCharge $charge): static
|
||||
{
|
||||
$this->charges->removeElement($charge);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?GatewayCheckoutStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(GatewayCheckoutStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGateway(): ?string
|
||||
{
|
||||
return $this->gateway;
|
||||
}
|
||||
|
||||
public function setGateway(string $gateway): static
|
||||
{
|
||||
$this->gateway = $gateway;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GatewayLink[]
|
||||
*/
|
||||
public function getGatewayLinks(): array
|
||||
{
|
||||
return $this->gatewayLinks;
|
||||
}
|
||||
|
||||
public function addGatewayLink(GatewayLink $link): static
|
||||
{
|
||||
$this->gatewayLinks = [...$this->gatewayLinks, $link];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeGatewayLink(GatewayLink $link): static
|
||||
{
|
||||
$this->gatewayLinks = \array_filter(
|
||||
$this->gatewayLinks,
|
||||
function (GatewayLink $existingLink) use ($link) {
|
||||
return $existingLink->href !== $link->href;
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GatewayTracking[]
|
||||
*/
|
||||
public function getGatewayTrackings(): array
|
||||
{
|
||||
return $this->gatewayTrackings;
|
||||
}
|
||||
|
||||
public function addGatewayTracking(GatewayTracking $tracking): static
|
||||
{
|
||||
$this->gatewayTrackings = [...$this->gatewayTrackings, $tracking];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeGatewayTracking(GatewayTracking $tracking): static
|
||||
{
|
||||
$this->gatewayTrackings = \array_filter(
|
||||
$this->gatewayTrackings,
|
||||
function (GatewayTracking $existingTracking) use ($tracking) {
|
||||
return $existingTracking->title !== $tracking->title
|
||||
&& $existingTracking->value !== $tracking->value;
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedId(): ?string
|
||||
{
|
||||
return $this->migratedId;
|
||||
}
|
||||
|
||||
public function setMigratedId(?string $migratedId): static
|
||||
{
|
||||
$this->migratedId = $migratedId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMetadata(): ?array
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
public function setMetadata(?array $metadata): static
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Interface;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
|
||||
interface AccountingOwnerInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getAccounting(): ?Accounting;
|
||||
|
||||
public function setAccounting(Accounting $accounting): static;
|
||||
}
|
||||
+15
-47
@@ -4,8 +4,11 @@ namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampableUpdationEntity;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
@@ -23,10 +26,11 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
'owner' => 'exact',
|
||||
])]
|
||||
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
|
||||
class Project
|
||||
class Project implements AccountingOwnerInterface
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampableUpdationEntity;
|
||||
use MigratedEntity;
|
||||
use TimestampedCreationEntity;
|
||||
use TimestampedUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -44,8 +48,7 @@ class Project
|
||||
* A Project's Accounting represents how much money the Project has raised from the community.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'project', cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'project', cascade: ['persist'])]
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
/**
|
||||
@@ -64,23 +67,12 @@ class Project
|
||||
#[ORM\Column(type: 'string', enumType: ProjectStatus::class)]
|
||||
private ProjectStatus $status;
|
||||
|
||||
/**
|
||||
* Project was migrated from Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private ?bool $migrated = null;
|
||||
|
||||
/**
|
||||
* The previous id of this Project in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $migratedId = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->migrated = false;
|
||||
$accounting = new Accounting();
|
||||
$accounting->setOwner($this);
|
||||
|
||||
$this->accounting = $accounting;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -105,7 +97,7 @@ class Project
|
||||
return $this->accounting;
|
||||
}
|
||||
|
||||
public function setAccounting(Accounting $accounting): static
|
||||
public function setAccounting(?Accounting $accounting): static
|
||||
{
|
||||
$this->accounting = $accounting;
|
||||
|
||||
@@ -135,28 +127,4 @@ class Project
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedId(): ?string
|
||||
{
|
||||
return $this->migratedId;
|
||||
}
|
||||
|
||||
public function setMigratedId(?string $migratedId): static
|
||||
{
|
||||
$this->migratedId = $migratedId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -3,6 +3,8 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Repository\TipjarRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
@@ -19,7 +21,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
#[API\Delete()]
|
||||
#[API\Patch()]
|
||||
#[ORM\Entity(repositoryClass: TipjarRepository::class)]
|
||||
class Tipjar
|
||||
class Tipjar implements AccountingOwnerInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -33,10 +35,17 @@ class Tipjar
|
||||
private ?string $name = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'tipjar', cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'tipjar', cascade: ['persist'])]
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$accounting = new Accounting();
|
||||
$accounting->setOwner($this);
|
||||
|
||||
$this->accounting = $accounting;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -59,7 +68,7 @@ class Tipjar
|
||||
return $this->accounting;
|
||||
}
|
||||
|
||||
public function setAccounting(Accounting $accounting): static
|
||||
public function setAccounting(?Accounting $accounting): static
|
||||
{
|
||||
$this->accounting = $accounting;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Trait;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait MigratedEntity
|
||||
{
|
||||
/**
|
||||
* Entity was migrated from the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column()]
|
||||
protected ?bool $migrated = false;
|
||||
|
||||
/**
|
||||
* Previous ID of the entity in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
protected ?string $migratedId = null;
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedId(): ?string
|
||||
{
|
||||
return $this->migratedId;
|
||||
}
|
||||
|
||||
public function setMigratedId(string $migratedId): static
|
||||
{
|
||||
$this->migratedId = $migratedId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
trait TimestampableCreationEntity
|
||||
trait TimestampedCreationEntity
|
||||
{
|
||||
/**
|
||||
* Entity creation timestamp.
|
||||
+1
-1
@@ -7,7 +7,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
trait TimestampableUpdationEntity
|
||||
trait TimestampedUpdationEntity
|
||||
{
|
||||
/**
|
||||
* Entity last update timestamp.
|
||||
+18
-47
@@ -3,9 +3,12 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Interface\UserOwnedInterface;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampableUpdationEntity;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Filter\OrderedLikeFilter;
|
||||
use App\Filter\UserQueryFilter;
|
||||
use App\Repository\UserRepository;
|
||||
@@ -38,10 +41,11 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')]
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Index(fields: ['migratedId'])]
|
||||
class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface
|
||||
class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampableUpdationEntity;
|
||||
use MigratedEntity;
|
||||
use TimestampedCreationEntity;
|
||||
use TimestampedUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -95,8 +99,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
private ?bool $emailConfirmed = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'user', cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'user', cascade: ['persist'])]
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
/**
|
||||
@@ -125,20 +128,6 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $name = null;
|
||||
|
||||
/**
|
||||
* User was migrated from Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private ?bool $migrated = null;
|
||||
|
||||
/**
|
||||
* The previous id of this User in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $migratedId = null;
|
||||
|
||||
/**
|
||||
* The projects owned by this User.
|
||||
*/
|
||||
@@ -151,7 +140,13 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->migrated = false;
|
||||
$accounting = new Accounting();
|
||||
$accounting->setOwner($this);
|
||||
|
||||
$this->accounting = $accounting;
|
||||
|
||||
$this->emailConfirmed = false;
|
||||
$this->active = false;
|
||||
|
||||
$this->tokens = new ArrayCollection();
|
||||
$this->projects = new ArrayCollection();
|
||||
@@ -289,7 +284,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
return $this->accounting;
|
||||
}
|
||||
|
||||
public function setAccounting(Accounting $accounting): static
|
||||
public function setAccounting(?Accounting $accounting): static
|
||||
{
|
||||
$this->accounting = $accounting;
|
||||
|
||||
@@ -362,30 +357,6 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedId(): ?string
|
||||
{
|
||||
return $this->migratedId;
|
||||
}
|
||||
|
||||
public function setMigratedId(?string $migratedId): static
|
||||
{
|
||||
$this->migratedId = $migratedId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Project>
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Entity;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Dto\UserTokenLoginDto;
|
||||
use App\Entity\Interface\UserOwnedInterface;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Repository\UserTokenRepository;
|
||||
use App\State\UserTokenLoginProcessor;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -25,7 +25,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\Entity(repositoryClass: UserTokenRepository::class)]
|
||||
class UserToken implements UserOwnedInterface
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampedCreationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\WalletFinancementRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* A Financement represents how much money was moved from incoming to outgoing Wallet Statements.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: WalletFinancementRepository::class)]
|
||||
class WalletFinancement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The monetary value used in this Financement.
|
||||
*/
|
||||
#[ORM\Embedded(class: Money::class)]
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* An incoming Statement that originally saved the money in this Financement.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'financesto')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?WalletStatement $origin = null;
|
||||
|
||||
/**
|
||||
* An outgoing Statement that eventually spent the money in this Financement.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'financedBy')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?WalletStatement $target = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getMoney(): ?Money
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(Money $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrigin(): ?WalletStatement
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function setOrigin(?WalletStatement $origin): static
|
||||
{
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTarget(): ?WalletStatement
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function setTarget(?WalletStatement $target): static
|
||||
{
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Gateway\Wallet\StatementDirection;
|
||||
use App\Repository\WalletStatementRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* WalletStatements are used by the Wallet Gateway to track available balances and money movements.\
|
||||
* \
|
||||
* Statements are relative to a Transaction, which either:\
|
||||
* \
|
||||
* a. Targets an User's Accounting, for which an incoming Statement will be created and which will hold the money until it is spent.\
|
||||
* b. Originates from an User's Accounting via the Wallet Gateway, which will create an outgoing Statement with money financed from previous incoming statements.
|
||||
*/
|
||||
#[API\ApiResource()]
|
||||
#[ORM\Entity(repositoryClass: WalletStatementRepository::class)]
|
||||
class WalletStatement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Transaction $transaction = null;
|
||||
|
||||
/**
|
||||
* An `incoming` direction Statement means the Transaction saved money into the wallet.\
|
||||
* An `outgoing` direction Statement means the Transaction spent money from the wallet.
|
||||
*/
|
||||
#[ORM\Column(enumType: StatementDirection::class)]
|
||||
private ?StatementDirection $direction = null;
|
||||
|
||||
/**
|
||||
* For incoming Statements, the balance tells the available money.\
|
||||
* i.e: money not used yet by outgoing Statements.
|
||||
*/
|
||||
#[ORM\Embedded(class: Money::class)]
|
||||
private ?Money $balance = null;
|
||||
|
||||
/**
|
||||
* A collection of Financements for outgoing Statements that were made using money from this Statement.
|
||||
*
|
||||
* Only not empty for incoming Statements.
|
||||
*
|
||||
* @var Collection<int, WalletFinancement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'origin', targetEntity: WalletFinancement::class, cascade: ['persist'])]
|
||||
private Collection $financesTo;
|
||||
|
||||
/**
|
||||
* A collection of Financements from incoming Statements that provided money for this Statement.
|
||||
*
|
||||
* Only not empty for outgoing Statements.
|
||||
*
|
||||
* @var Collection<int, WalletFinancement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'target', targetEntity: WalletFinancement::class, cascade: ['persist'])]
|
||||
private Collection $financedBy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->financesTo = new ArrayCollection();
|
||||
$this->financedBy = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTransaction(): ?Transaction
|
||||
{
|
||||
return $this->transaction;
|
||||
}
|
||||
|
||||
public function setTransaction(Transaction $transaction): static
|
||||
{
|
||||
$this->transaction = $transaction;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDirection(): ?StatementDirection
|
||||
{
|
||||
return $this->direction;
|
||||
}
|
||||
|
||||
public function hasDirection(StatementDirection $direction): bool
|
||||
{
|
||||
return $this->direction === $direction;
|
||||
}
|
||||
|
||||
public function setDirection(StatementDirection $direction): static
|
||||
{
|
||||
$this->direction = $direction;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBalance(): ?Money
|
||||
{
|
||||
return $this->balance;
|
||||
}
|
||||
|
||||
public function setBalance(Money $balance): static
|
||||
{
|
||||
$this->balance = $balance;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WalletFinancement>
|
||||
*/
|
||||
public function getFinancesTo(): Collection
|
||||
{
|
||||
return $this->financesTo;
|
||||
}
|
||||
|
||||
public function addFinancesTo(WalletFinancement $financesTo): static
|
||||
{
|
||||
if (!$this->financesTo->contains($financesTo)) {
|
||||
$this->financesTo->add($financesTo);
|
||||
$financesTo->setOrigin($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeFinancesTo(WalletFinancement $financesTo): static
|
||||
{
|
||||
if ($this->financesTo->removeElement($financesTo)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($financesTo->getOrigin() === $this) {
|
||||
$financesTo->setOrigin(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WalletFinancement>
|
||||
*/
|
||||
public function getFinancedBy(): Collection
|
||||
{
|
||||
return $this->financedBy;
|
||||
}
|
||||
|
||||
public function addFinancedBy(WalletFinancement $financedBy): static
|
||||
{
|
||||
if (!$this->financedBy->contains($financedBy)) {
|
||||
$this->financedBy->add($financedBy);
|
||||
$financedBy->setTarget($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeFinancedBy(WalletFinancement $financedBy): static
|
||||
{
|
||||
if ($this->financedBy->removeElement($financedBy)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($financedBy->getTarget() === $this) {
|
||||
$financedBy->setTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\User;
|
||||
use App\Gateway\Wallet\WalletService;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
event: Events::postPersist,
|
||||
method: 'processTransaction',
|
||||
entity: Transaction::class
|
||||
)]
|
||||
final class WalletTransactionsListener
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private WalletService $wallet,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates a WalletStatement for User-received Transactions.
|
||||
*/
|
||||
public function processTransaction(
|
||||
Transaction $transaction,
|
||||
PostPersistEventArgs $event,
|
||||
) {
|
||||
$target = $transaction->getTarget();
|
||||
|
||||
if ($target->getOwner() instanceof User) {
|
||||
$this->wallet->save($transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Gateway;
|
||||
|
||||
enum GatewayChargeType: string
|
||||
enum ChargeType: string
|
||||
{
|
||||
/**
|
||||
* A one-time charge to the money payer.
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Gateway;
|
||||
|
||||
enum GatewayCheckoutStatus: string
|
||||
enum CheckoutStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
abstract class CashGateway implements GatewayInterface
|
||||
{
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
abstract class CecaGateway implements GatewayInterface
|
||||
{
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
abstract class DropGateway implements GatewayInterface
|
||||
{
|
||||
+15
-10
@@ -1,20 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway;
|
||||
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Gateway interfaces are in charge of connecting with payment gateways to send them the data in `GatewayCheckout` instances.\
|
||||
* Gateway interfaces are in charge of connecting with payment gateways to send them the data in `Checkout` instances.\
|
||||
* \
|
||||
* The flow is:
|
||||
* 1. A client app creates a `GatewayCheckout` record
|
||||
* 2. `App\State\GatewayCheckoutProcessor` calls the `sendData` method of the respective interface for the new `GatewayCheckout`
|
||||
* 3. The interface connects with the gateway and sends the data for the `GatewayCheckout`
|
||||
* 4. The interface updates the `GatewayCheckout` with the checkout url and reference given by the gateway for the checkout session
|
||||
* 1. A client app creates a `Checkout` record
|
||||
* 2. `App\State\GatewayCheckoutProcessor` calls the `sendData` method of the respective interface for the new `Checkout`
|
||||
* 3. The interface connects with the gateway and sends the data for the `Checkout`
|
||||
* 4. The interface updates the `Checkout` with the checkout url and reference given by the gateway for the checkout session
|
||||
* 5. `App\State\GatewayCheckoutProcessor` returns the response data to the client app
|
||||
* 6. The client app redirects the user to the payment with the gateway
|
||||
* 7. The gateway processes the payment and redirects the user back to v4
|
||||
@@ -28,14 +28,19 @@ interface GatewayInterface
|
||||
*/
|
||||
public static function getName(): string;
|
||||
|
||||
/**
|
||||
* @return \App\Gateway\ChargeType[] The charge types that can be processed by this Gateway
|
||||
*/
|
||||
public static function getSupportedChargeTypes(): array;
|
||||
|
||||
/**
|
||||
* Connects with the payment gateway and creates a checkout session so the gateway can process the payment.
|
||||
*
|
||||
* @param GatewayCheckout $checkout The GatewayCheckout with the data for the payment to be charged
|
||||
* @param Checkout $checkout The Checkout with the data for the payment to be charged
|
||||
*
|
||||
* @return GatewayCheckout The GatewayCheckout updated with the data given by the gateway
|
||||
* @return Checkout The Checkout updated with the data given by the gateway
|
||||
*/
|
||||
public function process(GatewayCheckout $checkout): GatewayCheckout;
|
||||
public function process(Checkout $checkout): Checkout;
|
||||
|
||||
/**
|
||||
* When a user is redirected by the gateway we must handle the redirection
|
||||
@@ -1,9 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway;
|
||||
|
||||
use App\DependencyInjection\Compiler\GatewaysCompilerPass;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
|
||||
class GatewayLocator
|
||||
{
|
||||
@@ -19,7 +18,7 @@ class GatewayLocator
|
||||
$this->gatewaysByClass[$gateway::class] = $gateway;
|
||||
}
|
||||
|
||||
GatewaysCompilerPass::validateGatewayNames($this->gatewaysByClass);
|
||||
self::validateGatewayNames($this->gatewaysByClass);
|
||||
|
||||
foreach ($this->gatewaysByClass as $class => $gateway) {
|
||||
$this->gatewaysByName[$gateway::getName()] = $gateway;
|
||||
@@ -42,16 +41,6 @@ class GatewayLocator
|
||||
return \array_keys($this->gatewaysByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string> List of the available Gateway names
|
||||
*
|
||||
* @see GatewaysCompilerPass::compileGatewayNames()
|
||||
*/
|
||||
public static function getNamesStatic(): array
|
||||
{
|
||||
return explode(PHP_EOL, \file_get_contents(GatewaysCompilerPass::getLockFile()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GatewayInterface[]
|
||||
*/
|
||||
@@ -77,7 +66,7 @@ class GatewayLocator
|
||||
/**
|
||||
* @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway
|
||||
*/
|
||||
public function getGatewayOf(GatewayCheckout $checkout): GatewayInterface
|
||||
public function getGatewayOf(Checkout $checkout): GatewayInterface
|
||||
{
|
||||
$gateway = $checkout->getGateway();
|
||||
if (!$gateway) {
|
||||
@@ -86,4 +75,32 @@ class GatewayLocator
|
||||
|
||||
return $this->getGateway($gateway);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the gateway names are unique for each gateway.
|
||||
*
|
||||
* @param array $gatewayClasses Fully-qualified Gateway class names
|
||||
*
|
||||
* @throws \Exception If there are two different Gateway classes that return the same name string
|
||||
*/
|
||||
private static function validateGatewayNames(array $gatewayClasses): void
|
||||
{
|
||||
$gatewaysValidated = [];
|
||||
foreach ($gatewayClasses as $gatewayClass) {
|
||||
$gatewayName = $gatewayClass::getName();
|
||||
|
||||
if (\array_key_exists($gatewayName, $gatewaysValidated)) {
|
||||
$exceptionMessage = sprintf(
|
||||
"Duplicate Gateway name '%s' from class %s, name is already in use by class %s",
|
||||
$gatewayName,
|
||||
$gatewayClass,
|
||||
$gatewaysValidated[$gatewayName]
|
||||
);
|
||||
|
||||
throw new \Exception($exceptionMessage);
|
||||
}
|
||||
|
||||
$gatewaysValidated[$gatewayName] = $gatewayClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Gateway;
|
||||
|
||||
class GatewayLink
|
||||
class Link
|
||||
{
|
||||
/**
|
||||
* The complete target URL.
|
||||
@@ -26,5 +26,5 @@ class GatewayLink
|
||||
* `debug` links are for developers and platform maintainers to get useful information about the checkout.\
|
||||
* `payment` links are for end-users who must visit this link to complete the checkout.
|
||||
*/
|
||||
public GatewayLinkType $type;
|
||||
public LinkType $type;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Gateway;
|
||||
|
||||
enum GatewayLinkType: string
|
||||
enum LinkType: string
|
||||
{
|
||||
/**
|
||||
* A 'debug' type link indicates a link that is helpful to developers and platform maintainers to get info about the checkout.
|
||||
+56
-47
@@ -1,23 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Paypal;
|
||||
|
||||
use ApiPlatform\Api\IriConverterInterface;
|
||||
use App\Entity\GatewayCharge;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\GatewayCheckoutStatus;
|
||||
use App\Entity\GatewayLink;
|
||||
use App\Entity\GatewayLinkType;
|
||||
use App\Entity\GatewayTracking;
|
||||
use App\Repository\GatewayCheckoutRepository;
|
||||
use App\Service\GatewayCheckoutService;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\LinkType;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Brick\Money\Money as BrickMoney;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @see https://developer.paypal.com/studio/checkout/standard/integrate
|
||||
@@ -43,21 +44,26 @@ class PaypalGateway implements GatewayInterface
|
||||
public const TRACKING_TITLE_TRANSACTION = 'PayPal Transaction ID';
|
||||
|
||||
public function __construct(
|
||||
private PaypalGatewayService $paypal,
|
||||
private RouterInterface $router,
|
||||
private PaypalService $paypal,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private IriConverterInterface $iriConverter,
|
||||
private GatewayCheckoutService $checkoutService,
|
||||
private GatewayCheckoutRepository $checkoutRepository,
|
||||
) {
|
||||
}
|
||||
private CheckoutService $checkoutService,
|
||||
private CheckoutRepository $checkoutRepository,
|
||||
) {}
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'paypal';
|
||||
}
|
||||
|
||||
public function process(GatewayCheckout $checkout): GatewayCheckout
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
return [
|
||||
ChargeType::Single,
|
||||
];
|
||||
}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
$order = $this->paypal->postOrder([
|
||||
'intent' => self::PAYPAL_ORDER_INTENT,
|
||||
@@ -65,24 +71,24 @@ class PaypalGateway implements GatewayInterface
|
||||
'payment_source' => $this->getPaypalPaymentSource($checkout),
|
||||
]);
|
||||
|
||||
$tracking = new GatewayTracking();
|
||||
$tracking->setValue($order['id']);
|
||||
$tracking->setTitle(self::TRACKING_TITLE_ORDER);
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_ORDER;
|
||||
$tracking->value = $order['id'];
|
||||
|
||||
$checkout->addGatewayTracking($tracking);
|
||||
$checkout->addTracking($tracking);
|
||||
|
||||
foreach ($order['links'] as $linkData) {
|
||||
$linkType = \in_array($linkData['rel'], ['approve', 'payer-action'])
|
||||
? GatewayLinkType::Payment
|
||||
: GatewayLinkType::Debug;
|
||||
? LinkType::Payment
|
||||
: LinkType::Debug;
|
||||
|
||||
$link = new GatewayLink();
|
||||
$link->setHref($linkData['href']);
|
||||
$link->setRel($linkData['rel']);
|
||||
$link->setMethod($linkData['method']);
|
||||
$link->setType($linkType);
|
||||
$link = new Link();
|
||||
$link->href = $linkData['href'];
|
||||
$link->rel = $linkData['rel'];
|
||||
$link->method = $linkData['method'];
|
||||
$link->type = $linkType;
|
||||
|
||||
$checkout->addGatewayLink($link);
|
||||
$checkout->addLink($link);
|
||||
}
|
||||
|
||||
return $checkout;
|
||||
@@ -90,7 +96,7 @@ class PaypalGateway implements GatewayInterface
|
||||
|
||||
public function handleRedirect(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) {
|
||||
if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) {
|
||||
throw new \Exception(sprintf('Checkout was not completed successfully.'));
|
||||
}
|
||||
|
||||
@@ -98,10 +104,10 @@ class PaypalGateway implements GatewayInterface
|
||||
|
||||
$checkout = $this->checkoutRepository->find($checkoutId);
|
||||
if ($checkout === null) {
|
||||
throw new \Exception(sprintf("GatewayCheckout '%s' could not be found.", $checkoutId));
|
||||
throw new \Exception(sprintf("Checkout '%s' could not be found.", $checkoutId));
|
||||
}
|
||||
|
||||
if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) {
|
||||
if ($checkout->getStatus() === CheckoutStatus::Charged) {
|
||||
return new RedirectResponse($this->iriConverter->getIriFromResource($checkout));
|
||||
}
|
||||
|
||||
@@ -121,15 +127,18 @@ class PaypalGateway implements GatewayInterface
|
||||
}
|
||||
|
||||
foreach ($capture['purchase_units'] as $purchaseUnit) {
|
||||
$tracking = new GatewayTracking();
|
||||
$tracking->setValue($purchaseUnit['payments']['captures'][0]['id']);
|
||||
$tracking->setTitle(self::TRACKING_TITLE_TRANSACTION);
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$tracking->value = $purchaseUnit['payments']['captures'][0]['id'];
|
||||
|
||||
$checkout->addGatewayTracking($tracking);
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
|
||||
$this->entityManager->persist($checkout);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// TO-DO: This should redirect the user to a GUI
|
||||
return new RedirectResponse($this->iriConverter->getIriFromResource($checkout));
|
||||
}
|
||||
@@ -164,21 +173,21 @@ class PaypalGateway implements GatewayInterface
|
||||
$checkout = $this->checkoutRepository->findOneByTracking(self::TRACKING_TITLE_ORDER, $orderId);
|
||||
|
||||
if ($checkout === null) {
|
||||
throw new \Exception(sprintf("Could not find any GatewayCheckout by the GatewayTracking '%s'", $orderId), 1);
|
||||
throw new \Exception(sprintf("Could not find any Checkout by the Tracking '%s'", $orderId), 1);
|
||||
}
|
||||
|
||||
foreach ($event['resource']['purchase_units'] as $purchaseUnit) {
|
||||
$tracking = new GatewayTracking();
|
||||
$tracking->setValue($purchaseUnit['payments']['captures'][0]['id']);
|
||||
$tracking->setTitle(self::TRACKING_TITLE_TRANSACTION);
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$tracking->value = $purchaseUnit['payments']['captures'][0]['id'];
|
||||
|
||||
$checkout->addGatewayTracking($tracking);
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
}
|
||||
|
||||
private function getPaypalMoney(GatewayCharge $charge): array
|
||||
private function getPaypalMoney(Charge $charge): array
|
||||
{
|
||||
$brick = BrickMoney::ofMinor(
|
||||
$charge->getMoney()->amount,
|
||||
@@ -191,21 +200,21 @@ class PaypalGateway implements GatewayInterface
|
||||
];
|
||||
}
|
||||
|
||||
private function getPaypalPurchaseUnits(GatewayCheckout $checkout): array
|
||||
private function getPaypalPurchaseUnits(Checkout $checkout): array
|
||||
{
|
||||
$units = [];
|
||||
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
$money = $this->getPaypalMoney($charge);
|
||||
$reference = $this->checkoutService->getGatewayReference($checkout, $charge);
|
||||
$reference = $this->checkoutService->generateTracking($checkout, $charge);
|
||||
|
||||
$units[] = [
|
||||
'reference_id' => $reference,
|
||||
'custom_id' => $reference,
|
||||
'items' => [
|
||||
[
|
||||
'name' => $charge::MESSAGE_STATEMENT,
|
||||
'description' => $charge::MESSAGE_STATEMENT,
|
||||
'name' => $charge->getTitle(),
|
||||
'description' => $charge->getDescription(),
|
||||
'quantity' => '1',
|
||||
'unit_amount' => [
|
||||
...$money,
|
||||
@@ -226,7 +235,7 @@ class PaypalGateway implements GatewayInterface
|
||||
return $units;
|
||||
}
|
||||
|
||||
private function getPaypalPaymentSource(GatewayCheckout $checkout): array
|
||||
private function getPaypalPaymentSource(Checkout $checkout): array
|
||||
{
|
||||
return [
|
||||
'paypal' => [
|
||||
+8
-4
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Paypal;
|
||||
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
@@ -10,7 +10,7 @@ use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class PaypalGatewayService
|
||||
class PaypalService
|
||||
{
|
||||
private CacheInterface $cache;
|
||||
|
||||
@@ -21,7 +21,11 @@ class PaypalGatewayService
|
||||
private string $paypalWebhookId,
|
||||
private HttpClientInterface $httpClient,
|
||||
) {
|
||||
$this->cache = new FilesystemAdapter();
|
||||
$this->cache = new FilesystemAdapter(\preg_replace(
|
||||
'/[^-+_\.A-Za-z0-9]/',
|
||||
'-',
|
||||
$this->paypalApiAddress
|
||||
));
|
||||
|
||||
$httpOptions = new HttpOptions();
|
||||
$httpOptions->setBaseUri($paypalApiAddress);
|
||||
@@ -58,7 +62,7 @@ class PaypalGatewayService
|
||||
*/
|
||||
public function getAuthToken(): array
|
||||
{
|
||||
return $this->cache->get($this->paypalApiAddress, function (ItemInterface $item): array {
|
||||
return $this->cache->get(urlencode('/v1/oauth2/token'), function (ItemInterface $item): array {
|
||||
$tokenData = $this->generateAuthToken();
|
||||
|
||||
$item->expiresAfter($tokenData['expires_in']);
|
||||
+57
-36
@@ -1,16 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Stripe;
|
||||
|
||||
use ApiPlatform\Api\IriConverterInterface;
|
||||
use App\Entity\GatewayChargeType;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\GatewayCheckoutStatus;
|
||||
use App\Entity\GatewayLink;
|
||||
use App\Entity\GatewayLinkType;
|
||||
use App\Entity\GatewayTracking;
|
||||
use App\Repository\GatewayCheckoutRepository;
|
||||
use App\Service\GatewayCheckoutService;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\User;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\LinkType;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Stripe\Checkout\Session as StripeSession;
|
||||
use Stripe\StripeClient;
|
||||
@@ -19,7 +21,6 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class StripeGateway implements GatewayInterface
|
||||
{
|
||||
@@ -30,9 +31,8 @@ class StripeGateway implements GatewayInterface
|
||||
public function __construct(
|
||||
private string $stripeApiKey,
|
||||
private string $stripeWebhookSecret,
|
||||
private RouterInterface $router,
|
||||
private GatewayCheckoutService $checkoutService,
|
||||
private GatewayCheckoutRepository $checkoutRepository,
|
||||
private CheckoutService $checkoutService,
|
||||
private CheckoutRepository $checkoutRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private IriConverterInterface $iriConverter,
|
||||
) {
|
||||
@@ -44,10 +44,18 @@ class StripeGateway implements GatewayInterface
|
||||
return 'stripe';
|
||||
}
|
||||
|
||||
public function process(GatewayCheckout $checkout): GatewayCheckout
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
return [
|
||||
ChargeType::Single,
|
||||
ChargeType::Recurring,
|
||||
];
|
||||
}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
$session = $this->stripe->checkout->sessions->create([
|
||||
'customer_email' => $checkout->getOrigin()->getUser()->getEmail(),
|
||||
'customer_email' => $this->getStripeCustomer($checkout),
|
||||
'mode' => $this->getStripeMode($checkout),
|
||||
'line_items' => $this->getStripeLineItems($checkout),
|
||||
// Because Symfony's Router encodes query parameters, the value {CHECKOUT_SESSION_ID}
|
||||
@@ -57,20 +65,20 @@ class StripeGateway implements GatewayInterface
|
||||
'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $this->checkoutService->generateRedirectUrl($checkout)),
|
||||
]);
|
||||
|
||||
$link = new GatewayLink();
|
||||
$link = new Link();
|
||||
|
||||
$link->setHref($session->url);
|
||||
$link->setRel('approve');
|
||||
$link->setMethod(Request::METHOD_GET);
|
||||
$link->setType(GatewayLinkType::Payment);
|
||||
$link->href = $session->url;
|
||||
$link->rel = 'approve';
|
||||
$link->method = Request::METHOD_GET;
|
||||
$link->type = LinkType::Payment;
|
||||
|
||||
$checkout->addGatewayLink($link);
|
||||
$checkout->addLink($link);
|
||||
|
||||
$tracking = new GatewayTracking();
|
||||
$tracking->setValue($session->id);
|
||||
$tracking->setTitle(self::TRACKING_TITLE_CHECKOUT);
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_CHECKOUT;
|
||||
$tracking->value = $session->id;
|
||||
|
||||
$checkout->addGatewayTracking($tracking);
|
||||
$checkout->addTracking($tracking);
|
||||
|
||||
return $checkout;
|
||||
}
|
||||
@@ -83,14 +91,14 @@ class StripeGateway implements GatewayInterface
|
||||
$checkout = $this->checkoutRepository->find($request->query->get('checkoutId'));
|
||||
|
||||
if ($checkout === null) {
|
||||
throw new \Exception(sprintf("Stripe checkout '%s' exists but no GatewayCheckout with that reference was found.", $sessionId));
|
||||
throw new \Exception(sprintf("Stripe checkout '%s' exists but no Checkout with that reference was found.", $sessionId));
|
||||
}
|
||||
|
||||
if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) {
|
||||
if ($checkout->getStatus() === CheckoutStatus::Charged) {
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
if ($request->query->get('type') !== GatewayCheckoutService::RESPONSE_TYPE_SUCCESS) {
|
||||
if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) {
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
@@ -100,6 +108,9 @@ class StripeGateway implements GatewayInterface
|
||||
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
|
||||
$this->entityManager->persist($checkout);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// TO-DO: This should redirect the user to a GUI
|
||||
return new RedirectResponse($this->iriConverter->getIriFromResource($checkout));
|
||||
}
|
||||
@@ -121,10 +132,20 @@ class StripeGateway implements GatewayInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function getStripeMode(GatewayCheckout $checkout): string
|
||||
private function getStripeCustomer(Checkout $checkout): string
|
||||
{
|
||||
$owner = $checkout->getOrigin()->getOwner();
|
||||
if (!$owner instanceof User) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $owner->getEmail();
|
||||
}
|
||||
|
||||
private function getStripeMode(Checkout $checkout): string
|
||||
{
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
if ($charge->getType() === GatewayChargeType::Recurring) {
|
||||
if ($charge->getType() === ChargeType::Recurring) {
|
||||
return StripeSession::MODE_SUBSCRIPTION;
|
||||
}
|
||||
}
|
||||
@@ -132,7 +153,7 @@ class StripeGateway implements GatewayInterface
|
||||
return StripeSession::MODE_PAYMENT;
|
||||
}
|
||||
|
||||
private function getStripeLineItems(GatewayCheckout $checkout): array
|
||||
private function getStripeLineItems(Checkout $checkout): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
@@ -140,13 +161,13 @@ class StripeGateway implements GatewayInterface
|
||||
$price = [
|
||||
'currency' => $charge->getMoney()->currency,
|
||||
'unit_amount' => $charge->getMoney()->amount,
|
||||
'product_data' => [
|
||||
'name' => $charge::MESSAGE_STATEMENT,
|
||||
'statement_descriptor' => $charge::MESSAGE_STATEMENT,
|
||||
],
|
||||
'product_data' => \array_filter([
|
||||
'name' => $charge->getTitle(),
|
||||
'statement_descriptor' => $charge->getDescription(),
|
||||
], fn($v) => $v !== null),
|
||||
];
|
||||
|
||||
if ($charge->getType() === GatewayChargeType::Recurring) {
|
||||
if ($charge->getType() === ChargeType::Recurring) {
|
||||
$price['recurring'] = ['interval' => 'month'];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Gateway;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class GatewayTracking
|
||||
class Tracking
|
||||
{
|
||||
/**
|
||||
* A descriptive title for the tracking number.
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Wallet;
|
||||
|
||||
enum StatementDirection: string
|
||||
{
|
||||
/**
|
||||
* The Transaction was received by the Accounting.
|
||||
*/
|
||||
case Incoming = 'incoming';
|
||||
|
||||
/**
|
||||
* The Transaction was issued by the Accounting.
|
||||
*/
|
||||
case Outgoing = 'outgoing';
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Wallet;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\WalletStatement;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use App\Library\Economy\MoneyService;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class WalletGateway implements GatewayInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'wallet';
|
||||
}
|
||||
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
return [
|
||||
ChargeType::Single,
|
||||
];
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
private WalletService $wallet,
|
||||
private MoneyService $money,
|
||||
) {}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
$origin = $checkout->getOrigin();
|
||||
$available = $this->wallet->getBalance($origin);
|
||||
$chargeTotal = $this->getChargeTotal($checkout);
|
||||
|
||||
if ($this->money->isLess($available, $chargeTotal)) {
|
||||
throw new \Exception("Can't spend more than what you have!");
|
||||
}
|
||||
|
||||
$charges = $checkout->getCharges();
|
||||
foreach ($charges as $charge) {
|
||||
$transaction = new Transaction();
|
||||
$transaction->setMoney($charge->getMoney());
|
||||
$transaction->setOrigin($origin);
|
||||
$transaction->setTarget($charge->getTarget());
|
||||
|
||||
$outgoing = new WalletStatement();
|
||||
$outgoing->setTransaction($transaction);
|
||||
$outgoing->setDirection(StatementDirection::Outgoing);
|
||||
|
||||
$outgoing = $this->wallet->spend($transaction);
|
||||
}
|
||||
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
public function handleRedirect(Request $request): RedirectResponse
|
||||
{
|
||||
return new RedirectResponse('');
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request): Response
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
|
||||
private function getChargeTotal(Checkout $checkout): Money
|
||||
{
|
||||
$total = new Money(0, $checkout->getOrigin()->getCurrency());
|
||||
|
||||
$charges = $checkout->getCharges();
|
||||
foreach ($charges as $charge) {
|
||||
$total = $this->money->add($charge->getMoney(), $total);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
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 Brick\Money as Brick;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class WalletService
|
||||
{
|
||||
public function __construct(
|
||||
private MoneyService $money,
|
||||
private WalletStatementRepository $walletStatementRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return \App\Entity\WalletStatement[]
|
||||
*/
|
||||
public function getStatements(Accounting $accounting): array
|
||||
{
|
||||
return $this->walletStatementRepository->findByAccounting($accounting);
|
||||
}
|
||||
|
||||
public function getBalance(Accounting $accounting): Money
|
||||
{
|
||||
$total = Brick\Money::ofMinor(0, $accounting->getCurrency());
|
||||
$statements = $this->getStatements($accounting);
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
$balance = $this->money->toBrick($statement->getBalance());
|
||||
|
||||
if (!$statement->hasDirection(StatementDirection::Incoming)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$total = $total->plus($balance);
|
||||
}
|
||||
|
||||
return $this->money->toMoney($total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the money of a Transaction into the target wallet.
|
||||
*
|
||||
* @param Transaction $transaction The Transaction targetting a wallet
|
||||
*
|
||||
* @return WalletStatement An incoming statement for the target wallet
|
||||
*/
|
||||
public function save(Transaction $transaction): WalletStatement
|
||||
{
|
||||
$statement = new WalletStatement();
|
||||
$statement->setTransaction($transaction);
|
||||
$statement->setDirection(StatementDirection::Incoming);
|
||||
$statement->setBalance($transaction->getMoney());
|
||||
|
||||
$this->entityManager->persist($statement);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the money of a Transaction from the origin wallet.
|
||||
*
|
||||
* @param Transaction $transaction The Transaction originating from a wallet
|
||||
*
|
||||
* @return WalletStatement An outgoing statement financed by previous incoming statements
|
||||
*/
|
||||
public function spend(Transaction $transaction): WalletStatement
|
||||
{
|
||||
$origin = $transaction->getOrigin();
|
||||
|
||||
$spendGoal = $transaction->getMoney();
|
||||
$spentTotal = new Money(0, $spendGoal->currency);
|
||||
|
||||
$outgoing = new WalletStatement();
|
||||
$outgoing->setTransaction($transaction);
|
||||
$outgoing->setDirection(StatementDirection::Outgoing);
|
||||
|
||||
$statements = $this->getStatements($origin);
|
||||
foreach ($statements as $statement) {
|
||||
if ($spendGoal->amount === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$statement->hasDirection(StatementDirection::Incoming)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$incoming = $statement;
|
||||
$balance = $incoming->getBalance();
|
||||
|
||||
if ($balance->amount === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->money->isLess($spendGoal, $balance)) {
|
||||
$balanceSpent = $spendGoal;
|
||||
} else {
|
||||
$balanceSpent = $balance;
|
||||
}
|
||||
|
||||
$financement = new WalletFinancement();
|
||||
$financement->setMoney($balanceSpent);
|
||||
|
||||
$incoming->setBalance($this->money->substract($balanceSpent, $balance));
|
||||
$incoming->addFinancesTo($financement);
|
||||
|
||||
$outgoing->setBalance($this->money->add($balanceSpent, $spentTotal));
|
||||
$outgoing->addFinancedBy($financement);
|
||||
|
||||
$this->entityManager->persist($incoming);
|
||||
$this->entityManager->persist($outgoing);
|
||||
|
||||
$spendGoal = $this->money->substract($balanceSpent, $spendGoal);
|
||||
$spentTotal = $this->money->add($balanceSpent, $spentTotal);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $outgoing;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\DependencyInjection\Compiler\GatewaysCompilerPass;
|
||||
use App\DependencyInjection\Compiler\VersionedResourcePass;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
@@ -15,11 +14,6 @@ class Kernel extends BaseKernel
|
||||
|
||||
protected function build(ContainerBuilder $container): void
|
||||
{
|
||||
$container->addCompilerPass(
|
||||
new GatewaysCompilerPass(),
|
||||
PassConfig::TYPE_AFTER_REMOVING
|
||||
);
|
||||
|
||||
$container->addCompilerPass(
|
||||
new VersionedResourcePass(),
|
||||
PassConfig::TYPE_AFTER_REMOVING
|
||||
|
||||
@@ -11,14 +11,12 @@ abstract class AbstractPump implements PumpInterface
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(?string $key = null, mixed $default = null): array
|
||||
public function getConfig(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if ($key !== null) {
|
||||
return [
|
||||
$key => \array_key_exists($key, $this->config)
|
||||
? $this->config[$key]
|
||||
: $default,
|
||||
];
|
||||
return \array_key_exists($key, $this->config)
|
||||
? $this->config[$key]
|
||||
: $default;
|
||||
}
|
||||
|
||||
return $this->config;
|
||||
|
||||
@@ -2,26 +2,25 @@
|
||||
|
||||
namespace App\Library\Benzina\Pump;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\GatewayCharge;
|
||||
use App\Entity\GatewayChargeType;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\GatewayCheckoutStatus;
|
||||
use App\Entity\GatewayTracking;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Gateway\CashGateway;
|
||||
use App\Gateway\Gateway\CecaGateway;
|
||||
use App\Gateway\Gateway\DropGateway;
|
||||
use App\Gateway\Paypal\PaypalGateway;
|
||||
use App\Gateway\Stripe\StripeGateway;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Gateway\Wallet\WalletGateway;
|
||||
use App\Library\Benzina\Pump\Trait\ArrayPumpTrait;
|
||||
use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait;
|
||||
use App\Library\Economy\Payment\CashGateway;
|
||||
use App\Library\Economy\Payment\CecaGateway;
|
||||
use App\Library\Economy\Payment\DropGateway;
|
||||
use App\Library\Economy\Payment\PaypalGateway;
|
||||
use App\Library\Economy\Payment\StripeGateway;
|
||||
use App\Library\Economy\Payment\WalletGateway;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TipjarRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\GatewayCheckoutService;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
@@ -34,6 +33,10 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction';
|
||||
public const TRACKING_TITLE_PREAPPROVAL = 'v3 Invest Preapproval';
|
||||
|
||||
public const CHARGE_TITLE_PROJECT = 'Pago en Goteo v3 - Donación a proyecto';
|
||||
public const CHARGE_TITLE_POOL = 'Pago en Goteo v3 - Carga de monedero';
|
||||
public const CHARGE_TITLE_TIP = 'Pago en Goteo v3 - Propina a la plataforma';
|
||||
|
||||
private const PLATFORM_TIPJAR_NAME = 'platform';
|
||||
|
||||
private const MAX_INT = 2147483647;
|
||||
@@ -75,9 +78,8 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
private ProjectRepository $projectRepository,
|
||||
private TipjarRepository $tipjarRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private GatewayCheckoutService $checkoutService,
|
||||
) {
|
||||
}
|
||||
private CheckoutService $checkoutService,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $batch): bool
|
||||
{
|
||||
@@ -90,7 +92,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
|
||||
public function pump(mixed $batch): void
|
||||
{
|
||||
$batch = $this->skipPumped($batch, 'id', GatewayCheckout::class, 'migratedId');
|
||||
$batch = $this->skipPumped($batch, 'id', Checkout::class, 'migratedId');
|
||||
|
||||
$tipjar = $this->getPlatformTipjar();
|
||||
|
||||
@@ -112,13 +114,13 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
|
||||
$user = $users[$record['user']];
|
||||
|
||||
$checkout = new GatewayCheckout();
|
||||
$checkout = new Checkout();
|
||||
$checkout->setOrigin($user->getAccounting());
|
||||
$checkout->setStatus($this->getCheckoutStatus($record));
|
||||
$checkout->setGateway($this->getCheckoutGateway($record));
|
||||
|
||||
foreach ($this->getCheckoutTrackings($record) as $tracking) {
|
||||
$checkout->addGatewayTracking($tracking);
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$checkout->setMigrated(true);
|
||||
@@ -127,32 +129,35 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
$checkout->setDateCreated(new \DateTime($record['invested']));
|
||||
$checkout->setDateUpdated(new \DateTime());
|
||||
|
||||
$charge = new GatewayCharge();
|
||||
$charge = new Charge();
|
||||
$charge->setType($this->getChargeType($record));
|
||||
$charge->setMoney($this->getChargeMoney($record['amount'], $record['currency']));
|
||||
|
||||
if (empty($record['project'])) {
|
||||
$charge->setTitle(self::CHARGE_TITLE_POOL);
|
||||
$charge->setTarget($user->getAccounting());
|
||||
}
|
||||
|
||||
if (!empty($record['project'])) {
|
||||
$project = $projects[$record['project']];
|
||||
|
||||
$charge->setTitle(self::CHARGE_TITLE_PROJECT);
|
||||
$charge->setTarget($project->getAccounting());
|
||||
}
|
||||
|
||||
$checkout->addCharge($charge);
|
||||
|
||||
if ($record['donate_amount'] > 0) {
|
||||
$tip = new GatewayCharge();
|
||||
$tip->setType(GatewayChargeType::Single);
|
||||
$tip = new Charge();
|
||||
$tip->setType(ChargeType::Single);
|
||||
$tip->setTitle(self::CHARGE_TITLE_TIP);
|
||||
$tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency']));
|
||||
$tip->setTarget($tipjar->getAccounting());
|
||||
|
||||
$checkout->addCharge($tip);
|
||||
}
|
||||
|
||||
if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) {
|
||||
if ($checkout->getStatus() === CheckoutStatus::Charged) {
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
}
|
||||
|
||||
@@ -208,23 +213,19 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
$tipjar = new Tipjar();
|
||||
$tipjar->setName(self::PLATFORM_TIPJAR_NAME);
|
||||
|
||||
$accounting = new Accounting();
|
||||
$accounting->setTipjar($tipjar);
|
||||
|
||||
$this->entityManager->persist($tipjar);
|
||||
$this->entityManager->persist($accounting);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $tipjar;
|
||||
}
|
||||
|
||||
private function getChargeType(array $record): GatewayChargeType
|
||||
private function getChargeType(array $record): ChargeType
|
||||
{
|
||||
if (\in_array($record['method'], ['stripe_subscription'])) {
|
||||
return GatewayChargeType::Recurring;
|
||||
return ChargeType::Recurring;
|
||||
}
|
||||
|
||||
return GatewayChargeType::Single;
|
||||
return ChargeType::Single;
|
||||
}
|
||||
|
||||
private function getChargeMoney(int $amount, string $currency): Money
|
||||
@@ -238,13 +239,13 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
return new Money($amount, $currency);
|
||||
}
|
||||
|
||||
private function getCheckoutStatus(array $record): GatewayCheckoutStatus
|
||||
private function getCheckoutStatus(array $record): CheckoutStatus
|
||||
{
|
||||
if ($record['status'] < 1) {
|
||||
return GatewayCheckoutStatus::Pending;
|
||||
return CheckoutStatus::Pending;
|
||||
}
|
||||
|
||||
return GatewayCheckoutStatus::Charged;
|
||||
return CheckoutStatus::Charged;
|
||||
}
|
||||
|
||||
private function getCheckoutGateway(array $record): string
|
||||
@@ -268,18 +269,18 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GatewayTracking[]
|
||||
* @return Tracking[]
|
||||
*/
|
||||
private function getCheckoutTrackings(array $record): array
|
||||
{
|
||||
$v3Tracking = new GatewayTracking();
|
||||
$v3Tracking = new Tracking();
|
||||
$v3Tracking->title = self::TRACKING_TITLE_V3;
|
||||
$v3Tracking->value = $record['id'];
|
||||
|
||||
$trackings = [$v3Tracking];
|
||||
|
||||
if (!empty($record['payment'])) {
|
||||
$payment = new GatewayTracking();
|
||||
$payment = new Tracking();
|
||||
$payment->title = self::TRACKING_TITLE_PAYMENT;
|
||||
$payment->value = $record['payment'];
|
||||
|
||||
@@ -287,7 +288,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
}
|
||||
|
||||
if (!empty($record['transaction'])) {
|
||||
$transaction = new GatewayTracking();
|
||||
$transaction = new Tracking();
|
||||
$transaction->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$transaction->value = $record['transaction'];
|
||||
|
||||
@@ -295,7 +296,7 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
}
|
||||
|
||||
if (!empty($record['preapproval'])) {
|
||||
$preapproval = new GatewayTracking();
|
||||
$preapproval = new Tracking();
|
||||
$preapproval->title = self::TRACKING_TITLE_PREAPPROVAL;
|
||||
$preapproval->value = $record['preapproval'];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Library\Benzina\Pump;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectStatus;
|
||||
use App\Entity\User;
|
||||
@@ -97,8 +97,7 @@ class ProjectsPump extends AbstractPump implements PumpInterface
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function supports(mixed $batch): bool
|
||||
{
|
||||
@@ -133,11 +132,7 @@ class ProjectsPump extends AbstractPump implements PumpInterface
|
||||
$project->setDateCreated(new \DateTime($record['created']));
|
||||
$project->setDateUpdated(new \DateTime());
|
||||
|
||||
$accounting = $this->getAccounting($record);
|
||||
$accounting->setProject($project);
|
||||
|
||||
$this->entityManager->persist($project);
|
||||
$this->entityManager->persist($accounting);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
@@ -17,9 +17,9 @@ interface PumpInterface
|
||||
* @param string|null $key A configuration array key to return
|
||||
* @param mixed|null $default Any value to return in case the configuration key does not exist
|
||||
*
|
||||
* @return array The configuration array at the specified key, or all keys if null
|
||||
* @return array The configuration value at the specified key, or full keys and values if null
|
||||
*/
|
||||
public function getConfig(?string $key = null, mixed $default = null): array;
|
||||
public function getConfig(?string $key = null, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* Determines if a data batch is supported by this pump.
|
||||
|
||||
@@ -31,7 +31,7 @@ trait DoctrinePumpTrait
|
||||
string $entityClass,
|
||||
string $entityKey,
|
||||
): array {
|
||||
if (!$this->getConfig('skip-pumped', false)) {
|
||||
if (!$this->getConfig('skip-pumped')) {
|
||||
return $batch;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Library\Benzina\Pump;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\User;
|
||||
use App\Library\Benzina\Pump\Trait\ArrayPumpTrait;
|
||||
use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait;
|
||||
@@ -52,8 +51,7 @@ class UsersPump extends AbstractPump implements PumpInterface
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function supports(mixed $batch): bool
|
||||
{
|
||||
@@ -69,8 +67,12 @@ class UsersPump extends AbstractPump implements PumpInterface
|
||||
$batch = $this->skipPumped($batch, 'id', User::class, 'migratedId');
|
||||
|
||||
foreach ($batch as $key => $record) {
|
||||
$created = new \DateTime($record['created']);
|
||||
if ($created < new \DateTime('2000-01-01')) {
|
||||
$created = new \DateTime($record['modified']);
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setAccounting(new Accounting());
|
||||
$user->setUsername($this->getUsername($record));
|
||||
$user->setPassword($record['password'] ?? '');
|
||||
$user->setEmail($record['email']);
|
||||
@@ -79,12 +81,10 @@ class UsersPump extends AbstractPump implements PumpInterface
|
||||
$user->setActive(false);
|
||||
$user->setMigrated(true);
|
||||
$user->setMigratedId($record['id']);
|
||||
|
||||
$accounting = new Accounting();
|
||||
$accounting->setUser($user);
|
||||
$user->setDateCreated($created);
|
||||
$user->setDateUpdated(new \DateTime());
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->persist($accounting);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
@@ -125,11 +125,11 @@ class EuropeanCentralBankExchange implements ExchangeInterface
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function getConversion(MoneyContainer $money, string $currency): EntityMoney
|
||||
public function convert(MoneyContainer $money, string $toCurrency): EntityMoney
|
||||
{
|
||||
$converted = $this->converter->convert(
|
||||
$money,
|
||||
$currency,
|
||||
$toCurrency,
|
||||
null,
|
||||
RoundingMode::HALF_EVEN
|
||||
);
|
||||
@@ -140,14 +140,14 @@ class EuropeanCentralBankExchange implements ExchangeInterface
|
||||
);
|
||||
}
|
||||
|
||||
public function getConversionRate(string $source, string $target): float
|
||||
public function getConversionRate(string $fromCurrency, string $toCurrency): float
|
||||
{
|
||||
return $this->provider->getExchangeRate($source, $target)->toFloat();
|
||||
return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat();
|
||||
}
|
||||
|
||||
public function getConversionDate(string $source, string $target): \DateTimeInterface
|
||||
public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface
|
||||
{
|
||||
$this->provider->getExchangeRate($source, $target);
|
||||
$this->provider->getExchangeRate($fromCurrency, $toCurrency);
|
||||
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Library\Economy\Currency;
|
||||
|
||||
use App\Entity\Money as EntityMoney;
|
||||
use App\Entity\Money;
|
||||
use Brick\Money\Exception\CurrencyConversionException;
|
||||
use Brick\Money\MoneyContainer;
|
||||
|
||||
@@ -19,32 +19,32 @@ interface ExchangeInterface
|
||||
public function getWeight(): int;
|
||||
|
||||
/**
|
||||
* @param MoneyContainer $money The money to be converted
|
||||
* @param string $currency The currency to convert to
|
||||
* @param MoneyContainer $money The money to be converted
|
||||
* @param string $toCurrency The currency to convert to
|
||||
*
|
||||
* @return EntityMoney The converted Money
|
||||
* @return Money The converted Money
|
||||
*
|
||||
* @throws CurrencyConversionException If the exchange rate is not available
|
||||
*/
|
||||
public function getConversion(MoneyContainer $money, string $currency): EntityMoney;
|
||||
public function convert(MoneyContainer $money, string $toCurrency): Money;
|
||||
|
||||
/**
|
||||
* @param string $source The currency to convert from
|
||||
* @param string $target The currency to convert to
|
||||
* @param string $fromCurrency The currency to convert from
|
||||
* @param string $toCurrency The currency to convert to
|
||||
*
|
||||
* @return float The rate of the conversion
|
||||
*
|
||||
* @throws CurrencyConversionException If the exchange rate is not available
|
||||
*/
|
||||
public function getConversionRate(string $source, string $target): float;
|
||||
public function getConversionRate(string $fromCurrency, string $toCurrency): float;
|
||||
|
||||
/**
|
||||
* @param string $source The currency to convert from
|
||||
* @param string $target The currency to convert to
|
||||
* @param string $fromCurrency The currency to convert from
|
||||
* @param string $toCurrency The currency to convert to
|
||||
*
|
||||
* @return \DateTimeInterface The date and time at which the rate was last updated
|
||||
*
|
||||
* @throws CurrencyConversionException If the exchange rate is not available
|
||||
*/
|
||||
public function getConversionDate(string $source, string $target): \DateTimeInterface;
|
||||
public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\Library\Economy\Currency\ExchangeLocator;
|
||||
use Brick\Money as Brick;
|
||||
|
||||
class MoneyService
|
||||
{
|
||||
public function __construct(
|
||||
private ExchangeLocator $exchangeLocator,
|
||||
) {}
|
||||
|
||||
public static function toMoney(Brick\Money $brick): Money
|
||||
{
|
||||
return new Money(
|
||||
$brick->getMinorAmount()->toInt(),
|
||||
$brick->getCurrency()->getCurrencyCode()
|
||||
);
|
||||
}
|
||||
|
||||
public static function toBrick(Money $money): Brick\Money
|
||||
{
|
||||
return Brick\Money::ofMinor($money->amount, $money->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `a` to `b`.
|
||||
*/
|
||||
public function add(Money $a, Money $b): Money
|
||||
{
|
||||
$a = $this->convert($a, $b->currency);
|
||||
$ab = self::toBrick($b)->plus($a);
|
||||
|
||||
return self::toMoney($ab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substracts `a` from `b`.
|
||||
*/
|
||||
public function substract(Money $a, Money $b): Money
|
||||
{
|
||||
$a = $this->convert($a, $b->currency);
|
||||
$ab = self::toBrick($b)->minus($a);
|
||||
|
||||
return self::toMoney($ab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares if one Money is of less value than other.
|
||||
*
|
||||
* @return bool `true` if `$money` is less than `$than`
|
||||
*/
|
||||
public function isLess(Money $money, Money $than): bool
|
||||
{
|
||||
$money = $this->convert($money, $than->currency);
|
||||
|
||||
return $money->isLessThan(self::toBrick($than));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares if one Money is of greater or equal value than other.
|
||||
*
|
||||
* @return bool `true` if `$money` is more than or same as `$than`
|
||||
*/
|
||||
public function isMoreOrSame(Money $money, Money $than): bool
|
||||
{
|
||||
$money = $this->convert($money, $than->currency);
|
||||
|
||||
return $money->isGreaterThanOrEqualTo(self::toBrick($than));
|
||||
}
|
||||
|
||||
private function convert(Money $money, string $toCurrency): Brick\Money
|
||||
{
|
||||
$fromCurrency = $money->currency;
|
||||
if ($fromCurrency === $toCurrency) {
|
||||
return self::toBrick($money);
|
||||
}
|
||||
|
||||
$exchange = $this->exchangeLocator->getExchangeFor($fromCurrency, $toCurrency);
|
||||
|
||||
return self::toBrick($exchange->convert(self::toBrick($money), $toCurrency));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
|
||||
abstract class WalletGateway implements GatewayInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'wallet';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Accounting;
|
||||
|
||||
use App\ApiResource\Accounting as Resource;
|
||||
use App\Entity\Accounting as Entity;
|
||||
use App\Repository\Accounting\AccountingRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class AccountingMapper
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AccountingRepository $accountingRepository,
|
||||
) {}
|
||||
|
||||
public function toResource(Entity\Accounting $entity): Resource\Accounting
|
||||
{
|
||||
$owner = $entity->getOwner();
|
||||
|
||||
$resource = new Resource\Accounting();
|
||||
$resource->id = $entity->getId();
|
||||
$resource->currency = $entity->getCurrency();
|
||||
$resource->owner = $owner;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function toEntity(Resource\Accounting $resource): Entity\Accounting
|
||||
{
|
||||
$entity = new Entity\Accounting();
|
||||
|
||||
if ($resource->id !== null) {
|
||||
$entity = $this->accountingRepository->find($resource->id);
|
||||
}
|
||||
|
||||
$entity->setCurrency($resource->currency);
|
||||
$entity->setOwner($resource->owner);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Gateway;
|
||||
|
||||
use App\ApiResource\Gateway as Resource;
|
||||
use App\Entity\Gateway as Entity;
|
||||
use App\Mapping\Accounting\AccountingMapper;
|
||||
use App\Repository\Gateway\ChargeRepository;
|
||||
|
||||
class ChargeMapper
|
||||
{
|
||||
public function __construct(
|
||||
private AccountingMapper $accountingMapper,
|
||||
private ChargeRepository $chargeRepository,
|
||||
) {}
|
||||
|
||||
public function toResource(Entity\Charge $entity): Resource\Charge
|
||||
{
|
||||
$resource = new Resource\Charge();
|
||||
$resource->id = $entity->getId();
|
||||
$resource->type = $entity->getType();
|
||||
$resource->title = $entity->getTitle();
|
||||
$resource->description = $entity->getDescription();
|
||||
$resource->money = $entity->getMoney();
|
||||
$resource->target = $this->accountingMapper->toResource($entity->getTarget());
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function toEntity(Resource\Charge $resource): Entity\Charge
|
||||
{
|
||||
$entity = new Entity\Charge();
|
||||
|
||||
if ($resource->id !== null) {
|
||||
$entity = $this->chargeRepository->find($resource->id);
|
||||
}
|
||||
|
||||
$entity->setType($resource->type);
|
||||
$entity->setTitle($resource->title);
|
||||
$entity->setDescription($resource->description);
|
||||
$entity->setMoney($resource->money);
|
||||
$entity->setTarget($this->accountingMapper->toEntity($resource->target));
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Gateway;
|
||||
|
||||
use App\ApiResource\Gateway as Resource;
|
||||
use App\Entity\Gateway as Entity;
|
||||
use App\Gateway\GatewayLocator;
|
||||
use App\Mapping\Accounting\AccountingMapper;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
class CheckoutMapper
|
||||
{
|
||||
public function __construct(
|
||||
private GatewayLocator $gatewayLocator,
|
||||
private GatewayMapper $gatewayMapper,
|
||||
private CheckoutRepository $checkoutRepository,
|
||||
private AccountingMapper $accountingMapper,
|
||||
private ChargeMapper $chargeMapper,
|
||||
) {}
|
||||
|
||||
public function toResource(Entity\Checkout $entity): Resource\Checkout
|
||||
{
|
||||
$resource = new Resource\Checkout();
|
||||
$resource->id = $entity->getId();
|
||||
|
||||
$gateway = $this->gatewayLocator->getGateway($entity->getGateway());
|
||||
$resource->gateway = $this->gatewayMapper->toResource($gateway);
|
||||
|
||||
$resource->origin = $this->accountingMapper->toResource($entity->getOrigin());
|
||||
|
||||
$charges = [];
|
||||
foreach ($entity->getCharges() as $charge) {
|
||||
$charges[] = $this->chargeMapper->toResource($charge);
|
||||
}
|
||||
|
||||
$resource->charges = $charges;
|
||||
|
||||
$resource->status = $entity->getStatus();
|
||||
$resource->links = $entity->getLinks();
|
||||
$resource->trackings = $entity->getTrackings();
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function toEntity(Resource\Checkout $resource): Entity\Checkout
|
||||
{
|
||||
$entity = new Entity\Checkout();
|
||||
|
||||
if ($resource->id !== null) {
|
||||
$entity = $this->checkoutRepository->find($resource->id);
|
||||
}
|
||||
|
||||
$entity->setGateway($resource->gateway->name);
|
||||
$entity->setOrigin($this->accountingMapper->toEntity($resource->origin));
|
||||
|
||||
$charges = [];
|
||||
foreach ($resource->charges as $charge) {
|
||||
$charges[] = $this->chargeMapper->toEntity($charge);
|
||||
}
|
||||
|
||||
$entity->setCharges(new ArrayCollection($charges));
|
||||
|
||||
$entity->setStatus($resource->status);
|
||||
$entity->setLinks($resource->links);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Gateway;
|
||||
|
||||
use App\ApiResource\Gateway\Gateway;
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
class GatewayMapper
|
||||
{
|
||||
public function toResource(GatewayInterface $gateway): Gateway
|
||||
{
|
||||
$resource = new Gateway();
|
||||
$resource->name = $gateway::getName();
|
||||
$resource->supports = $gateway::getSupportedChargeTypes();
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,7 @@ use ApiPlatform\OpenApi\OpenApi;
|
||||
|
||||
class OpenApiFactory implements OpenApiFactoryInterface
|
||||
{
|
||||
public function __construct(private OpenApiFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
public function __construct(private OpenApiFactoryInterface $decorated) {}
|
||||
|
||||
private function updateOperation(Operation $operation): Operation
|
||||
{
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
namespace App\Repository\Accounting;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
+11
-11
@@ -1,28 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
namespace App\Repository\Accounting;
|
||||
|
||||
use App\Entity\AccountingTransaction;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AccountingTransaction>
|
||||
* @extends ServiceEntityRepository<Transaction>
|
||||
*
|
||||
* @method AccountingTransaction|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method AccountingTransaction|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method AccountingTransaction[] findAll()
|
||||
* @method AccountingTransaction[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
* @method Transaction|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Transaction|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Transaction[] findAll()
|
||||
* @method Transaction[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class AccountingTransactionRepository extends ServiceEntityRepository
|
||||
class TransactionRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AccountingTransaction::class);
|
||||
parent::__construct($registry, Transaction::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return AccountingTransaction[] Returns an array of AccountingTransaction objects
|
||||
// * @return Transaction[] Returns an array of Transaction objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
@@ -36,7 +36,7 @@ class AccountingTransactionRepository extends ServiceEntityRepository
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?AccountingTransaction
|
||||
// public function findOneBySomeField($value): ?Transaction
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
+11
-11
@@ -1,28 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
namespace App\Repository\Gateway;
|
||||
|
||||
use App\Entity\GatewayCharge;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<GatewayCharge>
|
||||
* @extends ServiceEntityRepository<Charge>
|
||||
*
|
||||
* @method GatewayCharge|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method GatewayCharge|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method GatewayCharge[] findAll()
|
||||
* @method GatewayCharge[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
* @method Charge|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Charge|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Charge[] findAll()
|
||||
* @method Charge[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class GatewayChargeRepository extends ServiceEntityRepository
|
||||
class ChargeRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, GatewayCharge::class);
|
||||
parent::__construct($registry, Charge::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return GatewayCharge[] Returns an array of GatewayCharge objects
|
||||
// * @return Charge[] Returns an array of Charge objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
@@ -36,7 +36,7 @@ class GatewayChargeRepository extends ServiceEntityRepository
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?GatewayCharge
|
||||
// public function findOneBySomeField($value): ?Charge
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository\Gateway;
|
||||
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Checkout>
|
||||
*
|
||||
* @method Checkout|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Checkout|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Checkout[] findAll()
|
||||
* @method Checkout[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class CheckoutRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Checkout::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Checkout|null
|
||||
*/
|
||||
public function findOneByTracking(string $title, string $value): ?array
|
||||
{
|
||||
// TO-DO: Use a JSON lookup into the 'trackings' column to see if it contains title and value
|
||||
return null;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Checkout[] Returns an array of Checkout objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('g.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Checkout
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\GatewayCheckout;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<GatewayCheckout>
|
||||
*
|
||||
* @method GatewayCheckout|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method GatewayCheckout|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method GatewayCheckout[] findAll()
|
||||
* @method GatewayCheckout[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class GatewayCheckoutRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, GatewayCheckout::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GatewayCheckout|null
|
||||
*/
|
||||
public function findOneByTracking(string $title, string $value): array
|
||||
{
|
||||
return $this->createQueryBuilder('g')
|
||||
->join('g.gatewayTrackings', 'gt', Join::WITH, 'gt.checkout = g.id')
|
||||
->andWhere('gt.value = :val')
|
||||
->andWhere('gt.title = :title')
|
||||
->setParameter('val', $value)
|
||||
->setParameter('title', $title)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return GatewayCheckout[] Returns an array of GatewayCheckout objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('g.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?GatewayCheckout
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\WalletFinancement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<WalletFinancement>
|
||||
*/
|
||||
class WalletFinancementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, WalletFinancement::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return WalletFinancement[] Returns an array of WalletFinancement objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('w')
|
||||
// ->andWhere('w.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('w.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?WalletFinancement
|
||||
// {
|
||||
// return $this->createQueryBuilder('w')
|
||||
// ->andWhere('w.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\WalletStatement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<WalletStatement>
|
||||
*/
|
||||
class WalletStatementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, WalletStatement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WalletStatement[]
|
||||
*/
|
||||
public function findByAccounting(Accounting $accounting): array
|
||||
{
|
||||
return $this->createQueryBuilder('w')
|
||||
->join('w.transaction', 'wt', Join::WITH, 'wt.id = w.transaction')
|
||||
->where('wt.origin = :val')
|
||||
->orWhere('wt.target = :val')
|
||||
->setParameter('val', $accounting->getId())
|
||||
->orderBy('w.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WalletStatement[]
|
||||
*/
|
||||
public function findByOrigin(Accounting $accounting): array
|
||||
{
|
||||
return $this->createQueryBuilder('w')
|
||||
->join('w.transaction', 'wt', Join::WITH, 'wt.id = w.transaction')
|
||||
->andWhere('wt.origin = :val')
|
||||
->setParameter('val', $accounting->getId())
|
||||
->orderBy('w.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WalletStatement[]
|
||||
*/
|
||||
public function findByTarget(Accounting $accounting): array
|
||||
{
|
||||
return $this->createQueryBuilder('w')
|
||||
->join('w.transaction', 'wt', Join::WITH, 'wt.id = w.transaction')
|
||||
->andWhere('wt.target = :val')
|
||||
->setParameter('val', $accounting->getId())
|
||||
->orderBy('w.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return WalletStatement[] Returns an array of WalletStatement objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('w')
|
||||
// ->andWhere('w.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('w.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?WalletStatement
|
||||
// {
|
||||
// return $this->createQueryBuilder('w')
|
||||
// ->andWhere('w.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -9,9 +9,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
|
||||
class AccessTokenHandler implements AccessTokenHandlerInterface
|
||||
{
|
||||
public function __construct(private UserTokenRepository $userTokenRepository)
|
||||
{
|
||||
}
|
||||
public function __construct(private UserTokenRepository $userTokenRepository) {}
|
||||
|
||||
public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ class AccountingVoter extends Voter
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return in_array($attribute, [self::EDIT])
|
||||
&& $subject instanceof \App\Entity\Accounting;
|
||||
&& $subject instanceof \App\Entity\Accounting\Accounting;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
/**
|
||||
* Normalizes between Entity classnames and API resource names.
|
||||
*/
|
||||
class ApiResourceNormalizer
|
||||
class ApiService
|
||||
{
|
||||
/**
|
||||
* Converts an Entity class name into an API resource name.
|
||||
@@ -13,8 +13,7 @@ class AuthService
|
||||
|
||||
public function __construct(
|
||||
private string $appSecret,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{CORS_ALLOW_ORIGIN: string, SESSION_LIFETIME: int}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
namespace App\Service\Gateway;
|
||||
|
||||
use App\Controller\GatewaysController;
|
||||
use App\Entity\AccountingTransaction;
|
||||
use App\Entity\GatewayCharge;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\GatewayCheckoutStatus;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Gateway\CheckoutStatus;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class GatewayCheckoutService
|
||||
class CheckoutService
|
||||
{
|
||||
public const RESPONSE_TYPE_SUCCESS = 'success';
|
||||
public const RESPONSE_TYPE_FAILURE = 'failure';
|
||||
|
||||
public function __construct(
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates a suitable value for redirection params to external gateways.
|
||||
@@ -25,7 +24,7 @@ class GatewayCheckoutService
|
||||
* @return string Absolute URL to the gateways redirection handler
|
||||
*/
|
||||
public function generateRedirectUrl(
|
||||
GatewayCheckout $checkout,
|
||||
Checkout $checkout,
|
||||
string $type = self::RESPONSE_TYPE_SUCCESS,
|
||||
array $parameters = [],
|
||||
): string {
|
||||
@@ -46,7 +45,7 @@ class GatewayCheckoutService
|
||||
*
|
||||
* @return string A tracking code suitable for external gateway data matching
|
||||
*/
|
||||
public function generateTracking(GatewayCheckout $checkout, GatewayCharge $charge): string
|
||||
public function generateTracking(Checkout $checkout, Charge $charge): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s-%s',
|
||||
@@ -60,7 +59,7 @@ class GatewayCheckoutService
|
||||
*
|
||||
* @return string A tracking code suitable for external gateway data matching
|
||||
*/
|
||||
public function generateCheckoutTracking(GatewayCheckout $checkout): string
|
||||
public function generateCheckoutTracking(Checkout $checkout): string
|
||||
{
|
||||
return \sprintf(
|
||||
'AO%d-CO%d',
|
||||
@@ -74,7 +73,7 @@ class GatewayCheckoutService
|
||||
*
|
||||
* @return string A tracking code suitable for external gateway data matching
|
||||
*/
|
||||
public function generateChargeTracking(GatewayCharge $charge): string
|
||||
public function generateChargeTracking(Charge $charge): string
|
||||
{
|
||||
return \sprintf(
|
||||
'CH%d-AT%d',
|
||||
@@ -84,23 +83,23 @@ class GatewayCheckoutService
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a successful GatewayCheckout and generates the transactions for each charge.
|
||||
* Updates a successful Checkout and generates the transactions for each charge.
|
||||
*
|
||||
* @param GatewayCheckout $checkout The GatewayCheckout to be updated
|
||||
* @param Checkout $checkout The Checkout to be updated
|
||||
*
|
||||
* @return GatewayCheckout The updated GatewayCheckout with updated charges and generated transactions
|
||||
* @return Checkout The updated Checkout with updated charges and generated transactions
|
||||
*/
|
||||
public function chargeCheckout(GatewayCheckout $checkout): GatewayCheckout
|
||||
public function chargeCheckout(Checkout $checkout): Checkout
|
||||
{
|
||||
$checkout->setStatus(GatewayCheckoutStatus::Charged);
|
||||
$checkout->setStatus(CheckoutStatus::Charged);
|
||||
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
$transaction = new AccountingTransaction();
|
||||
$transaction = new Transaction();
|
||||
$transaction->setMoney($charge->getMoney());
|
||||
$transaction->setOrigin($checkout->getOrigin());
|
||||
$transaction->setTarget($charge->getTarget());
|
||||
|
||||
$charge->setTransaction($transaction);
|
||||
$charge->addTransaction($transaction);
|
||||
}
|
||||
|
||||
return $checkout;
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\State\Accounting;
|
||||
|
||||
use ApiPlatform\Doctrine\Common\State\PersistProcessor;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\ApiResource\Accounting as ApiResource;
|
||||
use App\Entity\Accounting as Entity;
|
||||
use App\Mapping\Accounting\AccountingMapper;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class AccountingStateProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AccountingMapper $accountingMapper,
|
||||
#[Autowire(service: PersistProcessor::class)]
|
||||
private ProcessorInterface $persistProcessor,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param ApiResource\Accounting $data
|
||||
*
|
||||
* @return ApiResource\Accounting
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
|
||||
{
|
||||
/** @var Entity\Accounting */
|
||||
$entity = $this->accountingMapper->toEntity($data);
|
||||
|
||||
$this->persistProcessor->process($entity, $operation, $uriVariables, $context);
|
||||
|
||||
return $this->accountingMapper->toResource($entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\State\Accounting;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
|
||||
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
|
||||
use ApiPlatform\Metadata\CollectionOperationInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\Pagination\TraversablePaginator;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\Accounting as ApiResource;
|
||||
use App\Entity\Accounting as Entity;
|
||||
use App\Mapping\Accounting\AccountingMapper;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class AccountingStateProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: ItemProvider::class)]
|
||||
private ProviderInterface $itemProvider,
|
||||
#[Autowire(service: CollectionProvider::class)]
|
||||
private ProviderInterface $collectionProvider,
|
||||
private AccountingMapper $accountingMapper,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
if ($operation instanceof CollectionOperationInterface) {
|
||||
$accountings = $this->collectionProvider->provide($operation, $uriVariables, $context);
|
||||
|
||||
$resources = [];
|
||||
foreach ($accountings as $accounting) {
|
||||
$resources[] = $this->toResource($accounting);
|
||||
}
|
||||
|
||||
return new TraversablePaginator(
|
||||
new \ArrayIterator($resources),
|
||||
$accountings->getCurrentPage(),
|
||||
$accountings->getItemsPerPage(),
|
||||
$accountings->getTotalItems()
|
||||
);
|
||||
}
|
||||
|
||||
$accounting = $this->itemProvider->provide($operation, $uriVariables, $context);
|
||||
|
||||
return $this->toResource($accounting);
|
||||
}
|
||||
|
||||
private function toResource(?Entity\Accounting $accounting): ?ApiResource\Accounting
|
||||
{
|
||||
if ($accounting === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->accountingMapper->toResource($accounting);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\State\Gateway;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Mapping\Gateway\ChargeMapper;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class ChargeStateProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: ItemProvider::class)]
|
||||
private ProviderInterface $itemProvider,
|
||||
private ChargeMapper $chargeMapper,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
$charge = $this->itemProvider->provide($operation, $uriVariables, $context);
|
||||
|
||||
if ($charge === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->chargeMapper->toResource($charge);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\State\Gateway;
|
||||
|
||||
use ApiPlatform\Doctrine\Common\State\PersistProcessor;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\ApiResource\Gateway as Resource;
|
||||
use App\Gateway\GatewayLocator;
|
||||
use App\Mapping\Gateway\CheckoutMapper;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class CheckoutStateProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CheckoutMapper $checkoutMapper,
|
||||
#[Autowire(service: PersistProcessor::class)]
|
||||
private ProcessorInterface $innerProcessor,
|
||||
private GatewayLocator $gatewayLocator,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Resource\Checkout $data
|
||||
*
|
||||
* @return Resource\Checkout
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
|
||||
{
|
||||
$entity = $this->checkoutMapper->toEntity($data);
|
||||
$entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context);
|
||||
|
||||
$entity = $this->gatewayLocator->getGateway($data->gateway->name)->process($entity);
|
||||
$entity = $this->innerProcessor->process($entity, $operation, $uriVariables, $context);
|
||||
|
||||
return $this->checkoutMapper->toResource($entity);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\State;
|
||||
namespace App\State\Gateway;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\Gateway;
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use App\ApiResource\Gateway\Gateway;
|
||||
use App\Gateway\GatewayLocator;
|
||||
use App\Mapping\Gateway\GatewayMapper;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class GatewayStateProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GatewayLocator $gateways,
|
||||
) {
|
||||
}
|
||||
|
||||
private function getGateways(): array
|
||||
{
|
||||
$gateways = [];
|
||||
foreach ($this->gateways->getGateways() as $gateway) {
|
||||
$gateways[] = new Gateway($gateway);
|
||||
}
|
||||
|
||||
return $gateways;
|
||||
}
|
||||
|
||||
private function getGateway(string $name): Gateway
|
||||
{
|
||||
try {
|
||||
$gateway = $this->gateways->getGateway($name);
|
||||
|
||||
return new Gateway($gateway);
|
||||
} catch (\Exception $e) {
|
||||
throw new NotFoundHttpException('Not Found');
|
||||
}
|
||||
}
|
||||
private GatewayMapper $gatewayMapper,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
@@ -46,4 +26,25 @@ class GatewayStateProvider implements ProviderInterface
|
||||
return $this->getGateway($uriVariables['name']);
|
||||
}
|
||||
}
|
||||
|
||||
private function getGateways(): array
|
||||
{
|
||||
$gateways = [];
|
||||
foreach ($this->gateways->getGateways() as $gateway) {
|
||||
$gateways[] = $this->gatewayMapper->toResource($gateway);
|
||||
}
|
||||
|
||||
return $gateways;
|
||||
}
|
||||
|
||||
private function getGateway(string $name): Gateway
|
||||
{
|
||||
try {
|
||||
$gateway = $this->gateways->getGateway($name);
|
||||
|
||||
return $this->gatewayMapper->toResource($gateway);
|
||||
} catch (\Exception $e) {
|
||||
throw new NotFoundHttpException('Not Found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class GatewayCheckoutProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||
private ProcessorInterface $innerProcessor,
|
||||
private GatewayLocator $gatewayLocator,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GatewayCheckout $data
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
|
||||
{
|
||||
$checkout = $this->innerProcessor->process($data, $operation, $uriVariables, $context);
|
||||
|
||||
$gateway = $this->gatewayLocator->getGatewayOf($checkout);
|
||||
$checkout = $gateway->process($checkout);
|
||||
|
||||
return $this->innerProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,7 @@ class UserStateProcessor implements ProcessorInterface
|
||||
public function __construct(
|
||||
private ProcessorInterface $innerProcessor,
|
||||
private UserPasswordHasherInterface $userPasswordHasher,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return T2
|
||||
|
||||
@@ -21,8 +21,7 @@ class UserTokenLoginProcessor implements ProcessorInterface
|
||||
private UserRepository $userRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UserPasswordHasherInterface $userPasswordHasher,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param UserTokenLoginDto $data
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Validator;
|
||||
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use Symfony\Component\Validator\Constraints\Choice;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class GatewayName extends Choice
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
options: GatewayLocator::getNamesStatic()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Validator;
|
||||
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Choice;
|
||||
use Symfony\Component\Validator\Constraints\ChoiceValidator;
|
||||
|
||||
class GatewayNameValidator extends ChoiceValidator
|
||||
{
|
||||
public function __construct(
|
||||
private GatewayLocator $gateways,
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
/* @var App\Validator\GatewayName $constraint */
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->gateways->getGateway($value)) {
|
||||
return;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ value }}', $this->formatValue($value))
|
||||
->setParameter('{{ choices }}', $this->formatValues($constraint->choices))
|
||||
->setCode(Choice::NO_SUCH_CHOICE_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Validator;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Validates that a GatewayCheckout has GatewayCharges with charge types supported by the given Gateway.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class SupportedChargeTypes extends Constraint
|
||||
{
|
||||
public string $message = 'The charge type "{{ value }}" is not supported by the "{{ gateway }}" Gateway.';
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Validator;
|
||||
|
||||
use App\Gateway\GatewayLocator;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
class SupportedChargeTypesValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(
|
||||
private GatewayLocator $gatewayLocator,
|
||||
) {}
|
||||
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof Collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Collection<int, \App\Entity\Gateway\Charge> */
|
||||
$charges = $value;
|
||||
|
||||
$checkout = $charges->toArray()[0]->getCheckout();
|
||||
$gateway = $this->gatewayLocator->getGatewayOf($checkout);
|
||||
|
||||
foreach ($charges as $charge) {
|
||||
if (!\in_array(
|
||||
$charge->getType(),
|
||||
$gateway->getSupportedChargeTypes()
|
||||
)) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ value }}', $charge->getType()->value)
|
||||
->setParameter('{{ gateway }}', $gateway->getName())
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
namespace App\Tests\Entity\Accounting;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Tipjar;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
@@ -27,7 +27,7 @@ class AccountingTest extends KernelTestCase
|
||||
$tipjarA->setName('TEST_TIPJAR_A');
|
||||
|
||||
$accountingA = new Accounting();
|
||||
$accountingA->setTipjar($tipjarA);
|
||||
$accountingA->setOwner($tipjarA);
|
||||
|
||||
$this->entityManager->persist($tipjarA);
|
||||
$this->entityManager->flush();
|
||||
@@ -38,14 +38,14 @@ class AccountingTest extends KernelTestCase
|
||||
->getAccounting();
|
||||
|
||||
$this->assertNotNull($accountingA->getId());
|
||||
$this->assertEquals(Tipjar::class, $accountingA->getOwnerClass());
|
||||
$this->assertEquals('TEST_TIPJAR_A', $accountingA->getTipjar()->getName());
|
||||
$this->assertEquals($tipjarA->getId(), $accountingA->getOwner()->getId());
|
||||
$this->assertEquals(Tipjar::class, $accountingA->getOwner()::class);
|
||||
|
||||
$tipjarB = new Tipjar();
|
||||
$tipjarB->setName('TEST_TIPJAR_B');
|
||||
|
||||
$accountingB = new Accounting();
|
||||
$accountingB->setTipjar($tipjarB);
|
||||
$accountingB->setOwner($tipjarB);
|
||||
|
||||
$this->entityManager->persist($tipjarB);
|
||||
$this->entityManager->flush();
|
||||
@@ -56,7 +56,7 @@ class AccountingTest extends KernelTestCase
|
||||
->getAccounting();
|
||||
|
||||
$this->assertNotNull($accountingB->getId());
|
||||
$this->assertEquals(Tipjar::class, $accountingB->getOwnerClass());
|
||||
$this->assertEquals('TEST_TIPJAR_B', $accountingB->getTipjar()->getName());
|
||||
$this->assertEquals($tipjarB->getId(), $accountingB->getOwner()->getId());
|
||||
$this->assertEquals(Tipjar::class, $accountingB->getOwner()::class);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,21 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Library\Economy\Payment;
|
||||
namespace App\Tests\Gateway\Paypal;
|
||||
|
||||
use App\Library\Economy\Payment\PaypalGatewayService;
|
||||
use App\Gateway\Paypal\PaypalService;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
class PaypalGatewayServiceTest extends KernelTestCase
|
||||
class PaypalServiceTest extends KernelTestCase
|
||||
{
|
||||
private PaypalGatewayService $paypalService;
|
||||
private PaypalService $paypalService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->paypalService = static::getContainer()->get(PaypalGatewayService::class);
|
||||
$this->paypalService = static::getContainer()->get(PaypalService::class);
|
||||
}
|
||||
|
||||
public function testAuthenticates(): void
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Gateway\Wallet;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Entity\User;
|
||||
use App\Gateway\Wallet\StatementDirection;
|
||||
use App\Gateway\Wallet\WalletService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
class WalletServiceTest extends KernelTestCase
|
||||
{
|
||||
use ResetDatabase;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
private WalletService $walletService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$this->walletService = static::getContainer()->get(WalletService::class);
|
||||
}
|
||||
|
||||
private function getUser(): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('wallettestuser');
|
||||
$user->setEmail('testuser@wallet.com');
|
||||
$user->setPassword('wallettestpassword');
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function getTipjar(): Tipjar
|
||||
{
|
||||
$tipjar = new Tipjar();
|
||||
$tipjar->setName('WALLET_TEST_TIPJAR');
|
||||
|
||||
$this->entityManager->persist($tipjar);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $tipjar;
|
||||
}
|
||||
|
||||
public function testTransactionsAddFunds()
|
||||
{
|
||||
$tipjar = $this->getTipjar()->getAccounting();
|
||||
$user = $this->getUser()->getAccounting();
|
||||
|
||||
$statements = $this->walletService->getStatements($user);
|
||||
|
||||
$this->assertCount(0, $statements);
|
||||
|
||||
$balance = $this->walletService->getBalance($user);
|
||||
|
||||
$this->assertEquals(0, $balance->amount);
|
||||
$this->assertEquals($user->getCurrency(), $balance->currency);
|
||||
|
||||
$incoming = new Transaction();
|
||||
$incoming->setMoney(new Money(100, 'EUR'));
|
||||
$incoming->setOrigin($tipjar);
|
||||
$incoming->setTarget($user);
|
||||
|
||||
/*
|
||||
* $this->walletService->save($incoming);
|
||||
* called automatically on Transaction persist.
|
||||
*
|
||||
* @see \App\EventListener\WalletTransactionsListener
|
||||
*/
|
||||
$this->entityManager->persist($incoming);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$balance = $this->walletService->getBalance($user);
|
||||
|
||||
$this->assertEquals(100, $balance->amount);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
|
||||
$statements = $this->walletService->getStatements($user);
|
||||
|
||||
$this->assertCount(1, $statements);
|
||||
|
||||
$statement = $statements[0];
|
||||
$this->assertEquals(100, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(0, $statement->getFinancesTo());
|
||||
$this->assertCount(0, $statement->getFinancedBy());
|
||||
}
|
||||
|
||||
public function testTransactionsGetFinanced()
|
||||
{
|
||||
$tipjar = $this->getTipjar()->getAccounting();
|
||||
$user = $this->getUser()->getAccounting();
|
||||
|
||||
$incoming = new Transaction();
|
||||
$incoming->setMoney(new Money(100, 'EUR'));
|
||||
$incoming->setOrigin($tipjar);
|
||||
$incoming->setTarget($user);
|
||||
|
||||
$this->entityManager->persist($incoming);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$outgoing = new Transaction();
|
||||
$outgoing->setMoney(new Money(20, 'EUR'));
|
||||
$outgoing->setOrigin($user);
|
||||
$outgoing->setTarget($tipjar);
|
||||
|
||||
$this->walletService->spend($outgoing);
|
||||
|
||||
$balance = $this->walletService->getBalance($user);
|
||||
|
||||
$this->assertEquals(80, $balance->amount);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
|
||||
$statements = $this->walletService->getStatements($user);
|
||||
|
||||
$this->assertCount(2, $statements);
|
||||
|
||||
$statement = $statements[0];
|
||||
$this->assertEquals(80, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(1, $statement->getFinancesTo());
|
||||
$this->assertCount(0, $statement->getFinancedBy());
|
||||
|
||||
$statement = $statements[1];
|
||||
$this->assertEquals(20, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(0, $statement->getFinancesTo());
|
||||
$this->assertCount(1, $statement->getFinancedBy());
|
||||
|
||||
$outgoing = new Transaction();
|
||||
$outgoing->setMoney(new Money(30, 'EUR'));
|
||||
$outgoing->setOrigin($user);
|
||||
$outgoing->setTarget($tipjar);
|
||||
|
||||
$this->walletService->spend($outgoing);
|
||||
|
||||
$balance = $this->walletService->getBalance($user);
|
||||
|
||||
$this->assertEquals(50, $balance->amount);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
|
||||
$statements = $this->walletService->getStatements($user);
|
||||
|
||||
$this->assertCount(3, $statements);
|
||||
|
||||
$statement = $statements[0];
|
||||
$this->assertEquals(50, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(2, $statement->getFinancesTo());
|
||||
$this->assertCount(0, $statement->getFinancedBy());
|
||||
|
||||
$statement = $statements[1];
|
||||
$this->assertEquals(20, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(0, $statement->getFinancesTo());
|
||||
$this->assertCount(1, $statement->getFinancedBy());
|
||||
|
||||
$financement = $statements[0]->getFinancesTo()[0];
|
||||
$this->assertEquals(20, $financement->getMoney()->amount);
|
||||
$this->assertEquals('EUR', $financement->getMoney()->currency);
|
||||
$this->assertSame($financement, $statement->getFinancedBy()[0]);
|
||||
$this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId());
|
||||
|
||||
$statement = $statements[2];
|
||||
$this->assertEquals(30, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(0, $statement->getFinancesTo());
|
||||
$this->assertCount(1, $statement->getFinancedBy());
|
||||
|
||||
$financement = $statements[0]->getFinancesTo()[1];
|
||||
$this->assertEquals(30, $financement->getMoney()->amount);
|
||||
$this->assertEquals('EUR', $financement->getMoney()->currency);
|
||||
$this->assertSame($financement, $statement->getFinancedBy()[0]);
|
||||
$this->assertEquals($financement->getId(), $statement->getFinancedBy()[0]->getId());
|
||||
}
|
||||
|
||||
public function testBalanceIsFIFO()
|
||||
{
|
||||
$tipjar = $this->getTipjar()->getAccounting();
|
||||
$user = $this->getUser()->getAccounting();
|
||||
|
||||
$incoming = new Transaction();
|
||||
$incoming->setMoney(new Money(10, 'EUR'));
|
||||
$incoming->setOrigin($tipjar);
|
||||
$incoming->setTarget($user);
|
||||
|
||||
$this->entityManager->persist($incoming);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$incoming = new Transaction();
|
||||
$incoming->setMoney(new Money(11, 'EUR'));
|
||||
$incoming->setOrigin($tipjar);
|
||||
$incoming->setTarget($user);
|
||||
|
||||
$this->entityManager->persist($incoming);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$outgoing = new Transaction();
|
||||
$outgoing->setMoney(new Money(12, 'EUR'));
|
||||
$outgoing->setOrigin($user);
|
||||
$outgoing->setTarget($tipjar);
|
||||
|
||||
$this->walletService->spend($outgoing);
|
||||
|
||||
$incoming = new Transaction();
|
||||
$incoming->setMoney(new Money(13, 'EUR'));
|
||||
$incoming->setOrigin($tipjar);
|
||||
$incoming->setTarget($user);
|
||||
|
||||
$this->entityManager->persist($incoming);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$balance = $this->walletService->getBalance($user);
|
||||
|
||||
$this->assertEquals(22, $balance->amount);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
|
||||
$statements = $this->walletService->getStatements($user);
|
||||
|
||||
$this->assertCount(4, $statements);
|
||||
|
||||
$statement = $statements[0];
|
||||
$this->assertEquals(0, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(1, $statement->getFinancesTo());
|
||||
$this->assertCount(0, $statement->getFinancedBy());
|
||||
|
||||
$statement = $statements[1];
|
||||
$this->assertEquals(9, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(1, $statement->getFinancesTo());
|
||||
$this->assertCount(0, $statement->getFinancedBy());
|
||||
|
||||
$statement = $statements[2];
|
||||
$this->assertEquals(12, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Outgoing->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(0, $statement->getFinancesTo());
|
||||
$this->assertCount(2, $statement->getFinancedBy());
|
||||
|
||||
$statement = $statements[3];
|
||||
$this->assertEquals(13, $statement->getBalance()->amount);
|
||||
$this->assertEquals(StatementDirection::Incoming->value, $statement->getDirection()->value);
|
||||
$this->assertEquals('EUR', $balance->currency);
|
||||
$this->assertCount(0, $statement->getFinancesTo());
|
||||
$this->assertCount(0, $statement->getFinancedBy());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Library\Economy;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\Library\Economy\MoneyService;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* WARNING: This test compares currencies, which change over time
|
||||
* and might cause the test to fail without justifiable cause in the code.
|
||||
*
|
||||
* Test currencies and values have been selected for their stability to minimize chances of external factors such as
|
||||
* * Hyper inflation/deflation
|
||||
* * Currency intervention
|
||||
* * Demonetization
|
||||
*/
|
||||
class MoneyServiceTest extends KernelTestCase
|
||||
{
|
||||
private MoneyService $moneyService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->moneyService = static::getContainer()->get(MoneyService::class);
|
||||
}
|
||||
|
||||
public function testComparesLess()
|
||||
{
|
||||
$a = new Money(300, 'EUR');
|
||||
$b = new Money(200, 'EUR');
|
||||
|
||||
$this->assertTrue($this->moneyService->isLess($b, $a));
|
||||
$this->assertFalse($this->moneyService->isLess($a, $b));
|
||||
|
||||
$c = new Money(100, 'JPY');
|
||||
|
||||
// We are cooked if this fails and I'm not talking about code
|
||||
$this->assertTrue($this->moneyService->isLess($c, $a));
|
||||
}
|
||||
|
||||
public function testComparesMore()
|
||||
{
|
||||
$a = new Money(100, 'GBP');
|
||||
$b = new Money(100, 'GBP');
|
||||
|
||||
$this->assertTrue($this->moneyService->isMoreOrSame($a, $b));
|
||||
$this->assertTrue($this->moneyService->isMoreOrSame($b, $a));
|
||||
|
||||
$c = new Money(300, 'GBP');
|
||||
|
||||
$this->assertTrue($this->moneyService->isMoreOrSame($c, $b));
|
||||
$this->assertFalse($this->moneyService->isMoreOrSame($b, $c));
|
||||
|
||||
$d = new Money(100, 'MXN');
|
||||
|
||||
$this->assertFalse($this->moneyService->isMoreOrSame($d, $c));
|
||||
$this->assertFalse($this->moneyService->isMoreOrSame($d, $a));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user