+5
-7
@@ -1,14 +1,12 @@
|
||||
/public/bundles/
|
||||
/var/
|
||||
var/
|
||||
vendor/
|
||||
|
||||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
phpunit.xml
|
||||
|
||||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
.php-cs-fixer.php
|
||||
.php-cs-fixer.cache
|
||||
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
.git/
|
||||
.github/
|
||||
.vscode/
|
||||
|
||||
@@ -35,7 +35,6 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> doctrineencryptbundle/doctrine-encrypt-bundle ###
|
||||
DOCTRINE_ENCRYPT_KEY=""
|
||||
CRYPTO_SECRET=""
|
||||
###< doctrineencryptbundle/doctrine-encrypt-bundle ###
|
||||
|
||||
@@ -52,6 +51,10 @@ PAYPAL_CLIENT_SECRET=""
|
||||
PAYPAL_WEBHOOK_ID="WEBHOOK_ID"
|
||||
###< App\Gateway\Paypal\PaypalGateway ###
|
||||
|
||||
###> App\Money\MoneyService ###
|
||||
CURRENCY_DEFAULT="EUR"
|
||||
###< App\Money\MoneyService ###
|
||||
|
||||
###> symfony/lock ###
|
||||
# Choose one of the stores below
|
||||
# postgresql+advisory://db_user:db_password@localhost/db_name
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: docker/php/Dockerfile
|
||||
file: docker/dev/php/Dockerfile
|
||||
target: prod
|
||||
tags: ${{ secrets.REGISTRY_URL }}/goteo-v4:latest
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"dockerfilePath": "./docker/main/Dockerfile"
|
||||
}
|
||||
+5
-5
@@ -17,7 +17,7 @@
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||
"doctrine/orm": "^3.3",
|
||||
"doctrineencryptbundle/doctrine-encrypt-bundle": "^5.4",
|
||||
"goteo/benzina": "^0.4",
|
||||
"goteo/benzina": "^0.5",
|
||||
"jolicode/automapper": "^9.2",
|
||||
"mpratt/embera": "~2.0",
|
||||
"nelmio/cors-bundle": "^2.3",
|
||||
@@ -36,6 +36,7 @@
|
||||
"symfony/framework-bundle": "6.4.*",
|
||||
"symfony/http-client": "6.4.*",
|
||||
"symfony/intl": "6.4.*",
|
||||
"symfony/maker-bundle": "^1.62",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/property-access": "6.4.*",
|
||||
"symfony/property-info": "6.4.*",
|
||||
@@ -44,7 +45,8 @@
|
||||
"symfony/serializer": "6.4.*",
|
||||
"symfony/twig-bundle": "6.4.*",
|
||||
"symfony/validator": "6.4.*",
|
||||
"symfony/yaml": "6.4.*"
|
||||
"symfony/yaml": "6.4.*",
|
||||
"zenstruck/foundry": "^2.4"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
@@ -99,8 +101,6 @@
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
"symfony/maker-bundle": "^1.62",
|
||||
"symfony/phpunit-bridge": "^7.2",
|
||||
"zenstruck/foundry": "^2.4"
|
||||
"symfony/phpunit-bridge": "^7.2"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+929
-836
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ api_platform:
|
||||
standard_put: true
|
||||
pagination_enabled: true
|
||||
pagination_items_per_page: 30
|
||||
pagination_client_enabled: true
|
||||
pagination_maximum_items_per_page: 100
|
||||
pagination_client_items_per_page: true
|
||||
use_symfony_listeners: true
|
||||
swagger:
|
||||
|
||||
@@ -4,6 +4,7 @@ nelmio_cors:
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
allow_credentials: true
|
||||
expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
paths:
|
||||
|
||||
@@ -25,6 +25,8 @@ security:
|
||||
|
||||
access_token:
|
||||
token_handler: App\Security\AccessTokenHandler
|
||||
custom_authenticators:
|
||||
- App\Security\CookieAuthenticator
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
when@dev: &dev
|
||||
# See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
|
||||
zenstruck_foundry:
|
||||
persistence:
|
||||
flush_once: true
|
||||
|
||||
when@test: *dev
|
||||
|
||||
+30
-4
@@ -14,8 +14,14 @@ services:
|
||||
_instanceof:
|
||||
App\Gateway\GatewayInterface:
|
||||
tags: ['app.gateway.gateway']
|
||||
App\Library\Economy\Currency\ExchangeInterface:
|
||||
tags: ['app.lib.economy.currency.exchange']
|
||||
App\Money\Conversion\ExchangeInterface:
|
||||
tags: ['app.money.conversion.exchange']
|
||||
App\Money\Totalization\TotalizerInterface:
|
||||
tags: ['app.money.totalization.totalizer']
|
||||
App\Matchfunding\Formula\FormulaInterface:
|
||||
tags: ['app.matchfunding.formula']
|
||||
App\Matchfunding\Rule\RuleInterface:
|
||||
tags: ['app.matchfunding.rule']
|
||||
App\Service\Embed\Filter\EmbedFilterInterface:
|
||||
tags: ['app.embed.embed_filter']
|
||||
App\Service\Embed\Provider\EmbedProviderFactoryInterface:
|
||||
@@ -63,6 +69,14 @@ services:
|
||||
tags:
|
||||
- { name: api_platform.uri_variables.transformer }
|
||||
|
||||
App\Matchfunding\Formula\FormulaLocator:
|
||||
arguments:
|
||||
- !tagged 'app.matchfunding.formula'
|
||||
|
||||
App\Matchfunding\Rule\RuleLocator:
|
||||
arguments:
|
||||
- !tagged 'app.matchfunding.rule'
|
||||
|
||||
App\Service\Auth\AuthService:
|
||||
arguments:
|
||||
- '%env(APP_SECRET)%'
|
||||
@@ -82,9 +96,21 @@ services:
|
||||
$hosts:
|
||||
- 'peertube.plataformess.org'
|
||||
|
||||
App\Library\Economy\Currency\ExchangeLocator:
|
||||
App\State\MoneyTotalStateProvider:
|
||||
arguments:
|
||||
- !tagged 'app.lib.economy.currency.exchange'
|
||||
- !tagged 'api_platform.doctrine.orm.query_extension.collection'
|
||||
|
||||
App\Money\MoneyService:
|
||||
arguments:
|
||||
- '%env(CURRENCY_DEFAULT)%'
|
||||
|
||||
App\Money\Conversion\ExchangeLocator:
|
||||
arguments:
|
||||
- !tagged 'app.money.conversion.exchange'
|
||||
|
||||
App\Money\Totalization\TotalizerLocator:
|
||||
arguments:
|
||||
- !tagged 'app.money.totalization.totalizer'
|
||||
|
||||
App\Mapping\AutoMapper:
|
||||
arguments:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
php:
|
||||
volumes:
|
||||
# ./docker/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
||||
- ./docker/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro
|
||||
- ./docker/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro
|
||||
# ./docker/dev/php/ssl/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
||||
- ./docker/dev/php/conf.d/opcache.dev.ini:/usr/local/etc/php/conf.d/opcache.ini:ro
|
||||
- ./docker/dev/php/conf.d/xdebug.dev.ini:/usr/local/etc/php/conf.d/xdebug.ini:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
|
||||
+4
-4
@@ -2,20 +2,20 @@ services:
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/php/Dockerfile
|
||||
dockerfile: docker/dev/php/Dockerfile
|
||||
target: dev
|
||||
container_name: goteo-api-php
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./docker/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro
|
||||
- ./docker/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro
|
||||
- ./docker/dev/php/conf.d/openssl.ini:/usr/local/etc/php/conf.d/openssl.ini:ro
|
||||
- ./docker/dev/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: goteo-api-nginx
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./docker/dev/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- "${APP_HTTP_PORT:-8090}:80"
|
||||
- "${APP_HTTPS_PORT:-8091}:443"
|
||||
|
||||
@@ -47,7 +47,7 @@ FROM base AS prod
|
||||
COPY . /app
|
||||
RUN chown -R www-data:www-data /app
|
||||
|
||||
COPY /docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY /docker/dev/php/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
USER www-data
|
||||
@@ -0,0 +1,70 @@
|
||||
FROM php:8.3-fpm-alpine AS system-setup
|
||||
|
||||
RUN apk --no-cache add \
|
||||
curl \
|
||||
git \
|
||||
linux-headers \
|
||||
icu-dev \
|
||||
zip \
|
||||
libzip-dev \
|
||||
unzip \
|
||||
nginx \
|
||||
supervisor \
|
||||
$PHPIZE_DEPS && \
|
||||
pecl install apcu
|
||||
|
||||
RUN curl --remote-name https://curl.se/ca/cacert.pem \
|
||||
&& mv cacert.pem /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
RUN docker-php-ext-install \
|
||||
pdo \
|
||||
pdo_mysql \
|
||||
intl \
|
||||
zip \
|
||||
opcache && \
|
||||
docker-php-ext-enable apcu
|
||||
|
||||
COPY --from=composer:2.8.9 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
COPY ./docker/main/default.conf /etc/nginx/http.d/default.conf
|
||||
COPY ./docker/main/supervisord.conf /etc/supervisord.conf
|
||||
COPY ./docker/main/www.conf /usr/local/etc/php-fpm.d/www.conf
|
||||
COPY ./docker/main/local.ini /usr/local/etc/php/conf.d/local.ini
|
||||
|
||||
# FPM configuration via ENV
|
||||
ENV FPM_PM=dynamic
|
||||
ENV FPM_PM_MAX_CHILDREN=5
|
||||
ENV FPM_PM_START_SERVERS=2
|
||||
ENV FPM_PM_MIN_SPARE_SERVERS=1
|
||||
ENV FPM_PM_MAX_SPARE_SERVERS=3
|
||||
|
||||
# PHP configuration via ENV
|
||||
ENV LOG_TO_STDOUT=1
|
||||
ENV PHP_MEMORY_LIMIT=512M
|
||||
ENV PHP_MAX_EXECUTION_TIME=30
|
||||
ENV PHP_UPLOAD_MAX_FILESIZE=120M
|
||||
ENV PHP_POST_MAX_SIZE=128M
|
||||
ENV PHP_SESSION_SAVE_HANDLER=files
|
||||
ENV PHP_SESSION_SAVE_PATH=
|
||||
ENV PHP_DISPLAY_ERRORS=0
|
||||
ENV PHP_DISPLAY_STARTUP_ERRORS=0
|
||||
|
||||
FROM system-setup AS app-setup
|
||||
|
||||
COPY . /app
|
||||
RUN chown -R nginx:nginx /app
|
||||
|
||||
COPY ./docker/main/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
USER nginx
|
||||
WORKDIR /app
|
||||
|
||||
RUN composer install --prefer-dist --no-scripts --no-dev --optimize-autoloader
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
|
||||
CMD curl -s -o /dev/null -f http://localhost/v4/index.json || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
@@ -0,0 +1,33 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /app/public;
|
||||
|
||||
index index.php;
|
||||
|
||||
error_log /dev/stdout;
|
||||
access_log /dev/stdout;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
fastcgi_pass localhost:9000;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_ROOT $realpath_root;
|
||||
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 4 256k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
|
||||
internal;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# Check if migrations need to be applied
|
||||
if ! php bin/console doctrine:migrations:up-to-date --quiet; then
|
||||
echo "Applying migrations..."
|
||||
php bin/console doctrine:migrations:migrate --no-interaction
|
||||
fi
|
||||
|
||||
# Check if FORCE_SCHEMA_UPDATE is set to true
|
||||
if [ "$FORCE_SCHEMA_UPDATE" = "true" ]; then
|
||||
echo "Forcing schema update..."
|
||||
php bin/console doctrine:schema:update --force
|
||||
fi
|
||||
|
||||
php bin/console cache:warmup
|
||||
php bin/console assets:install
|
||||
|
||||
echo "Start Supervisord"
|
||||
|
||||
supervisord -c /etc/supervisord.conf
|
||||
@@ -0,0 +1,9 @@
|
||||
memory_limit = ${PHP_MEMORY_LIMIT}
|
||||
max_execution_time = ${PHP_MAX_EXECUTION_TIME}
|
||||
upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}
|
||||
post_max_size = ${PHP_POST_MAX_SIZE}
|
||||
allow_url_fopen = 1
|
||||
session.save_handler = ${PHP_SESSION_SAVE_HANDLER}
|
||||
session.save_path = ${PHP_SESSION_SAVE_PATH}
|
||||
display_errors = ${PHP_DISPLAY_ERRORS}
|
||||
display_startup_errors = ${PHP_DISPLAY_STARTUP_ERRORS}
|
||||
@@ -0,0 +1,26 @@
|
||||
[supervisord]
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
logfile_backups=0
|
||||
loglevel=info
|
||||
nodaemon=true
|
||||
|
||||
[program:php-fpm]
|
||||
command=php-fpm --nodaemonize
|
||||
autorestart=true
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile_maxbytes=0
|
||||
stdout_logfile=/dev/stdout
|
||||
stderr_logfile=/dev/stderr
|
||||
|
||||
[program:nginx]
|
||||
command=nginx -g 'daemon off;'
|
||||
autorestart=true
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile_maxbytes=0
|
||||
stdout_logfile=/dev/stdout
|
||||
stderr_logfile=/dev/stderr
|
||||
@@ -0,0 +1,18 @@
|
||||
[global]
|
||||
daemonize = no
|
||||
|
||||
[www]
|
||||
user = nginx
|
||||
group = nginx
|
||||
listen = 9000
|
||||
listen.owner = nginx
|
||||
listen.group = nginx
|
||||
listen.mode = 0660
|
||||
|
||||
pm = ${FPM_PM}
|
||||
pm.max_children = ${FPM_PM_MAX_CHILDREN}
|
||||
pm.start_servers = ${FPM_PM_START_SERVERS}
|
||||
pm.min_spare_servers = ${FPM_PM_MIN_SPARE_SERVERS}
|
||||
pm.max_spare_servers = ${FPM_PM_MAX_SPARE_SERVERS}
|
||||
|
||||
catch_workers_output = yes
|
||||
@@ -4,18 +4,17 @@ namespace App\ApiResource\Accounting;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Matchfunding\MatchCallApiResource;
|
||||
use App\ApiResource\Project\ProjectApiResource;
|
||||
use App\ApiResource\TipjarApiResource;
|
||||
use App\ApiResource\User\UserApiResource;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\Matchfunding\MatchCall;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Entity\User\User;
|
||||
use App\Mapping\Transformer\AccountingBalanceMapTransformer;
|
||||
use App\State\Accounting\AccountingStateProcessor;
|
||||
use App\State\Accounting\AccountingStateProvider;
|
||||
use AutoMapper\Attribute\MapFrom;
|
||||
|
||||
/**
|
||||
* v4 features an advanced economy model under the hood.
|
||||
@@ -44,16 +43,21 @@ class AccountingApiResource
|
||||
*/
|
||||
public string $currency;
|
||||
|
||||
/**
|
||||
* The money currently held by the Accounting.
|
||||
*/
|
||||
#[MapFrom(Accounting::class, transformer: AccountingBalanceMapTransformer::class)]
|
||||
#[API\ApiProperty(writable: false, security: 'is_granted("ACCOUNTING_VIEW", object)')]
|
||||
public Money $balance;
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public string $ownerClass;
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public ?UserApiResource $user = null;
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public ?ProjectApiResource $project = null;
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public ?TipjarApiResource $tipjar = null;
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public ?MatchCallApiResource $matchCall;
|
||||
|
||||
/**
|
||||
* The resource owning this Accounting.
|
||||
*
|
||||
@@ -68,17 +72,10 @@ class AccountingApiResource
|
||||
return $this->project;
|
||||
case Tipjar::class:
|
||||
return $this->tipjar;
|
||||
case MatchCall::class:
|
||||
return $this->matchCall;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public ?UserApiResource $user = null;
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public ?ProjectApiResource $project = null;
|
||||
|
||||
#[API\ApiProperty(readable: false, writable: false)]
|
||||
public ?TipjarApiResource $tipjar = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Accounting;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\State\Accounting\AccountingBalanceStateProvider;
|
||||
|
||||
/**
|
||||
* The AccountingBalance represents how much Money is currently held under one Accounting.\
|
||||
* \
|
||||
* This is calculated from incoming vs outgoing Transactions in the Accounting's history.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'AccountingBalance',
|
||||
)]
|
||||
#[API\Get(
|
||||
provider: AccountingBalanceStateProvider::class,
|
||||
uriTemplate: '/accountings/{id}/balance',
|
||||
uriVariables: [
|
||||
'id' => new API\Link(
|
||||
fromClass: AccountingApiResource::class,
|
||||
fromProperty: 'id',
|
||||
description: 'Accounting identifier'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class AccountingBalance
|
||||
{
|
||||
#[API\ApiProperty(identifier: true)]
|
||||
public AccountingApiResource $accounting;
|
||||
|
||||
/**
|
||||
* The money currently held by the Accounting.
|
||||
*/
|
||||
#[API\ApiProperty(security: 'is_granted("ACCOUNTING_VIEW", object.accounting)')]
|
||||
public ApiMoney $balance;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace App\ApiResource\Accounting;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use ApiPlatform\Metadata\QueryParameter;
|
||||
use App\Entity\Money;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\State\Accounting\AccountingBalancePointStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -57,7 +57,7 @@ class AccountingBalancePoint
|
||||
/**
|
||||
* Resulting balance for items in this point.
|
||||
*/
|
||||
public Money $balance;
|
||||
public ApiMoney $balance;
|
||||
|
||||
/**
|
||||
* The number of items aggregated in this point.
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace App\ApiResource\Accounting;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Money;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ class TransactionApiResource
|
||||
/**
|
||||
* The monetary value received at target and issued at origin.
|
||||
*/
|
||||
public Money $money;
|
||||
public ApiMoney $money;
|
||||
|
||||
/**
|
||||
* The Accounting from which the Transaction comes from.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Money\Conversion\Conversion;
|
||||
use App\Money\MoneyInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class ApiMoney implements MoneyInterface
|
||||
{
|
||||
/**
|
||||
* An amount of currency.\
|
||||
* Expressed as the minor unit, e.g: cents, pennies, etc.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Positive()]
|
||||
public int $amount;
|
||||
|
||||
/**
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Currency()]
|
||||
public string $currency;
|
||||
|
||||
/**
|
||||
* Conversion metadata.
|
||||
*/
|
||||
#[API\ApiProperty(readable: true, writable: false)]
|
||||
public ?array $conversion = null;
|
||||
|
||||
public function __construct(
|
||||
int $amount,
|
||||
string $currency,
|
||||
) {
|
||||
$this->amount = $amount;
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
public static function of(MoneyInterface $moneyInterface): self
|
||||
{
|
||||
$money = new self(
|
||||
$moneyInterface->getAmount(),
|
||||
$moneyInterface->getCurrency(),
|
||||
);
|
||||
|
||||
$money->conversion = $moneyInterface->getConversion()?->toArray();
|
||||
|
||||
return $money;
|
||||
}
|
||||
|
||||
public function getAmount(): int
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
public function getCurrency(): string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function getConversion(): ?Conversion
|
||||
{
|
||||
return $this->conversion
|
||||
? Conversion::fromArray($this->conversion)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@ use ApiPlatform\Doctrine\Orm\Filter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\AccountingApiResource;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\Dto\Gateway\ChargeUpdationDto;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Money;
|
||||
use App\Filter\GatewayFilter;
|
||||
use App\Gateway\ChargeStatus;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
@@ -38,6 +39,8 @@ class ChargeApiResource
|
||||
* The Checkout to which this Charge item belongs to.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false, security: 'is_granted("IS_AUTHENTICATED_FULLY")')]
|
||||
#[API\ApiFilter(GatewayFilter::class, properties: ['checkout.gateway'])]
|
||||
#[API\ApiFilter(Filter\SearchFilter::class, properties: ['checkout.trackings.value'])]
|
||||
public CheckoutApiResource $checkout;
|
||||
|
||||
/**
|
||||
@@ -76,17 +79,22 @@ class ChargeApiResource
|
||||
* It is money before fees and taxes, not accountable.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiFilter(Filter\SearchFilter::class, properties: ['money.amount' => 'exact'])]
|
||||
#[API\ApiFilter(Filter\RangeFilter::class, properties: ['money.amount'])]
|
||||
public Money $money;
|
||||
#[API\ApiFilter(Filter\SearchFilter::class, properties: ['money.currency' => 'exact'])]
|
||||
public ApiMoney $money;
|
||||
|
||||
/**
|
||||
* The status of the charge item with the Gateway.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(Filter\SearchFilter::class, strategy: 'exact')]
|
||||
public ChargeStatus $status = ChargeStatus::InPending;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(Filter\DateFilter::class)]
|
||||
public \DateTimeInterface $dateCreated;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(Filter\DateFilter::class)]
|
||||
public \DateTimeInterface $dateUpdated;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\ApiResource\Gateway;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\AccountingApiResource;
|
||||
use App\Dto\CheckoutUpdationDto;
|
||||
use App\Dto\Gateway\CheckoutUpdationDto;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
@@ -60,7 +60,7 @@ class CheckoutApiResource
|
||||
*
|
||||
* @var ChargeApiResource[]
|
||||
*/
|
||||
#[API\ApiProperty(readableLink: true, writableLink: true)]
|
||||
#[API\ApiProperty(readableLink: true)]
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Count(min: 1)]
|
||||
public array $charges = [];
|
||||
@@ -106,20 +106,16 @@ class CheckoutApiResource
|
||||
* @var Tracking[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[MapTo(Checkout::class, transformer: 'parseTrackings')]
|
||||
#[MapFrom(Checkout::class, transformer: 'parseTrackings')]
|
||||
public array $trackings = [];
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public \DateTimeInterface $dateCreated;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public \DateTimeInterface $dateUpdated;
|
||||
|
||||
public static function parseLinks(array $values)
|
||||
{
|
||||
return \array_map(fn($value) => Link::tryFrom($value), $values);
|
||||
}
|
||||
|
||||
public static function parseTrackings(array $values)
|
||||
{
|
||||
return \array_map(fn($value) => Tracking::tryFrom($value), $values);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Matchfunding;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Matchfunding\Formula\FormulaInterface;
|
||||
use App\State\Matchfunding\FormulaStateProvider;
|
||||
|
||||
/**
|
||||
* A MatchFormula is a predefined code implementation for matching funds in Transactions under a MatchCall.
|
||||
* MatchFormulas can be chosen by the managers in a MatchCall and their behaviour fine-tuned.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'MatchFormula',
|
||||
provider: FormulaStateProvider::class
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Get()]
|
||||
class FormulaApiResource
|
||||
{
|
||||
#[API\ApiProperty(identifier: true, writable: false)]
|
||||
public string $name;
|
||||
|
||||
/**
|
||||
* The underlying math expressed as a common-notation formula.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public string $expression;
|
||||
|
||||
public static function from(FormulaInterface $interface): FormulaApiResource
|
||||
{
|
||||
$resource = new FormulaApiResource();
|
||||
$resource->name = $interface::getName();
|
||||
$resource->expression = $interface::getAsExpression();
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Matchfunding;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\AccountingApiResource;
|
||||
use App\ApiResource\User\UserApiResource;
|
||||
use App\Entity\Matchfunding\MatchCall;
|
||||
use App\Entity\Matchfunding\MatchCallStatus;
|
||||
use App\Entity\Territory;
|
||||
use App\State\ApiResourceStateProcessor;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A MatchCall is a managed event which accepts MatchCallSubmissions from Projects to receive *matchfunding* financement.
|
||||
* This means money going to a Project in a MatchCall can be matched with funds from the MatchCall accounting.
|
||||
* \
|
||||
* \
|
||||
* MatchCallSubmissions from Projects can be accepted or rejected by the managers.
|
||||
* \
|
||||
* \
|
||||
* How and when does a MatchCall match funds is determined by the MatchStrategy.
|
||||
* Everytime there is a Charge item going to a Project accepted in a MatchCall the strategy is evaluated for matching.
|
||||
* The strategy defines a series of rules that determine if the Charge is eligible for triggering the matching of funds.
|
||||
* When a Charge is to be matched the strategy uses a MatchFormula,
|
||||
* which is a predefined implementation for common mathematical operations used to match money.
|
||||
* The formula is fine-tuned in the strategy by defining the variables in the formula.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'MatchCall',
|
||||
stateOptions: new Options(entityClass: MatchCall::class),
|
||||
provider: ApiResourceStateProvider::class,
|
||||
processor: ApiResourceStateProcessor::class
|
||||
)]
|
||||
class MatchCallApiResource
|
||||
{
|
||||
#[API\ApiProperty(identifier: true, writable: false)]
|
||||
public int $id;
|
||||
|
||||
/**
|
||||
* The Accounting which holds and spends the funds for this MatchCall.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public AccountingApiResource $accounting;
|
||||
|
||||
/**
|
||||
* The MatchStrategies define the matching behaviour for this MatchCall.
|
||||
*
|
||||
* @var MatchStrategyApiResource[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $strategies;
|
||||
|
||||
/**
|
||||
* A list of the MatchCallSubmissions received by this MatchCall.
|
||||
*
|
||||
* @var MatchCallSubmissionApiResource[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $submissions;
|
||||
|
||||
/**
|
||||
* A list of Users who can modify this MatchCall.
|
||||
*
|
||||
* @var UserApiResource[]
|
||||
*/
|
||||
public array $managers;
|
||||
|
||||
/**
|
||||
* Main display title.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public string $title;
|
||||
|
||||
/**
|
||||
* Long-form secondary display text.
|
||||
*/
|
||||
public string $description;
|
||||
|
||||
/**
|
||||
* Codes for the territory of interest in this MatchCall.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Valid]
|
||||
public Territory $territory;
|
||||
|
||||
/**
|
||||
* The current status of the MatchCall.
|
||||
*/
|
||||
public MatchCallStatus $status = MatchCallStatus::InEditing;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Matchfunding;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Project\ProjectApiResource;
|
||||
use App\Dto\Matchfunding\MatchCallSubmissionCreationDto;
|
||||
use App\Entity\Matchfunding\MatchCallSubmission;
|
||||
use App\Entity\Matchfunding\MatchCallSubmissionStatus;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\Matchfunding\MatchCallSubmissionStateProcessor;
|
||||
|
||||
/**
|
||||
* MatchCallSubmissions represent the will of a Project to be held under a MatchCall and receive matchfunding financement.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'MatchCallSubmission',
|
||||
stateOptions: new Options(entityClass: MatchCallSubmission::class),
|
||||
provider: ApiResourceStateProvider::class,
|
||||
processor: MatchCallSubmissionStateProcessor::class
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post(
|
||||
input: MatchCallSubmissionCreationDto::class,
|
||||
securityPostDenormalize: 'is_granted("PROJECT_EDIT", object.project)',
|
||||
securityPostDenormalizeMessage: 'You do not have permission to submit that Project to a MatchCall'
|
||||
)]
|
||||
#[API\Get()]
|
||||
#[API\Patch(
|
||||
security: 'is_granted("MATCHCALL_EDIT", object.call)',
|
||||
securityMessage: 'You do not have permission to edit this MatchCallSubmission'
|
||||
)]
|
||||
class MatchCallSubmissionApiResource
|
||||
{
|
||||
#[API\ApiProperty(identifier: true, writable: false)]
|
||||
public int $id;
|
||||
|
||||
/**
|
||||
* The MatchCall to which this MatchCallSubmission belongs to.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public MatchCallApiResource $call;
|
||||
|
||||
/**
|
||||
* The Project that applied for the MatchCall.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public ProjectApiResource $project;
|
||||
|
||||
/**
|
||||
* The status of the Project's application for the MatchCall.\
|
||||
* Only MatchCallSubmissions with an status `accepted` will receive matchfunding.
|
||||
*/
|
||||
public MatchCallSubmissionStatus $status = MatchCallSubmissionStatus::DEFAULT;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Matchfunding;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\Entity\Matchfunding\MatchAgainst;
|
||||
use App\Entity\Matchfunding\MatchStrategy;
|
||||
use App\Mapping\Transformer\MatchFormulaMapTransformer;
|
||||
use App\Mapping\Transformer\MatchRulesMapTransformer;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\Matchfunding\MatchStrategyStateProcessor;
|
||||
use AutoMapper\Attribute\MapFrom;
|
||||
use AutoMapper\Attribute\MapTo;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A MatchStrategy defines how will a MatchCall perform the match-making process.
|
||||
* \
|
||||
* The match-making flow is:
|
||||
* 1. A Project accepted in a MatchCall receives a successful Charge.
|
||||
* 2. The match-making loads the MatchCall's MatchStrategies.
|
||||
* 3. For each MatchStrategy the rules are executed, if one rule fails the match-making is cancelled for the strategy.
|
||||
* 3. The first valid MatchStrategy's formula function is passed the respective limit, factor and money of the MatchAgainst.
|
||||
* 4. The result of the MatchStrategy formula function execution is put in a Transaction from the MatchCall to the Project.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'MatchStrategy',
|
||||
stateOptions: new Options(entityClass: MatchStrategy::class),
|
||||
provider: ApiResourceStateProvider::class,
|
||||
processor: MatchStrategyStateProcessor::class,
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post()]
|
||||
#[API\Get()]
|
||||
#[API\Patch(security: 'is_granted("MATCHCALL_EDIT", object.call)')]
|
||||
#[API\Delete(security: 'is_granted("MATCHCALL_EDIT", object.call)')]
|
||||
class MatchStrategyApiResource
|
||||
{
|
||||
#[API\ApiProperty(identifier: true, writable: false)]
|
||||
public int $id;
|
||||
|
||||
/**
|
||||
* The MatchCall to which this strategy belongs to.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
|
||||
public MatchCallApiResource $call;
|
||||
|
||||
/**
|
||||
* The ranking is the index order of this strategy among the others of the MatchCall
|
||||
* when this is up for match making. The ranking value of items will be sorted
|
||||
* after each change in the number of strategies or the ranking values.
|
||||
*/
|
||||
#[Assert\PositiveOrZero()]
|
||||
#[API\ApiFilter(OrderFilter::class)]
|
||||
public int $ranking = 0;
|
||||
|
||||
/**
|
||||
* The MatchRules used to decide if the match making strategy should be executed or not.
|
||||
*
|
||||
* @var RuleApiResource[]
|
||||
*/
|
||||
#[MapTo(MatchStrategy::class, property: 'ruleNames', transformer: [self::class, 'rulesToNames'])]
|
||||
#[MapFrom(MatchStrategy::class, property: 'ruleNames', transformer: MatchRulesMapTransformer::class)]
|
||||
public array $rules;
|
||||
|
||||
/**
|
||||
* The MatchFormula used to calculate matched funds.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[MapTo(MatchStrategy::class, property: 'formulaName', transformer: 'source.formula.name')]
|
||||
#[MapFrom(MatchStrategy::class, property: 'formulaName', transformer: MatchFormulaMapTransformer::class)]
|
||||
public FormulaApiResource $formula;
|
||||
|
||||
/**
|
||||
* The assigned maximum amount of funding that will be given by the MatchFormula per operation.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Valid()]
|
||||
public ApiMoney $limit;
|
||||
|
||||
/**
|
||||
* The `x` factor used to calculate the resulting match of funds with the MatchFormula.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Positive()]
|
||||
public float $factor;
|
||||
|
||||
/**
|
||||
* The money to be matched by the formula is
|
||||
* - `charge` the money in the Charge item
|
||||
* - `budget_min` the minimum in the Project's budget
|
||||
* - `budget_opt` the optimum in the Project's budget.
|
||||
*/
|
||||
public MatchAgainst $against = MatchAgainst::DEFAULT;
|
||||
|
||||
/**
|
||||
* @param array<int, RuleApiResource>
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function rulesToNames(array $values)
|
||||
{
|
||||
return \array_map(fn($r) => $r->name, $values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Matchfunding;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Matchfunding\Rule\RuleInterface;
|
||||
use App\State\Matchfunding\RuleStateProvider;
|
||||
|
||||
/**
|
||||
* A MatchRule is a predefined code implementation for validating
|
||||
* that a match making condition is met when there is a Charge to be matched.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'MatchRule',
|
||||
provider: RuleStateProvider::class
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Get()]
|
||||
class RuleApiResource
|
||||
{
|
||||
#[API\ApiProperty(identifier: true, writable: false)]
|
||||
public string $name;
|
||||
|
||||
/**
|
||||
* A plain-text description about what the rules validates for.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public string $description;
|
||||
|
||||
public static function from(RuleInterface $interface): RuleApiResource
|
||||
{
|
||||
$resource = new RuleApiResource();
|
||||
$resource->name = \array_slice(\explode('\\', $interface::class), -1, 1)[0];
|
||||
$resource->description = $interface::getDescription();
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class Money
|
||||
{
|
||||
/**
|
||||
* An amount of currency.\
|
||||
* Expressed as the minor unit, e.g: cents, pennies, etc.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Positive()]
|
||||
public int $amount;
|
||||
|
||||
/**
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Currency()]
|
||||
public string $currency;
|
||||
}
|
||||
@@ -5,8 +5,8 @@ namespace App\ApiResource\Project;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\ApiResource\LocalizedApiResourceTrait;
|
||||
use App\ApiResource\Money;
|
||||
use App\Entity\Project\BudgetItem;
|
||||
use App\Entity\Project\BudgetItemType;
|
||||
use App\Entity\Project\ProjectDeadline;
|
||||
@@ -58,7 +58,7 @@ class BudgetItemApiResource
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Valid()]
|
||||
public Money $money;
|
||||
public ApiMoney $money;
|
||||
|
||||
/**
|
||||
* Defines the budget category for this item within the project.
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
namespace App\ApiResource\Project;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\ApiResource\ApiMoney;
|
||||
|
||||
class BudgetSummary
|
||||
{
|
||||
/**
|
||||
* The total money by the included items.
|
||||
*/
|
||||
public Money $money;
|
||||
public ApiMoney $money;
|
||||
|
||||
/**
|
||||
* The total money of type 'task'.
|
||||
*/
|
||||
public Money $task;
|
||||
public ApiMoney $task;
|
||||
|
||||
/**
|
||||
* The total money of type 'material'.
|
||||
*/
|
||||
public Money $material;
|
||||
public ApiMoney $material;
|
||||
|
||||
/**
|
||||
* The total money of type 'infrastructure'.
|
||||
*/
|
||||
public Money $infra;
|
||||
public ApiMoney $infra;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Entity\Project\ProjectCategory;
|
||||
use App\Entity\Project\ProjectDeadline;
|
||||
use App\Entity\Project\ProjectStatus;
|
||||
use App\Entity\Project\ProjectVideo;
|
||||
use App\Entity\Territory;
|
||||
use App\Mapping\Transformer\BudgetMapTransformer;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\Project\ProjectStateProcessor;
|
||||
@@ -118,7 +119,7 @@ class ProjectApiResource
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Valid()]
|
||||
public ProjectTerritoryApiResource $territory;
|
||||
public Territory $territory;
|
||||
|
||||
/**
|
||||
* Free-form rich text description for the Project.
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Project;
|
||||
|
||||
use App\Validator\CountrySubdivision;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* ISO 3166 data about the Project's territory of interest.
|
||||
*/
|
||||
class ProjectTerritoryApiResource
|
||||
{
|
||||
/**
|
||||
* ISO 3166-1 alpha-2 two-letter country code.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Country(alpha3: false)]
|
||||
public string $country;
|
||||
|
||||
/**
|
||||
* ISO 3166-2 first level subdivision code.\
|
||||
* e.g: ES-AN (Andalucía, Spain).
|
||||
*/
|
||||
#[CountrySubdivision()]
|
||||
public string $subLvl1;
|
||||
|
||||
/**
|
||||
* ISO 3166-2 second level subdivision code.\
|
||||
* e.g: ES-GR (Granada, Andalucía, Spain).
|
||||
*/
|
||||
#[CountrySubdivision()]
|
||||
public string $subLvl2;
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\ApiResource\Project;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\ApiResource\LocalizedApiResourceTrait;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\Project\Reward;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\Project\RewardStateProcessor;
|
||||
@@ -53,28 +55,35 @@ class RewardApiResource
|
||||
* The minimal monetary sum to be able to claim this reward.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Money $money;
|
||||
#[API\ApiFilter(OrderFilter::class, properties: ['money.amount'])]
|
||||
public ApiMoney $money;
|
||||
|
||||
/**
|
||||
* Rewards might be finite, i.e: has a limited amount of existing unitsTotal.
|
||||
*/
|
||||
#[Assert\NotNull()]
|
||||
#[Assert\Type('bool')]
|
||||
public bool $hasUnits;
|
||||
#[API\ApiFilter(BooleanFilter::class)]
|
||||
public bool $isFinite = false;
|
||||
|
||||
/**
|
||||
* For finite rewards, the total amount of existing unitsTotal.\
|
||||
* Required if `hasUnits`.
|
||||
* For finite rewards, the total amount of existing units.\
|
||||
* Required if `isFinite`.
|
||||
*/
|
||||
#[Assert\When(
|
||||
'this.hasUnits == true',
|
||||
constraints: [new Assert\Positive()]
|
||||
)]
|
||||
public int $unitsTotal = 0;
|
||||
#[Assert\When('this.isFinite == true', [new Assert\Positive()])]
|
||||
public ?int $unitsTotal = null;
|
||||
|
||||
/**
|
||||
* For finite rewards, the currently available amount of unitsTotal that can be claimed.
|
||||
* The total amount of claims on this Reward.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(OrderFilter::class)]
|
||||
public int $unitsClaimed = 0;
|
||||
|
||||
/**
|
||||
* For finite rewards, the currently available amount of units that can be claimed.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(OrderFilter::class)]
|
||||
public int $unitsAvailable = 0;
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\ApiResource\Project;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Gateway\ChargeApiResource;
|
||||
use App\ApiResource\User\UserApiResource;
|
||||
use App\Dto\RewardClaimCreationDto;
|
||||
use App\Entity\Project\RewardClaim;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\Project\RewardClaimStateProcessor;
|
||||
use App\Validator\AvailableRewardUnits;
|
||||
use App\Validator\EnoughRewardCharge;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
@@ -22,7 +23,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
provider: ApiResourceStateProvider::class,
|
||||
processor: RewardClaimStateProcessor::class
|
||||
)]
|
||||
#[EnoughRewardCharge()]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post(input: RewardClaimCreationDto::class)]
|
||||
#[API\Get()]
|
||||
#[API\Delete()]
|
||||
class RewardClaimApiResource
|
||||
{
|
||||
#[API\ApiProperty(identifier: true, writable: false)]
|
||||
@@ -32,12 +36,14 @@ class RewardClaimApiResource
|
||||
* The User claiming the ProjectReward.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
|
||||
public UserApiResource $owner;
|
||||
|
||||
/**
|
||||
* The GatewayCharge granting access to the ProjectReward.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
|
||||
public ChargeApiResource $charge;
|
||||
|
||||
/**
|
||||
@@ -45,5 +51,6 @@ class RewardClaimApiResource
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[AvailableRewardUnits()]
|
||||
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
|
||||
public RewardApiResource $reward;
|
||||
}
|
||||
|
||||
@@ -2,25 +2,54 @@
|
||||
|
||||
namespace App\ApiResource\Project;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Gateway\ChargeApiResource;
|
||||
use App\ApiResource\User\UserApiResource;
|
||||
use App\Entity\Money;
|
||||
use App\ApiResource\Accounting\AccountingApiResource;
|
||||
use App\ApiResource\Accounting\TransactionApiResource;
|
||||
use App\ApiResource\ApiMoney;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Mapping\Transformer\SupportMoneyMapTransformer;
|
||||
use App\Money\Totalization\TotalizedMoney;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\MoneyTotalStateProvider;
|
||||
use App\State\Project\SupportStateProcessor;
|
||||
use AutoMapper\Attribute\MapFrom;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* ProjectSupports gather Transactions going from one same origin to one same Project.\
|
||||
* \
|
||||
* MatchCalls specially might make several different Transactions to the same Project,
|
||||
* but their ProjectSupport remains the same just with updated money.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'ProjectSupport',
|
||||
stateOptions: new Options(entityClass: Support::class),
|
||||
provider: ApiResourceStateProvider::class,
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\GetCollection(
|
||||
uriTemplate: '/project_supports/money_total',
|
||||
provider: MoneyTotalStateProvider::class,
|
||||
output: TotalizedMoney::class,
|
||||
paginationEnabled: false,
|
||||
openapi: new \ApiPlatform\OpenApi\Model\Operation(
|
||||
summary: 'Get money total',
|
||||
description: 'Returns a single TotalizedMoney object',
|
||||
responses: [
|
||||
'200' => [
|
||||
'description' => 'Totalized money',
|
||||
'content' => [
|
||||
'application/json' => [
|
||||
'schema' => [
|
||||
'$ref' => '#/components/schemas/ProjectSupport.TotalizedMoney',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
)
|
||||
)]
|
||||
#[API\Get()]
|
||||
#[API\Patch(
|
||||
security: 'is_granted("SUPPORT_EDIT", previous_object)',
|
||||
@@ -32,40 +61,40 @@ class SupportApiResource
|
||||
public int $id;
|
||||
|
||||
/**
|
||||
* The User who created the ProjectSupport record.\
|
||||
* \
|
||||
* When `anonymous` is *true* it will only be public to admins and the User.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false, security: 'is_granted("SUPPORT_VIEW", object)')]
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
|
||||
public ?UserApiResource $owner;
|
||||
|
||||
/**
|
||||
* The Project being targeted in the Charges.
|
||||
* The Project being supported.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
|
||||
public ProjectApiResource $project;
|
||||
|
||||
/**
|
||||
* The Charges that were paid by the User.
|
||||
*
|
||||
* @var array<int, ChargeApiResource>
|
||||
* The Accounting of origin for the Transactions under this ProjectSupport record.\
|
||||
* \
|
||||
* When `anonymous` is *true* it will only be public to admins and the User.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $charges;
|
||||
#[API\ApiProperty(writable: false, security: 'is_granted("SUPPORT_VIEW", object)')]
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
|
||||
public ?AccountingApiResource $origin;
|
||||
|
||||
/**
|
||||
* The total monetary value of the Charges paid by the User.
|
||||
* The Transactions that were issued to the Project by the origin.
|
||||
*
|
||||
* @var array<int, TransactionApiResource>
|
||||
*/
|
||||
#[MapFrom(Support::class, transformer: SupportMoneyMapTransformer::class)]
|
||||
public Money $money;
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $transactions;
|
||||
|
||||
/**
|
||||
* The total monetary value of the Transactions going to the Project.
|
||||
*/
|
||||
public ApiMoney $money;
|
||||
|
||||
/**
|
||||
* User's will to have their support to the Project be shown publicly.
|
||||
*/
|
||||
#[Assert\NotNull()]
|
||||
#[Assert\Type('bool')]
|
||||
#[API\ApiFilter(BooleanFilter::class)]
|
||||
public bool $anonymous = true;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\AccountingApiResource;
|
||||
@@ -38,5 +39,6 @@ class TipjarApiResource
|
||||
* Human readable, non white space, unique string.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiFilter(Filter\SearchFilter::class, strategy: 'partial')]
|
||||
public string $name;
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Entity\User\User;
|
||||
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\Repository\Project\ProjectRepository;
|
||||
use App\Repository\TipjarRepository;
|
||||
use App\Repository\User\UserRepository;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Goteo\Benzina\Pump\ArrayPumpTrait;
|
||||
use Goteo\Benzina\Pump\DoctrinePumpTrait;
|
||||
use Goteo\Benzina\Pump\PumpInterface;
|
||||
|
||||
class CheckoutsPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
use CheckoutsPumpTrait;
|
||||
|
||||
public const TRACKING_TITLE_V3 = 'v3 Invest ID';
|
||||
public const TRACKING_TITLE_PAYMENT = 'v3 Invest Payment';
|
||||
public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction';
|
||||
public const TRACKING_TITLE_PREAPPROVAL = 'v3 Invest Preapproval';
|
||||
|
||||
public const CHARGE_TITLE_PROJECT = 'Pago en Goteo v3 - Donación a proyecto';
|
||||
public const CHARGE_TITLE_POOL = 'Pago en Goteo v3 - Carga de monedero';
|
||||
public const CHARGE_TITLE_TIP = 'Pago en Goteo v3 - Propina a la plataforma';
|
||||
|
||||
private const PLATFORM_TIPJAR_NAME = 'platform';
|
||||
|
||||
private const MAX_INT = 2147483647;
|
||||
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private ProjectRepository $projectRepository,
|
||||
private TipjarRepository $tipjarRepository,
|
||||
private CheckoutService $checkoutService,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $sample): bool
|
||||
{
|
||||
if ($this->hasAllKeys($sample, self::INVEST_KEYS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function pump(mixed $record, array $context): void
|
||||
{
|
||||
if (!$record['user'] || empty($record['user'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$record['amount'] || $record['amount'] < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$record['method'] || empty($record['method'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->getUser($record);
|
||||
if ($user === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$project = $this->getProject($record);
|
||||
$tipjar = $this->getPlatformTipjar();
|
||||
|
||||
$checkout = new Checkout();
|
||||
$checkout->setOrigin($user->getAccounting());
|
||||
$checkout->setStatus($this->getCheckoutStatus($record));
|
||||
$checkout->setGatewayName($this->getCheckoutGateway($record));
|
||||
|
||||
foreach ($this->getCheckoutTrackings($record) as $tracking) {
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$checkout->setMigrated(true);
|
||||
$checkout->setMigratedId($record['id']);
|
||||
|
||||
$checkout->setDateCreated(new \DateTime($record['invested']));
|
||||
$checkout->setDateUpdated(new \DateTime());
|
||||
|
||||
$charge = new Charge();
|
||||
$charge->setType($this->getChargeType($record));
|
||||
$charge->setMoney($this->getChargeMoney($record['amount'], $record['currency']));
|
||||
|
||||
if ($project === null) {
|
||||
$charge->setTitle(self::CHARGE_TITLE_POOL);
|
||||
$charge->setTarget($user->getAccounting());
|
||||
} else {
|
||||
$charge->setTitle(self::CHARGE_TITLE_PROJECT);
|
||||
$charge->setTarget($project->getAccounting());
|
||||
}
|
||||
|
||||
$checkout->addCharge($charge);
|
||||
|
||||
if ($record['donate_amount'] > 0) {
|
||||
$tip = new Charge();
|
||||
$tip->setType(ChargeType::Single);
|
||||
$tip->setTitle(self::CHARGE_TITLE_TIP);
|
||||
$tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency']));
|
||||
$tip->setTarget($tipjar->getAccounting());
|
||||
|
||||
$checkout->addCharge($tip);
|
||||
}
|
||||
|
||||
if ($checkout->getStatus() === CheckoutStatus::Charged) {
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
}
|
||||
|
||||
$this->persist($checkout, $context);
|
||||
}
|
||||
|
||||
private function getUser(array $record): ?User
|
||||
{
|
||||
return $this->userRepository->findOneBy(['migratedId' => $record['user']]);
|
||||
}
|
||||
|
||||
private function getProject(array $record): ?Project
|
||||
{
|
||||
if (empty($record['project'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->projectRepository->findOneBy(['migratedId' => $record['project']]);
|
||||
}
|
||||
|
||||
private function getPlatformTipjar(): Tipjar
|
||||
{
|
||||
$tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]);
|
||||
|
||||
if ($tipjar) {
|
||||
return $tipjar;
|
||||
}
|
||||
|
||||
$tipjar = new Tipjar();
|
||||
$tipjar->setName(self::PLATFORM_TIPJAR_NAME);
|
||||
|
||||
$this->entityManager->persist($tipjar);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $tipjar;
|
||||
}
|
||||
|
||||
private function getChargeType(array $record): ChargeType
|
||||
{
|
||||
if (\in_array($record['method'], ['stripe_subscription'])) {
|
||||
return ChargeType::Recurring;
|
||||
}
|
||||
|
||||
return ChargeType::Single;
|
||||
}
|
||||
|
||||
private function getChargeMoney(int $amount, string $currency): Money
|
||||
{
|
||||
$amount = $amount * 100;
|
||||
|
||||
if ($amount >= self::MAX_INT) {
|
||||
$amount = self::MAX_INT;
|
||||
}
|
||||
|
||||
return new Money($amount, $currency);
|
||||
}
|
||||
|
||||
private function getCheckoutStatus(array $record): CheckoutStatus
|
||||
{
|
||||
if ($record['status'] < 1) {
|
||||
return CheckoutStatus::InPending;
|
||||
}
|
||||
|
||||
return CheckoutStatus::Charged;
|
||||
}
|
||||
|
||||
private function getCheckoutGateway(array $record): string
|
||||
{
|
||||
switch ($record['method']) {
|
||||
case 'stripe_subscription':
|
||||
return StripeGateway::getName();
|
||||
case 'pool':
|
||||
return WalletGateway::getName();
|
||||
case 'paypal':
|
||||
return PaypalGateway::getName();
|
||||
case 'tpv':
|
||||
return CecaGateway::getName();
|
||||
case 'cash':
|
||||
return CashGateway::getName();
|
||||
case 'drop':
|
||||
return DropGateway::getName();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tracking[]
|
||||
*/
|
||||
private function getCheckoutTrackings(array $record): array
|
||||
{
|
||||
$v3Tracking = new Tracking();
|
||||
$v3Tracking->title = self::TRACKING_TITLE_V3;
|
||||
$v3Tracking->value = $record['id'];
|
||||
|
||||
$trackings = [$v3Tracking];
|
||||
|
||||
if (!empty($record['payment'])) {
|
||||
$payment = new Tracking();
|
||||
$payment->title = self::TRACKING_TITLE_PAYMENT;
|
||||
$payment->value = $record['payment'];
|
||||
|
||||
$trackings[] = $payment;
|
||||
}
|
||||
|
||||
if (!empty($record['transaction'])) {
|
||||
$transaction = new Tracking();
|
||||
$transaction->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$transaction->value = $record['transaction'];
|
||||
|
||||
$trackings[] = $transaction;
|
||||
}
|
||||
|
||||
if (!empty($record['preapproval'])) {
|
||||
$preapproval = new Tracking();
|
||||
$preapproval->title = self::TRACKING_TITLE_PREAPPROVAL;
|
||||
$preapproval->value = $record['preapproval'];
|
||||
|
||||
$trackings[] = $preapproval;
|
||||
}
|
||||
|
||||
return $trackings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Project\Reward;
|
||||
use App\Entity\Project\RewardClaim;
|
||||
use App\Entity\User\User;
|
||||
use App\Gateway\ChargeStatus;
|
||||
use App\Money\MoneyService;
|
||||
use App\Repository\Gateway\ChargeRepository;
|
||||
use App\Repository\Project\RewardRepository;
|
||||
use App\Repository\User\UserRepository;
|
||||
use Goteo\Benzina\Pump\ArrayPumpTrait;
|
||||
use Goteo\Benzina\Pump\DoctrinePumpTrait;
|
||||
use Goteo\Benzina\Pump\PumpInterface;
|
||||
|
||||
class InvestRewardsPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use DatabasePumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
use InvestsPumpTrait;
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $userCache = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $chargeCache = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $rewardCache = [];
|
||||
|
||||
public function __construct(
|
||||
private RewardRepository $rewardRepository,
|
||||
private UserRepository $userRepository,
|
||||
private ChargeRepository $chargeRepository,
|
||||
private MoneyService $moneyService,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $sample): bool
|
||||
{
|
||||
if ($this->hasAllKeys($sample, ['invest', 'reward', 'fulfilled'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function pump(mixed $record, array $context): void
|
||||
{
|
||||
$reward = $this->getReward($record);
|
||||
if ($reward === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$invest = $this->getInvest($record, $context);
|
||||
if ($invest === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$charge = $this->getCharge($invest);
|
||||
if ($charge === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->getUser($invest);
|
||||
if ($user === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($charge->getStatus() !== ChargeStatus::Charged) {
|
||||
return;
|
||||
}
|
||||
|
||||
$claim = new RewardClaim();
|
||||
$claim->setOwner($user);
|
||||
$claim->setCharge($charge);
|
||||
|
||||
$reward->addClaim($claim);
|
||||
$claim->setReward($reward);
|
||||
|
||||
$this->persist($claim, $context);
|
||||
}
|
||||
|
||||
private function getInvest(array $record, array $context): ?array
|
||||
{
|
||||
$query = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `invest` i WHERE i.id = :invest'
|
||||
);
|
||||
|
||||
$query->execute(['invest' => $record['invest']]);
|
||||
|
||||
$result = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getReward(array $record): ?Reward
|
||||
{
|
||||
$id = $record['reward'];
|
||||
|
||||
if (isset($this->rewardCache[$id])) {
|
||||
return $this->rewardRepository->find($this->rewardCache[$id]);
|
||||
}
|
||||
|
||||
$reward = $this->rewardRepository->findOneBy(['migratedId' => $id]);
|
||||
if (!$reward) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->rewardCache[$id] = $reward->getId();
|
||||
|
||||
return $reward;
|
||||
}
|
||||
|
||||
private function getCharge(array $invest): ?Charge
|
||||
{
|
||||
$id = $invest['id'];
|
||||
|
||||
if (isset($this->chargeCache[$id])) {
|
||||
return $this->chargeRepository->find($this->chargeCache[$id]);
|
||||
}
|
||||
|
||||
$charge = $this->chargeRepository->findOneBy(['migratedId' => $id]);
|
||||
if (!$charge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->chargeCache[$id] = $charge->getId();
|
||||
|
||||
return $charge;
|
||||
}
|
||||
|
||||
private function getUser(array $invest): ?User
|
||||
{
|
||||
$id = $invest['user'];
|
||||
|
||||
if (isset($this->userCache[$id])) {
|
||||
return $this->userRepository->find($this->userCache[$id]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->findOneBy(['migratedId' => $id]);
|
||||
|
||||
$this->userCache[$id] = $user->getId();
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
<?php
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Gateway\Tracking;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\Reward;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Entity\User\User;
|
||||
use App\Gateway\ChargeStatus;
|
||||
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\Wallet\WalletGateway;
|
||||
use App\Money\Money;
|
||||
use App\Money\MoneyService;
|
||||
use App\Repository\Project\ProjectRepository;
|
||||
use App\Repository\Project\RewardRepository;
|
||||
use App\Repository\Project\SupportRepository;
|
||||
use App\Repository\TipjarRepository;
|
||||
use App\Repository\User\UserRepository;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Goteo\Benzina\Pump\ArrayPumpTrait;
|
||||
use Goteo\Benzina\Pump\DoctrinePumpTrait;
|
||||
use Goteo\Benzina\Pump\PumpInterface;
|
||||
|
||||
class InvestsPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use DatabasePumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
use InvestsPumpTrait;
|
||||
|
||||
public const TRACKING_TITLE_V3 = 'v3 Invest ID';
|
||||
public const TRACKING_TITLE_PAYMENT = 'v3 Invest Payment';
|
||||
public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction';
|
||||
public const TRACKING_TITLE_PREAPPROVAL = 'v3 Invest Preapproval';
|
||||
|
||||
public const CHARGE_TITLE_PROJECT = 'Pago en Goteo v3 - Donación a proyecto';
|
||||
public const CHARGE_TITLE_POOL = 'Pago en Goteo v3 - Carga de monedero';
|
||||
public const CHARGE_TITLE_TIP = 'Pago en Goteo v3 - Propina a la plataforma';
|
||||
|
||||
private const PLATFORM_RETURN_URL = 'https://goteo.org';
|
||||
private const PLATFORM_TIPJAR_NAME = 'platform';
|
||||
|
||||
private const MAX_INT = 2147483647;
|
||||
private const CURRENCY = 'EUR';
|
||||
|
||||
private ?int $tipjarCache = null;
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $userCache = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $projectCache = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $supportCache = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $rewardCache = [];
|
||||
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private ProjectRepository $projectRepository,
|
||||
private SupportRepository $supportRepository,
|
||||
private TipjarRepository $tipjarRepository,
|
||||
private RewardRepository $rewardRepository,
|
||||
private CheckoutService $checkoutService,
|
||||
private MoneyService $moneyService,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $sample): bool
|
||||
{
|
||||
if ($this->hasAllKeys($sample, self::INVEST_KEYS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function pump(mixed $record, array $context): void
|
||||
{
|
||||
if (!$record['user'] || empty($record['user'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$record['amount'] || $record['amount'] < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$record['method'] || empty($record['method'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->getUser($record);
|
||||
if ($user === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$project = $this->getProject($record);
|
||||
$tipjar = $this->getPlatformTipjar();
|
||||
|
||||
$now = new \DateTime();
|
||||
$invested = new \DateTime($record['datetime'] ?? $record['invested']);
|
||||
|
||||
$checkout = new Checkout();
|
||||
$checkout->setMigrated(true);
|
||||
$checkout->setMigratedId($record['id']);
|
||||
$checkout->setDateCreated($invested);
|
||||
$checkout->setDateUpdated($now);
|
||||
|
||||
$checkout->setOrigin($user->getAccounting());
|
||||
$checkout->setStatus($this->getCheckoutStatus($record));
|
||||
$checkout->setGatewayName($this->getCheckoutGateway($record));
|
||||
$checkout->setReturnUrl(self::PLATFORM_RETURN_URL);
|
||||
|
||||
foreach ($this->getCheckoutTrackings($record) as $tracking) {
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$charge = new Charge();
|
||||
$charge->setMigrated(true);
|
||||
$charge->setMigratedId($record['id']);
|
||||
$charge->setDateCreated($invested);
|
||||
$charge->setDateUpdated($now);
|
||||
$charge->setStatus($this->getChargeStatus($record));
|
||||
$charge->setType($this->getChargeType($record));
|
||||
$charge->setMoney($this->getChargeMoney($record['amount'], self::CURRENCY));
|
||||
|
||||
if ($project === null) {
|
||||
$charge->setTitle(self::CHARGE_TITLE_POOL);
|
||||
$charge->setTarget($user->getAccounting());
|
||||
} else {
|
||||
$charge->setTitle(self::CHARGE_TITLE_PROJECT);
|
||||
$charge->setTarget($project->getAccounting());
|
||||
}
|
||||
|
||||
$checkout->addCharge($charge);
|
||||
|
||||
if ($record['donate_amount'] > 0) {
|
||||
$tip = new Charge();
|
||||
$tip->setMigrated(true);
|
||||
$tip->setMigratedId($record['id']);
|
||||
$tip->setDateCreated($invested);
|
||||
$tip->setDateUpdated($now);
|
||||
$tip->setStatus($this->getChargeStatus($record));
|
||||
$tip->setType(ChargeType::Single);
|
||||
$tip->setTitle(self::CHARGE_TITLE_TIP);
|
||||
$tip->setMoney($this->getChargeMoney($record['donate_amount'], self::CURRENCY));
|
||||
$tip->setTarget($tipjar->getAccounting());
|
||||
|
||||
$checkout->addCharge($tip);
|
||||
}
|
||||
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
if (!\in_array($charge->getStatus(), [ChargeStatus::Charged, ChargeStatus::Refunded])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$transaction = new Transaction();
|
||||
$transaction->setDateCreated($invested);
|
||||
$transaction->setMoney($charge->getMoney());
|
||||
$transaction->setOrigin($checkout->getOrigin());
|
||||
$transaction->setTarget($charge->getTarget());
|
||||
|
||||
$charge->addTransaction($transaction);
|
||||
|
||||
if (
|
||||
$charge->getTarget()->getOwner() instanceof Project
|
||||
&& $charge->getStatus() === ChargeStatus::Charged
|
||||
) {
|
||||
$support = $this->getSupport($charge);
|
||||
$support->setProject($charge->getTarget()->getOwner());
|
||||
$support->setOrigin($checkout->getOrigin());
|
||||
$support->addTransaction($transaction);
|
||||
$support->setMoney($this->calcSupportMoney($support));
|
||||
$support->setAnonymous($this->getSupportAnon($record, $support));
|
||||
$support->setMessage($this->getSupportMessage($record, $support, $context));
|
||||
|
||||
$this->entityManager->persist($support);
|
||||
}
|
||||
|
||||
if ($charge->getStatus() !== ChargeStatus::Refunded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$returned = new Transaction();
|
||||
$returned->setDateCreated(new \DateTime($record['returned'] ?? 'now'));
|
||||
$returned->setMoney($charge->getMoney());
|
||||
$returned->setOrigin($charge->getTarget());
|
||||
$returned->setTarget($checkout->getOrigin());
|
||||
|
||||
$charge->addTransaction($returned);
|
||||
}
|
||||
|
||||
$this->persist($checkout, $context);
|
||||
}
|
||||
|
||||
private function getUser(array $record): ?User
|
||||
{
|
||||
$id = $record['user'];
|
||||
|
||||
if (isset($this->userCache[$id])) {
|
||||
return $this->userRepository->find($this->userCache[$id]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->findOneBy(['migratedId' => $id]);
|
||||
|
||||
$this->userCache[$id] = $user->getId();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function getProject(array $record): ?Project
|
||||
{
|
||||
$id = $record['project'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($this->projectCache[$id])) {
|
||||
return $this->projectRepository->find($this->projectCache[$id]);
|
||||
}
|
||||
|
||||
$project = $this->projectRepository->findOneBy(['migratedId' => $id]);
|
||||
|
||||
if (!$project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->projectCache[$id] = $project->getId();
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function getPlatformTipjar(): Tipjar
|
||||
{
|
||||
if ($this->tipjarCache !== null) {
|
||||
return $this->tipjarRepository->find($this->tipjarCache);
|
||||
}
|
||||
|
||||
$tipjar = $this->tipjarRepository->findOneBy(['name' => self::PLATFORM_TIPJAR_NAME]);
|
||||
|
||||
if ($tipjar) {
|
||||
$this->tipjarCache = $tipjar->getId();
|
||||
|
||||
return $tipjar;
|
||||
}
|
||||
|
||||
$tipjar = new Tipjar();
|
||||
$tipjar->setName(self::PLATFORM_TIPJAR_NAME);
|
||||
|
||||
$this->entityManager->persist($tipjar);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $tipjar;
|
||||
}
|
||||
|
||||
private function getSupport(Charge $charge): ?Support
|
||||
{
|
||||
$origin = $charge->getCheckout()->getOrigin();
|
||||
$target = $charge->getTarget()->getOwner();
|
||||
|
||||
$originId = $origin->getId();
|
||||
$projectId = $target->getId();
|
||||
|
||||
$cacheKey = $projectId.'-'.$originId;
|
||||
|
||||
if (isset($this->supportCache[$cacheKey])) {
|
||||
return $this->supportRepository->find($this->supportCache[$cacheKey]);
|
||||
}
|
||||
|
||||
$support = $this->supportRepository->findOneBy([
|
||||
'origin' => $origin,
|
||||
'project' => $target,
|
||||
]);
|
||||
|
||||
if ($support !== null) {
|
||||
$this->supportCache[$cacheKey] = $support->getId();
|
||||
|
||||
return $support;
|
||||
}
|
||||
|
||||
return new Support();
|
||||
}
|
||||
|
||||
private function getSupportAnon(array $record, Support $support): bool
|
||||
{
|
||||
if ($record['anonymous'] === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($support->isAnonymous()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getSupportMessage(array $record, Support $support, array $context): ?string
|
||||
{
|
||||
$message = $support->getMessage();
|
||||
$msg = $this->getInvestMsg($record, $context);
|
||||
|
||||
if (!$msg || empty($msg['msg'])) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
return $msg['msg'];
|
||||
}
|
||||
|
||||
return \sprintf("\n***\n%s", $msg['msg']);
|
||||
}
|
||||
|
||||
private function calcSupportMoney(Support $support): EmbeddableMoney
|
||||
{
|
||||
$transactions = $support->getTransactions();
|
||||
if ($transactions->isEmpty()) {
|
||||
return EmbeddableMoney::of(new Money(0, self::CURRENCY));
|
||||
}
|
||||
|
||||
$first = $transactions->first();
|
||||
$money = new Money(0, $first->getMoney()->getCurrency());
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
$money = $this->moneyService->add($transaction->getMoney(), $money);
|
||||
}
|
||||
|
||||
return EmbeddableMoney::of($money);
|
||||
}
|
||||
|
||||
private function getInvestMsg(array $record, array $context)
|
||||
{
|
||||
$query = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `invest_msg` m WHERE m.invest = :invest'
|
||||
);
|
||||
|
||||
$query->execute(['invest' => $record['id']]);
|
||||
|
||||
return $query->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
private function getChargeType(array $record): ChargeType
|
||||
{
|
||||
if (\in_array($record['method'], ['stripe_subscription'])) {
|
||||
return ChargeType::Recurring;
|
||||
}
|
||||
|
||||
return ChargeType::Single;
|
||||
}
|
||||
|
||||
private function getChargeMoney(int $amount, string $currency): EmbeddableMoney
|
||||
{
|
||||
$amount = $amount * 100;
|
||||
|
||||
if ($amount >= self::MAX_INT) {
|
||||
$amount = self::MAX_INT;
|
||||
}
|
||||
|
||||
return new EmbeddableMoney($amount, $currency);
|
||||
}
|
||||
|
||||
private function getChargeStatus(array $record): ChargeStatus
|
||||
{
|
||||
switch ($record['status']) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 3:
|
||||
case 7:
|
||||
if ($record['issue'] === 1) {
|
||||
return ChargeStatus::InPending;
|
||||
}
|
||||
|
||||
return ChargeStatus::Charged;
|
||||
case 2:
|
||||
case 4:
|
||||
case 6:
|
||||
return ChargeStatus::Refunded;
|
||||
default:
|
||||
return ChargeStatus::InPending;
|
||||
}
|
||||
}
|
||||
|
||||
private function getCheckoutStatus(array $record): CheckoutStatus
|
||||
{
|
||||
switch ($record['status']) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 3:
|
||||
case 7:
|
||||
if ($record['issue'] === 1) {
|
||||
return CheckoutStatus::InPending;
|
||||
}
|
||||
|
||||
return CheckoutStatus::Charged;
|
||||
default:
|
||||
return CheckoutStatus::InPending;
|
||||
}
|
||||
}
|
||||
|
||||
private function getCheckoutGateway(array $record): string
|
||||
{
|
||||
switch ($record['method']) {
|
||||
case 'stripe_subscription':
|
||||
return StripeGateway::getName();
|
||||
case 'pool':
|
||||
return WalletGateway::getName();
|
||||
case 'paypal':
|
||||
return PaypalGateway::getName();
|
||||
case 'tpv':
|
||||
return CecaGateway::getName();
|
||||
case 'cash':
|
||||
return CashGateway::getName();
|
||||
case 'drop':
|
||||
return DropGateway::getName();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tracking[]
|
||||
*/
|
||||
private function getCheckoutTrackings(array $record): array
|
||||
{
|
||||
$v3Tracking = new Tracking();
|
||||
$v3Tracking->setTitle(self::TRACKING_TITLE_V3);
|
||||
$v3Tracking->setValue($record['id']);
|
||||
|
||||
$trackings = [$v3Tracking];
|
||||
|
||||
if (!empty($record['payment'])) {
|
||||
$payment = new Tracking();
|
||||
$payment->setTitle(self::TRACKING_TITLE_PAYMENT);
|
||||
$payment->setValue($record['payment']);
|
||||
|
||||
$trackings[] = $payment;
|
||||
}
|
||||
|
||||
if (!empty($record['transaction'])) {
|
||||
$transaction = new Tracking();
|
||||
$transaction->setTitle(self::TRACKING_TITLE_TRANSACTION);
|
||||
$transaction->setValue($record['transaction']);
|
||||
|
||||
$trackings[] = $transaction;
|
||||
}
|
||||
|
||||
if (!empty($record['preapproval'])) {
|
||||
$preapproval = new Tracking();
|
||||
$preapproval->setTitle(self::TRACKING_TITLE_PREAPPROVAL);
|
||||
$preapproval->setValue($record['preapproval']);
|
||||
|
||||
$trackings[] = $preapproval;
|
||||
}
|
||||
|
||||
return $trackings;
|
||||
}
|
||||
|
||||
private function getInvestReward(array $record, array $context)
|
||||
{
|
||||
$query = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `invest_reward` ir WHERE ir.invest = :invest'
|
||||
);
|
||||
|
||||
$query->execute(['invest' => $record['id']]);
|
||||
|
||||
return $query->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
private function getReward(array $record, array $context): ?Reward
|
||||
{
|
||||
$invested = $this->getInvestReward($record, $context);
|
||||
|
||||
if (!$invested) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$id = $invested['reward'];
|
||||
|
||||
if (isset($this->rewardCache[$id])) {
|
||||
return $this->rewardRepository->find($this->rewardCache[$id]);
|
||||
}
|
||||
|
||||
$reward = $this->rewardRepository->findOneBy(['migratedId' => $invested['reward']]);
|
||||
|
||||
$this->rewardCache[$id] = $reward->getId();
|
||||
|
||||
return $reward;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
trait CheckoutsPumpTrait
|
||||
trait InvestsPumpTrait
|
||||
{
|
||||
private const INVEST_KEYS = [
|
||||
'id',
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Interface\LocalizedEntityInterface;
|
||||
use Gedmo\Translatable\Entity\Translation;
|
||||
use Goteo\Benzina\Pump\ContextAwareTrait;
|
||||
use Goteo\Benzina\Pump\DoctrinePumpTrait;
|
||||
|
||||
trait LocalizedPumpTrait
|
||||
{
|
||||
use ContextAwareTrait;
|
||||
use DoctrinePumpTrait;
|
||||
|
||||
/** @var array<class-string, string[]> */
|
||||
private static array $translatableFieldsCache = [];
|
||||
|
||||
public function localize(
|
||||
LocalizedEntityInterface $entity,
|
||||
array $localizations = [],
|
||||
array $context = [],
|
||||
array $fieldCallbacks = [],
|
||||
): void {
|
||||
if ($this->isDryRun($context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$meta = $em->getClassMetadata($entity::class);
|
||||
$translations = $em->getRepository(Translation::class);
|
||||
|
||||
$translatableFields = $this->getTranslatableFields($meta);
|
||||
|
||||
foreach ($localizations as $localization) {
|
||||
$locale = $localization['lang'] ?? null;
|
||||
if (!$locale) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity->addLocale($locale);
|
||||
|
||||
foreach ($translatableFields as $field) {
|
||||
if (isset($fieldCallbacks[$field]) && is_callable($fieldCallbacks[$field])) {
|
||||
$value = $fieldCallbacks[$field]($localization);
|
||||
} else {
|
||||
$value = $meta->getFieldValue($entity, $field);
|
||||
}
|
||||
|
||||
if ($value !== null) {
|
||||
$translations->translate($entity, $field, $locale, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->preventFlushAndClear) {
|
||||
return;
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all #[Gedmo\Translatable] fields for this entity.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function getTranslatableFields($meta): array
|
||||
{
|
||||
$className = $meta->getName();
|
||||
|
||||
if (isset(self::$translatableFieldsCache[$className])) {
|
||||
return self::$translatableFieldsCache[$className];
|
||||
}
|
||||
|
||||
$reflection = $meta->getReflectionClass();
|
||||
$fields = [];
|
||||
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
$attrs = $property->getAttributes(\Gedmo\Mapping\Annotation\Translatable::class);
|
||||
if (!empty($attrs)) {
|
||||
$fields[] = $property->getName();
|
||||
}
|
||||
}
|
||||
|
||||
self::$translatableFieldsCache[$className] = $fields;
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Project\BudgetItem;
|
||||
use App\Entity\Project\BudgetItemType;
|
||||
use App\Entity\Project\Project;
|
||||
@@ -15,7 +15,11 @@ use Goteo\Benzina\Pump\PumpInterface;
|
||||
class ProjectsBudgetPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use DatabasePumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
use LocalizedPumpTrait;
|
||||
|
||||
private const MAX_INT = 2147483647;
|
||||
|
||||
private const COST_KEYS = [
|
||||
'id',
|
||||
@@ -49,24 +53,34 @@ class ProjectsBudgetPump implements PumpInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$project = $this->getBudgetProject($record);
|
||||
$project = $this->getProject($record);
|
||||
if ($project === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$budgetItem = new BudgetItem();
|
||||
$budgetItem->setMigrated(true);
|
||||
$budgetItem->setMigratedId($record['id']);
|
||||
$budgetItem->setTranslatableLocale($project->getLocales()[0]);
|
||||
$budgetItem->setProject($project);
|
||||
$budgetItem->setType($this->getCostType($record));
|
||||
$budgetItem->setTitle($record['cost']);
|
||||
$budgetItem->setDescription($record['description'] ?? $record['cost']);
|
||||
$budgetItem->setMoney(new Money($record['amount'] * 100, 'EUR'));
|
||||
$budgetItem->setMoney($this->getCostMoney($record['amount'], 'EUR'));
|
||||
$budgetItem->setDeadline($this->getDeadline($record));
|
||||
|
||||
$this->setPreventFlushAndClear(true);
|
||||
$this->persist($budgetItem, $context);
|
||||
|
||||
$localizations = $this->getCostLocalizations($budgetItem, $context);
|
||||
|
||||
$this->setPreventFlushAndClear(false);
|
||||
$this->localize($budgetItem, $localizations, $context, [
|
||||
'title' => fn($l) => $l['cost'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function getBudgetProject(array $record): ?Project
|
||||
private function getProject(array $record): ?Project
|
||||
{
|
||||
return $this->projectRepository->findOneBy(['migratedId' => $record['project']]);
|
||||
}
|
||||
@@ -91,4 +105,26 @@ class ProjectsBudgetPump implements PumpInterface
|
||||
|
||||
return ProjectDeadline::Optimum;
|
||||
}
|
||||
|
||||
private function getCostLocalizations(BudgetItem $budgetItem, array $context): array
|
||||
{
|
||||
$query = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `cost_lang` l WHERE l.id = :cost'
|
||||
);
|
||||
|
||||
$query->execute(['cost' => $budgetItem->getMigratedId()]);
|
||||
|
||||
return $query->fetchAll();
|
||||
}
|
||||
|
||||
private function getCostMoney(int $amount, string $currency): EmbeddableMoney
|
||||
{
|
||||
$amount = $amount * 100;
|
||||
|
||||
if ($amount >= self::MAX_INT) {
|
||||
$amount = self::MAX_INT;
|
||||
}
|
||||
|
||||
return new EmbeddableMoney($amount, $currency);
|
||||
}
|
||||
}
|
||||
|
||||
+113
-28
@@ -3,17 +3,19 @@
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectCalendar;
|
||||
use App\Entity\Project\ProjectCategory;
|
||||
use App\Entity\Project\ProjectDeadline;
|
||||
use App\Entity\Project\ProjectStatus;
|
||||
use App\Entity\Project\ProjectTerritory;
|
||||
use App\Entity\Project\ProjectVideo;
|
||||
use App\Entity\Project\Update;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\User\User;
|
||||
use App\Repository\Project\ProjectRepository;
|
||||
use App\Repository\User\UserRepository;
|
||||
use App\Service\Embed\EmbedService;
|
||||
use App\Service\Project\CalendarService;
|
||||
use App\Service\Project\TerritoryService;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Goteo\Benzina\Pump\ArrayPumpTrait;
|
||||
use Goteo\Benzina\Pump\DoctrinePumpTrait;
|
||||
use Goteo\Benzina\Pump\PumpInterface;
|
||||
@@ -24,12 +26,13 @@ class ProjectsPump implements PumpInterface
|
||||
use DoctrinePumpTrait;
|
||||
use DatabasePumpTrait;
|
||||
use ProjectsPumpTrait;
|
||||
use LocalizedPumpTrait;
|
||||
|
||||
public function __construct(
|
||||
private ProjectRepository $projectRepository,
|
||||
private UserRepository $userRepository,
|
||||
private TerritoryService $territoryService,
|
||||
private EmbedService $embedService,
|
||||
private CalendarService $calendarService,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $sample): bool
|
||||
@@ -48,7 +51,12 @@ class ProjectsPump implements PumpInterface
|
||||
}
|
||||
|
||||
$status = $this->getProjectStatus($record);
|
||||
if (\in_array($status, [ProjectStatus::InEditing, ProjectStatus::Rejected])) {
|
||||
if (\in_array($status, [ProjectStatus::Rejected])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$created = new \DateTime($record['created']);
|
||||
if (\in_array($status, [ProjectStatus::InDraft, ProjectStatus::InEditing]) && $created < new \DateTime('2024-01-01')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,38 +65,49 @@ class ProjectsPump implements PumpInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$project = new Project();
|
||||
$project->setTranslatableLocale($record['lang']);
|
||||
$project->setTitle($record['name']);
|
||||
$project = $this->getProject($record);
|
||||
if ($project === null) {
|
||||
$project = new Project();
|
||||
}
|
||||
|
||||
$project->setSlug($record['id']);
|
||||
$project->setSubtitle($record['subtitle']);
|
||||
$project->setCategory($this->getProjectCategory($record));
|
||||
$project->setTerritory($this->getProjectTerritory($record));
|
||||
$project->setDescription($record['description']);
|
||||
$project->setVideo($this->getProjectVideo($record));
|
||||
$project->setOwner($owner);
|
||||
$project->setStatus($status);
|
||||
$project->setMigrated(true);
|
||||
$project->setMigratedId($record['id']);
|
||||
$project->setDateCreated(new \DateTime($record['created']));
|
||||
$project->setDateCreated($created);
|
||||
$project->setDateUpdated(new \DateTime());
|
||||
|
||||
$updates = $this->getProjectUpdates($project, $context);
|
||||
foreach ($updates as $update) {
|
||||
$project->addUpdate($update);
|
||||
}
|
||||
$project->setTranslatableLocale($record['lang']);
|
||||
$project->setUpdates(new ArrayCollection($this->getProjectUpdates($project, $context)));
|
||||
|
||||
$conf = $this->getProjectConf($project, $context);
|
||||
|
||||
$project->setDeadline($this->getProjectDeadline($conf));
|
||||
$project->setCalendar($this->calendarService->makeCalendar(
|
||||
$project->getDeadline(),
|
||||
new \DateTimeImmutable($record['published']),
|
||||
$conf['days_round1'],
|
||||
$conf['days_round2'],
|
||||
));
|
||||
$project->setCalendar($this->getProjectCalendar($record));
|
||||
|
||||
$project->addLocale($record['lang']);
|
||||
$project->setTitle($record['name'] ?? '');
|
||||
$project->setSubtitle($record['subtitle'] ?? '');
|
||||
$project->setDescription($this->getProjectDescription($record));
|
||||
|
||||
$this->setPreventFlushAndClear(true);
|
||||
$this->persist($project, $context);
|
||||
|
||||
$localizations = $this->getProjectLocalizations($project, $context);
|
||||
|
||||
$this->setPreventFlushAndClear(false);
|
||||
$this->localize($project, $localizations, $context, [
|
||||
'title' => fn($l) => $l['name'],
|
||||
'description' => fn($l) => $this->getProjectDescription($l),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getProject(array $record): ?Project
|
||||
{
|
||||
return $this->projectRepository->findOneBy(['migratedId' => $record['id']]);
|
||||
}
|
||||
|
||||
private function getProjectOwner(array $record): ?User
|
||||
@@ -96,6 +115,55 @@ class ProjectsPump implements PumpInterface
|
||||
return $this->userRepository->findOneBy(['migratedId' => $record['owner']]);
|
||||
}
|
||||
|
||||
private function getProjectDescription(array $record): string
|
||||
{
|
||||
$lang = $record['lang'];
|
||||
$hasTitles = \array_key_exists($lang, self::PROJECT_DESC_TITLES);
|
||||
|
||||
$description = $record['description'];
|
||||
|
||||
if ($hasTitles) {
|
||||
$description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['about']);
|
||||
}
|
||||
|
||||
$description .= \sprintf("\n%s", $record['about']);
|
||||
|
||||
if ($hasTitles) {
|
||||
$description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['motivation']);
|
||||
}
|
||||
|
||||
$description .= \sprintf("\n\n%s", $record['motivation']);
|
||||
|
||||
if ($hasTitles) {
|
||||
$description .= \sprintf("\n\n## %s", self::PROJECT_DESC_TITLES[$lang]['related']);
|
||||
}
|
||||
|
||||
$description .= \sprintf("\n%s", $record['related']);
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
private function getProjectLocalizations(Project $project, array $context): array
|
||||
{
|
||||
$query = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `project_lang` l WHERE l.id = :project'
|
||||
);
|
||||
|
||||
$query->execute(['project' => $project->getMigratedId()]);
|
||||
|
||||
return $query->fetchAll();
|
||||
}
|
||||
|
||||
private function getProjectCalendar(array $record): ProjectCalendar
|
||||
{
|
||||
$calendar = new ProjectCalendar();
|
||||
$calendar->release = new \DateTimeImmutable($record['published']);
|
||||
$calendar->minimum = new \DateTimeImmutable($record['passed'] ?? $record['closed']);
|
||||
$calendar->optimum = new \DateTimeImmutable($record['success'] ?? $record['closed']);
|
||||
|
||||
return $calendar;
|
||||
}
|
||||
|
||||
private function getProjectStatus(array $record): ProjectStatus
|
||||
{
|
||||
switch ($record['status']) {
|
||||
@@ -148,24 +216,37 @@ class ProjectsPump implements PumpInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function getProjectTerritory(array $record): ProjectTerritory
|
||||
private function getProjectTerritory(array $record): Territory
|
||||
{
|
||||
if ($record['project_location'] === null) {
|
||||
return Territory::unknown();
|
||||
}
|
||||
|
||||
$cleanAddress = $this->cleanProjectLocation($record['project_location'], 2);
|
||||
|
||||
if ($cleanAddress === '') {
|
||||
return ProjectTerritory::unknown();
|
||||
return Territory::unknown();
|
||||
}
|
||||
|
||||
return $this->territoryService->search($cleanAddress);
|
||||
}
|
||||
|
||||
private function getProjectVideo(array $record): ?ProjectVideo
|
||||
private function getProjectVideoSource(array $record): ?string
|
||||
{
|
||||
if ($record['video'] === null) {
|
||||
return null;
|
||||
if ($record['video'] !== null) {
|
||||
return \trim($record['video']);
|
||||
}
|
||||
|
||||
$url = \trim($record['video']);
|
||||
if ($record['media'] !== null) {
|
||||
return \trim($record['media']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getProjectVideo(array $record): ?ProjectVideo
|
||||
{
|
||||
$url = $this->getProjectVideoSource($record);
|
||||
|
||||
if ($url === '') {
|
||||
return null;
|
||||
@@ -206,12 +287,16 @@ class ProjectsPump implements PumpInterface
|
||||
|
||||
$posts = $this->getProjectBlogPosts($project, $context);
|
||||
foreach ($posts as $post) {
|
||||
if ($post['publish'] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = new Update();
|
||||
$update->setProject($project);
|
||||
$update->setTranslatableLocale($project->getLocales()[0]);
|
||||
$update->setTitle($post['title']);
|
||||
$update->setSubtitle($post['subtitle'] ?? '');
|
||||
$update->setBody($post['text']);
|
||||
$update->setBody($post['text'] ?? '');
|
||||
$update->setDate(new \DateTime($post['date']));
|
||||
|
||||
$updates[] = $update;
|
||||
|
||||
@@ -23,7 +23,7 @@ trait ProjectsPumpTrait
|
||||
|
||||
// Remove secondary conjoined places from locations
|
||||
// e.g: "España y el mundo" -> "España"
|
||||
foreach ([' / ', ' | ', ' - ', ' y ', ' and ', ' & '] as $conjoinment) {
|
||||
foreach ([' / ', ' | ', ' - ', ' y ', ' i ', ' and ', ' & '] as $conjoinment) {
|
||||
if (\str_contains($location, $conjoinment)) {
|
||||
$location = \explode($conjoinment, $location)[0];
|
||||
}
|
||||
@@ -41,18 +41,6 @@ trait ProjectsPumpTrait
|
||||
|
||||
// Process comma-separated address pieces
|
||||
$location = \explode(',', $location);
|
||||
$location = \array_map(function ($l) {
|
||||
$l = trim($l);
|
||||
|
||||
// Normalize typos and name variations
|
||||
foreach (self::COMMON_VARIATIONS as $standard => $variations) {
|
||||
if (in_array(\mb_strtoupper($l), $variations)) {
|
||||
$l = $standard;
|
||||
}
|
||||
}
|
||||
|
||||
return $l;
|
||||
}, $location);
|
||||
|
||||
// Clean non desired location pieces
|
||||
$location = \array_filter($location, function ($l) {
|
||||
@@ -68,11 +56,26 @@ trait ProjectsPumpTrait
|
||||
return true;
|
||||
});
|
||||
|
||||
// Normalize location pieces
|
||||
$location = \array_map(function ($l) {
|
||||
// Trim spaces and numbers at start of piece
|
||||
$l = \preg_replace('/^[\d ]+/', '', \trim($l));
|
||||
|
||||
// Normalize typos and name variations
|
||||
foreach (self::COMMON_VARIATIONS as $standard => $variations) {
|
||||
if (in_array(\mb_strtoupper($l), $variations)) {
|
||||
$l = $standard;
|
||||
}
|
||||
}
|
||||
|
||||
return $l;
|
||||
}, $location);
|
||||
|
||||
$location = \join(', ', \array_slice($location, -1 * $detailLevel));
|
||||
|
||||
// Trim remaining numbers and punctuation marks
|
||||
$location = \preg_replace('/^[\d\.\-;]+/', '', $location);
|
||||
$location = \preg_replace('/[\d\.\-;]+$/', '', $location);
|
||||
$location = \preg_replace('/^[\d\.\,\-;]+/', '', $location);
|
||||
$location = \preg_replace('/[\d\.\,\-;]+$/', '', $location);
|
||||
|
||||
return \mb_strtoupper(\trim($location));
|
||||
}
|
||||
@@ -81,22 +84,90 @@ trait ProjectsPumpTrait
|
||||
* @var array<string, array> The standard preferred name and a list of possible variations and misspellings
|
||||
*/
|
||||
private const COMMON_VARIATIONS = [
|
||||
'ESPAÑA' => ['ESPANYA', 'ESPANHA', 'ESPAGNE', 'SPAGNA', 'SPANIEN', 'SPAIN'],
|
||||
'CATALUÑA' => ['CATALUNYA', 'PAÏSOS CATALANS'],
|
||||
'ANDALUCÍA' => ['ANDALUCIA', 'ANDALUSIA'],
|
||||
'PERÚ' => ['PÉROU'],
|
||||
'ECUADOR' => ['EQUADOR'],
|
||||
'MÉXICO' => ['MESSICO', 'MX'],
|
||||
'CIUDAD DE MÉXICO' => ['CMDX'],
|
||||
'EUROPA' => ['EUROPEAN UNION'],
|
||||
'FRANCIA' => ['FRANCE'],
|
||||
'ITALIA' => ['ITALY'],
|
||||
'ALEMANIA' => ['GERMANY'],
|
||||
'FINLANDIA' => ['FINLAND'],
|
||||
'SUECIA' => ['SWEDEN', 'SVERIGE'],
|
||||
'ESPAÑA' => ['ESPANYA', 'ESPANHA', 'ESPAGNE', 'SPAGNA', 'SPANIEN', 'SPAIN', 'ESTADO ESPAÑOL', 'ESPAINIA'],
|
||||
'ANDALUCÍA' => ['ANDALUCIA', 'ANDALUSIA'],
|
||||
'CÁDIZ' => ['CADIZ'],
|
||||
'CÓRDOBA' => ['CORDOBA'],
|
||||
'COMUNIDAD VALENCIANA' => ['COMUNITAT VALENCIANA', 'PAÍS VALENCIÀ'],
|
||||
'VALENCIA' => ['VALÈNCIA', 'PROVINCIA DE VALENCIA'],
|
||||
'ALICANTE' => ['ALACANT'],
|
||||
'CASTELLÓN DE LA PLANA' => ['CASTELLÓ DE LA PLANA'],
|
||||
'LAS PALMAS DE GRAN CANARIA' => ['LAS PALMAS'],
|
||||
'CÁCERES' => ['CACERES'],
|
||||
'MÉRIDA' => ['MERIDA'],
|
||||
'LEÓN' => ['LEON'],
|
||||
'LEGANÉS' => ['LEGANES'],
|
||||
'BALEARES' => ['BALEAREN', 'ISLAS BALEARES', 'ILLES BALEARS', 'BALEARIC ISLANDS'],
|
||||
'GALICIA' => ['GALIZA'],
|
||||
'BILBAO' => ['BILBO'],
|
||||
'A CORUÑA' => ['LA CORUÑA'],
|
||||
'OURENSE' => ['ORENSE'],
|
||||
'CATALUÑA' => ['CATALUNYA', 'PAÏSOS CATALANS'],
|
||||
'LLEIDA' => ['LÉRIDA'],
|
||||
'BARCELONA' => ['PROVINCIA DE BARCELONA'],
|
||||
'TARRASA' => ['TERRASA'],
|
||||
'BARCELONA' => ['PROVINCIA DE BARCELONA', 'BARCELONE', 'A BARCELONA'],
|
||||
'HOSPITALET DE LLOBREGAT' => ["L'HOSPITALET DE LLOBREGAT"],
|
||||
'SANT ADRIÀ DE BESÒS' => ['SANT ADRIÀ DEL BESÒS'],
|
||||
'MURCIA' => ['REGIÓN DE MURCIA'],
|
||||
'PAÍS VASCO' => ['EUSKADI', 'EUSKAL HERRIA'],
|
||||
'BILBAO' => ['BILBO'],
|
||||
'SAN SEBASTIÁN' => ['DONOSTIA', 'DONOSTIA-SAN SEBASTIÁN'],
|
||||
'VIZCAYA' => ['BIZKAIA'],
|
||||
'IRÚN' => ['IRUN'],
|
||||
'NAVARRA' => ['NAVARRE', 'NAFARROA'],
|
||||
'PAMLONA' => ['IRUÑEA'],
|
||||
];
|
||||
|
||||
private const PROJECT_DESC_TITLES = [
|
||||
'es' => [
|
||||
'about' => 'Descripción del proyecto. Características, fortalezas y diferenciales.',
|
||||
'motivation' => 'Motivación y a quién va dirigido el proyecto.',
|
||||
'related' => 'Experiencia previa y equipo.',
|
||||
],
|
||||
'gl' => [
|
||||
'about' => 'Características básicas.',
|
||||
'motivation' => 'Motivación e a quen vai dirixido o proxecto.',
|
||||
'related' => 'Experiencia previa e equipo.',
|
||||
],
|
||||
'eu' => [
|
||||
'about' => 'Oinarrizko ezaugarriak eta helburuak.',
|
||||
'motivation' => 'Zerk bultzatzen zaitu eta zeini zuzenduta dago proiektua.',
|
||||
'related' => 'Proiektuaren deskribapenean derrigorrekoa da azaltzea kontutan hartzen den taldearen esperientzia.',
|
||||
],
|
||||
'ca' => [
|
||||
'about' => 'Característiques bàsiques i objectius.',
|
||||
'motivation' => 'Motivació i a qui va dirigit el projecte.',
|
||||
'related' => 'Experiència prèvia i equip.',
|
||||
],
|
||||
'en' => [
|
||||
'about' => 'Main features and goals of the crowdfunding campaign.',
|
||||
'motivation' => 'Why this is important.',
|
||||
'related' => 'Team and experience.',
|
||||
],
|
||||
'fr' => [
|
||||
'about' => 'Caractéristiques principales.',
|
||||
'motivation' => 'Pourquoi est-ce important.',
|
||||
'related' => 'Expérience antérieure et équipe.',
|
||||
],
|
||||
'it' => [
|
||||
'about' => 'Caratteristiche fondamentali.',
|
||||
'motivation' => 'Obiettivo e a chi si dirige il progetto.',
|
||||
'related' => 'Esperienza e team.',
|
||||
],
|
||||
'de' => [
|
||||
'about' => 'Hauptmerkmale.',
|
||||
'motivation' => 'Warum das wichtig ist.',
|
||||
'related' => 'Team und Erfahrung.',
|
||||
],
|
||||
];
|
||||
|
||||
private const PROJECT_KEYS = [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Project\BudgetItemType;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\Reward;
|
||||
@@ -14,7 +14,9 @@ use Goteo\Benzina\Pump\PumpInterface;
|
||||
class ProjectsRewardsPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use DatabasePumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
use LocalizedPumpTrait;
|
||||
|
||||
private const REWARD_KEYS = [
|
||||
'id',
|
||||
@@ -59,24 +61,34 @@ class ProjectsRewardsPump implements PumpInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$project = $this->getBudgetProject($record);
|
||||
$project = $this->getProject($record);
|
||||
if ($project === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reward = new Reward();
|
||||
$reward->addLocale($project->getLocales()[0]);
|
||||
$reward->setProject($project);
|
||||
$reward->setMigrated(true);
|
||||
$reward->setMigratedId($record['id']);
|
||||
$reward->setTitle($record['reward']);
|
||||
$reward->setDescription($record['description'] ?? $record['reward']);
|
||||
$reward->setMoney(new Money($record['amount'] * 100, 'EUR'));
|
||||
$reward->setHasUnits($record['units'] > 0);
|
||||
$reward->setMoney(new EmbeddableMoney($record['amount'] * 100, 'EUR'));
|
||||
$reward->setIsFinite($record['units'] > 0);
|
||||
$reward->setUnitsTotal($record['units'] ?? 0);
|
||||
$reward->setUnitsAvailable($record['units'] ?? 0);
|
||||
|
||||
$this->setPreventFlushAndClear(true);
|
||||
$this->persist($reward, $context);
|
||||
|
||||
$localizations = $this->getRewardLocalizations($reward, $context);
|
||||
|
||||
$this->setPreventFlushAndClear(false);
|
||||
$this->localize($reward, $localizations, $context, [
|
||||
'title' => fn($l) => $l['reward'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function getBudgetProject(array $record): ?Project
|
||||
private function getProject(array $record): ?Project
|
||||
{
|
||||
return $this->projectRepository->findOneBy(['migratedId' => $record['project']]);
|
||||
}
|
||||
@@ -92,4 +104,15 @@ class ProjectsRewardsPump implements PumpInterface
|
||||
return BudgetItemType::Infrastructure;
|
||||
}
|
||||
}
|
||||
|
||||
private function getRewardLocalizations(Reward $reward, array $context): array
|
||||
{
|
||||
$query = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `reward_lang` l WHERE l.id = :reward'
|
||||
);
|
||||
|
||||
$query->execute(['reward' => $reward->getMigratedId()]);
|
||||
|
||||
return $query->fetchAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,20 +59,18 @@ class UsersPump implements PumpInterface
|
||||
$handle = UserService::asHandle($record['email']);
|
||||
}
|
||||
|
||||
$sequenceNumber = \substr($this->userCount, -2, 2);
|
||||
|
||||
return \sprintf('%s_%s', $handle, $sequenceNumber);
|
||||
return \sprintf('%s_%02d', $handle, $this->userCount % 100);
|
||||
}
|
||||
|
||||
private function getDateCreated(array $record): \DateTime
|
||||
{
|
||||
$created = new \DateTime($record['created']);
|
||||
$created = new \DateTime($record['created'] ?? '0000-00-00');
|
||||
|
||||
if ($created > new \DateTime('2011-01-01')) {
|
||||
return $created;
|
||||
}
|
||||
|
||||
return new \DateTime($record['modified']);
|
||||
return new \DateTime($record['modified'] ?? 'now');
|
||||
}
|
||||
|
||||
private function getUserType(array $record): UserType
|
||||
|
||||
@@ -2,33 +2,21 @@
|
||||
|
||||
namespace App\Dto\Gateway;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Gateway\ChargeStatus;
|
||||
use App\Gateway\ChargeType;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class ChargeUpdationDto
|
||||
{
|
||||
/**
|
||||
* The unique identifier for the charge.
|
||||
* The unique identifier for the GatewayCharge.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public int $id;
|
||||
|
||||
/**
|
||||
* The type of the charge.
|
||||
*/
|
||||
public ChargeType $type;
|
||||
|
||||
/**
|
||||
* The title or name of the charge.
|
||||
*/
|
||||
public string $title;
|
||||
|
||||
/**
|
||||
* A detailed description of the charge.
|
||||
*/
|
||||
public string $description;
|
||||
|
||||
/**
|
||||
* The current payment status of the charge.
|
||||
* To ask for a refund, set the status `to_refund`.
|
||||
*/
|
||||
#[Assert\Choice([ChargeStatus::ToRefund])]
|
||||
public ChargeStatus $status;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
namespace App\Dto\Gateway;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto\Matchfunding;
|
||||
|
||||
use App\ApiResource\Matchfunding\MatchCallApiResource;
|
||||
use App\ApiResource\Project\ProjectApiResource;
|
||||
use App\Validator\SingleProjectSubmissionPerCall;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[SingleProjectSubmissionPerCall()]
|
||||
class MatchCallSubmissionCreationDto
|
||||
{
|
||||
/**
|
||||
* The MatchCall to which this MatchCallSubmission belongs to.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Expression(
|
||||
expression: 'value.status === enum("App\\\Entity\\\Matchfunding\\\MatchCallStatus::InCalling")',
|
||||
message: 'MatchCallSubmissions can only relate to a MatchCall with `in_calling` status.'
|
||||
)]
|
||||
public MatchCallApiResource $call;
|
||||
|
||||
/**
|
||||
* The Project that applied for the MatchCall.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public ProjectApiResource $project;
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\ApiResource\Project\ProjectTerritoryApiResource;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectCategory;
|
||||
use App\Entity\Project\ProjectDeadline;
|
||||
use App\Entity\Territory;
|
||||
use App\Mapping\Transformer\ProjectVideoMapTransformer;
|
||||
use AutoMapper\Attribute\MapTo;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -36,7 +36,7 @@ class ProjectCreationDto
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Valid()]
|
||||
public ProjectTerritoryApiResource $territory;
|
||||
public Territory $territory;
|
||||
|
||||
/**
|
||||
* Free-form rich text description for the Project.
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
namespace App\Dto;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Project\ProjectTerritoryApiResource;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectCategory;
|
||||
use App\Entity\Project\ProjectDeadline;
|
||||
use App\Entity\Project\ProjectStatus;
|
||||
use App\Entity\Territory;
|
||||
use App\Mapping\Transformer\ProjectVideoMapTransformer;
|
||||
use AutoMapper\Attribute\MapTo;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -36,7 +36,7 @@ class ProjectUpdationDto
|
||||
* ISO 3166 data about the Project's territory of interest.
|
||||
*/
|
||||
#[Assert\Valid()]
|
||||
public ProjectTerritoryApiResource $territory;
|
||||
public Territory $territory;
|
||||
|
||||
/**
|
||||
* Free-form rich text description for the Project.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\ApiResource\Gateway\ChargeApiResource;
|
||||
use App\ApiResource\Project\RewardApiResource;
|
||||
use App\Validator\AvailableRewardUnits;
|
||||
use App\Validator\EnoughRewardCharge;
|
||||
use App\Validator\SameProjectRewardCharge;
|
||||
use App\Validator\SingleRewardClaimPerCharge;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[EnoughRewardCharge()]
|
||||
#[SameProjectRewardCharge()]
|
||||
#[SingleRewardClaimPerCharge()]
|
||||
class RewardClaimCreationDto
|
||||
{
|
||||
/**
|
||||
* The GatewayCharge granting access to the ProjectReward.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public ChargeApiResource $charge;
|
||||
|
||||
/**
|
||||
* The ProjectReward being claimed.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[AvailableRewardUnits()]
|
||||
public RewardApiResource $reward;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Entity\Accounting;
|
||||
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Matchfunding\MatchCall;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Entity\Trait\UserOwnedTrait;
|
||||
@@ -49,6 +50,9 @@ class Accounting
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?Tipjar $tipjar = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?MatchCall $matchCall = null;
|
||||
|
||||
/**
|
||||
* Create a new Accounting entity instance for the given owner.
|
||||
*/
|
||||
@@ -60,15 +64,6 @@ class Accounting
|
||||
return $accounting;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -107,6 +102,8 @@ class Accounting
|
||||
return $this->getProject();
|
||||
case Tipjar::class:
|
||||
return $this->getTipjar();
|
||||
case MatchCall::class:
|
||||
return $this->getMatchCall();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -127,6 +124,8 @@ class Accounting
|
||||
return $this->setProject($owner);
|
||||
case Tipjar::class:
|
||||
return $this->setTipjar($owner);
|
||||
case MatchCall::class:
|
||||
return $this->setMatchCall($owner);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -197,4 +196,21 @@ class Accounting
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMatchCall(): ?MatchCall
|
||||
{
|
||||
return $this->matchCall;
|
||||
}
|
||||
|
||||
public function setMatchCall(MatchCall $matchCall): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($matchCall->getAccounting() !== $this) {
|
||||
$matchCall->setAccounting($this);
|
||||
}
|
||||
|
||||
$this->matchCall = $matchCall;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Entity\Accounting;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\Entity\EmbeddableMoney as Money;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Accounting\TransactionRepository;
|
||||
@@ -21,6 +21,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Table(name: 'accounting_transaction')]
|
||||
#[ORM\Entity(repositoryClass: TransactionRepository::class)]
|
||||
#[ORM\Index(fields: ['origin'])]
|
||||
#[ORM\Index(fields: ['target'])]
|
||||
class Transaction
|
||||
{
|
||||
use TimestampedCreationEntity;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Money\Conversion\Conversion;
|
||||
use App\Money\MoneyInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* NOT FINANCIALLY SAFE.\
|
||||
* Use `Brick\Money` to perform monetary calculations.
|
||||
*
|
||||
* @see \Brick\Money
|
||||
*/
|
||||
#[ORM\Embeddable]
|
||||
class EmbeddableMoney implements MoneyInterface
|
||||
{
|
||||
/**
|
||||
* An amount of currency.\
|
||||
* Expressed as the minor unit, e.g: cents, pennies, etc.
|
||||
*/
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
public readonly ?int $amount;
|
||||
|
||||
/**
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[ORM\Column(type: 'string', nullable: true)]
|
||||
public readonly ?string $currency;
|
||||
|
||||
#[ORM\Column(type: 'json', nullable: true)]
|
||||
private readonly ?array $conversion;
|
||||
|
||||
public function __construct(
|
||||
?int $amount = null,
|
||||
?string $currency = null,
|
||||
?Conversion $conversion = null,
|
||||
) {
|
||||
$this->amount = $amount;
|
||||
$this->currency = $currency;
|
||||
$this->conversion = $conversion?->toArray();
|
||||
}
|
||||
|
||||
public static function of(MoneyInterface $moneyInterface): self
|
||||
{
|
||||
return new self(
|
||||
$moneyInterface->getAmount(),
|
||||
$moneyInterface->getCurrency(),
|
||||
$moneyInterface->getConversion()
|
||||
);
|
||||
}
|
||||
|
||||
public function getAmount(): int
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
public function getCurrency(): string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function getConversion(): ?Conversion
|
||||
{
|
||||
return $this->conversion
|
||||
? Conversion::fromArray($this->conversion)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ namespace App\Entity\Gateway;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\EmbeddableMoney as Money;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Gateway\ChargeStatus;
|
||||
@@ -25,9 +26,11 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[Gedmo\Loggable()]
|
||||
#[ORM\Table(name: 'checkout_charge')]
|
||||
#[ORM\Index(fields: ['migratedId'])]
|
||||
#[ORM\Entity(repositoryClass: ChargeRepository::class)]
|
||||
class Charge
|
||||
{
|
||||
use MigratedEntity;
|
||||
use TimestampedCreationEntity;
|
||||
use TimestampedUpdationEntity;
|
||||
|
||||
@@ -77,7 +80,7 @@ class Charge
|
||||
/**
|
||||
* @var Collection<int, Transaction>
|
||||
*/
|
||||
#[ORM\JoinTable(name: 'checkout_charge_transaction')]
|
||||
#[ORM\JoinTable(name: 'checkout_charge_trxs')]
|
||||
#[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])]
|
||||
private Collection $transactions;
|
||||
|
||||
@@ -86,12 +89,11 @@ class Charge
|
||||
*/
|
||||
#[Gedmo\Versioned]
|
||||
#[ORM\Column()]
|
||||
private ?ChargeStatus $status = null;
|
||||
private ?ChargeStatus $status = ChargeStatus::InPending;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->transactions = new ArrayCollection();
|
||||
$this->status = ChargeStatus::InPending;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\RefundStrategy;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use App\Validator\SupportedChargeTypes;
|
||||
@@ -77,8 +76,6 @@ class Checkout
|
||||
/**
|
||||
* The address to where the user must be redirected to.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Url()]
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private ?string $returnUrl = null;
|
||||
|
||||
@@ -99,18 +96,16 @@ class Checkout
|
||||
private array $links = [];
|
||||
|
||||
/**
|
||||
* A list of tracking codes provided by the Gateway for this checkout.\
|
||||
* e.g: Order ID, Payment Capture ID, Checkout Session Token.
|
||||
*
|
||||
* @var Tracking[]
|
||||
* @var Collection<int, Tracking>
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $trackings = [];
|
||||
#[ORM\OneToMany(targetEntity: Tracking::class, mappedBy: 'checkout', cascade: ['persist'])]
|
||||
private Collection $trackings;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->status = CheckoutStatus::InPending;
|
||||
$this->charges = new ArrayCollection();
|
||||
$this->trackings = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -268,41 +263,31 @@ class Checkout
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tracking[]
|
||||
* @return Collection<int, Tracking>
|
||||
*/
|
||||
public function getTrackings(): array
|
||||
public function getTrackings(): Collection
|
||||
{
|
||||
return array_map(fn($t) => Tracking::tryFrom($t), $this->trackings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tracking[] $trackings
|
||||
*/
|
||||
public function setTrackings(array $trackings): static
|
||||
{
|
||||
$this->trackings = $trackings;
|
||||
|
||||
return $this;
|
||||
return $this->trackings;
|
||||
}
|
||||
|
||||
public function addTracking(Tracking $tracking): static
|
||||
{
|
||||
$this->removeTracking($tracking);
|
||||
|
||||
$this->trackings = [...$this->trackings, $tracking];
|
||||
if (!$this->trackings->contains($tracking)) {
|
||||
$this->trackings->add($tracking);
|
||||
$tracking->setCheckout($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTracking(Tracking $tracking): static
|
||||
{
|
||||
$this->trackings = \array_filter(
|
||||
\array_map(fn($t) => Tracking::tryFrom($t), $this->trackings),
|
||||
function (Tracking $existingTracking) use ($tracking) {
|
||||
return $existingTracking->title !== $tracking->title
|
||||
&& $existingTracking->value !== $tracking->value;
|
||||
if ($this->trackings->removeElement($tracking)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($tracking->getCheckout() === $this) {
|
||||
$tracking->setCheckout(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Gateway;
|
||||
|
||||
use App\Mapping\Provider\TrackingMapProvider;
|
||||
use App\Repository\Gateway\TrackingRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[MapProvider(TrackingMapProvider::class)]
|
||||
#[ORM\Table(name: 'checkout_tracking')]
|
||||
#[ORM\Entity(repositoryClass: TrackingRepository::class)]
|
||||
class Tracking
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'trackings')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Checkout $checkout = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $value = null;
|
||||
|
||||
public function __construct(
|
||||
?string $title = null,
|
||||
?string $value = null,
|
||||
) {
|
||||
$this->title = $title ?? $title;
|
||||
$this->value = $value ?? $value;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCheckout(): ?Checkout
|
||||
{
|
||||
return $this->checkout;
|
||||
}
|
||||
|
||||
public function setCheckout(?Checkout $checkout): static
|
||||
{
|
||||
$this->checkout = $checkout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(string $value): static
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Matchfunding;
|
||||
|
||||
/**
|
||||
* Defines against what amount of money a Matchfunding shall be.
|
||||
*/
|
||||
enum MatchAgainst: string
|
||||
{
|
||||
public const DEFAULT = self::Charge;
|
||||
|
||||
/**
|
||||
* The money to be matched will be extracted from a Charge item.
|
||||
*/
|
||||
case Charge = 'charge';
|
||||
|
||||
/**
|
||||
* The money to be matched will be from the minimum amount in a Project's budget.
|
||||
*/
|
||||
case BudgetMin = 'budget_min';
|
||||
|
||||
/**
|
||||
* The money to be matched will be from the optimum amount in a Project's budget.
|
||||
*/
|
||||
case BudgetOpt = 'budget_opt';
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Matchfunding;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\User\User;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Matchfunding\MatchCallRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Entity(repositoryClass: MatchCallRepository::class)]
|
||||
class MatchCall implements AccountingOwnerInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\OneToOne(inversedBy: 'matchCall', cascade: ['persist'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MatchStrategy>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: MatchStrategy::class, mappedBy: 'call', cascade: ['persist'])]
|
||||
private Collection $strategies;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MatchCallSubmission>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'call', targetEntity: MatchCallSubmission::class)]
|
||||
private Collection $submissions;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
private Collection $managers;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Embedded(class: Territory::class)]
|
||||
private ?Territory $territory = null;
|
||||
|
||||
#[ORM\Column(enumType: MatchCallStatus::class)]
|
||||
private ?MatchCallStatus $status = MatchCallStatus::InEditing;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->accounting = Accounting::of($this);
|
||||
$this->managers = new ArrayCollection();
|
||||
$this->submissions = new ArrayCollection();
|
||||
$this->strategies = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getAccounting(): ?Accounting
|
||||
{
|
||||
return $this->accounting;
|
||||
}
|
||||
|
||||
public function setAccounting(Accounting $accounting): static
|
||||
{
|
||||
$this->accounting = $accounting;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MatchStrategy>
|
||||
*/
|
||||
public function getStrategies(): Collection
|
||||
{
|
||||
return $this->strategies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, MatchStrategy> $strategies
|
||||
*/
|
||||
public function setStrategies(Collection $strategies): static
|
||||
{
|
||||
$this->strategies = $strategies;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addStrategy(MatchStrategy $strategy): static
|
||||
{
|
||||
if (!$this->strategies->contains($strategy)) {
|
||||
$this->strategies = $this->strategies->map(function (MatchStrategy $s) use ($strategy) {
|
||||
if ($s->getRanking() >= $strategy->getRanking()) {
|
||||
$s->setRanking($s->getRanking() + 1);
|
||||
}
|
||||
|
||||
return $s;
|
||||
});
|
||||
|
||||
$this->strategies->add($strategy);
|
||||
|
||||
$strategy->setCall($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeStrategy(MatchStrategy $strategy): static
|
||||
{
|
||||
if ($this->strategies->removeElement($strategy)) {
|
||||
$this->strategies = $this->strategies->map(function (MatchStrategy $s) use ($strategy) {
|
||||
if ($s->getRanking() > $strategy->getRanking()) {
|
||||
$s->setRanking($s->getRanking() - 1);
|
||||
}
|
||||
|
||||
return $s;
|
||||
});
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MatchCallSubmission>
|
||||
*/
|
||||
public function getSubmissions(): Collection
|
||||
{
|
||||
return $this->submissions;
|
||||
}
|
||||
|
||||
public function addSubmission(MatchCallSubmission $submission): static
|
||||
{
|
||||
if (!$this->submissions->contains($submission)) {
|
||||
$this->submissions->add($submission);
|
||||
$submission->setCall($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSubmission(MatchCallSubmission $submission): static
|
||||
{
|
||||
if ($this->submissions->removeElement($submission)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($submission->getCall() === $this) {
|
||||
$submission->setCall(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, User>
|
||||
*/
|
||||
public function getManagers(): Collection
|
||||
{
|
||||
return $this->managers;
|
||||
}
|
||||
|
||||
public function addManager(User $manager): static
|
||||
{
|
||||
if (!$this->managers->contains($manager)) {
|
||||
$this->managers->add($manager);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeManager(User $manager): static
|
||||
{
|
||||
$this->managers->removeElement($manager);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTerritory(): ?Territory
|
||||
{
|
||||
return $this->territory;
|
||||
}
|
||||
|
||||
public function setTerritory(Territory $territory): static
|
||||
{
|
||||
$this->territory = $territory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?MatchCallStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(MatchCallStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Matchfunding;
|
||||
|
||||
enum MatchCallStatus: string
|
||||
{
|
||||
/**
|
||||
* The MatchCall is still being configured by the managers.
|
||||
*/
|
||||
case InEditing = 'in_editing';
|
||||
|
||||
/**
|
||||
* The MatchCall is taking in MatchCallSubmissions from Projects.
|
||||
*/
|
||||
case InCalling = 'in_calling';
|
||||
|
||||
/**
|
||||
* The MatchCall is giving matchmade funding to the accepted Projects.
|
||||
*/
|
||||
case InMatchmaking = 'in_matchmaking';
|
||||
|
||||
/**
|
||||
* The MatchCall finished giving funds and closing actions can now be performed.
|
||||
*/
|
||||
case ToClosed = 'to_closed';
|
||||
|
||||
/**
|
||||
* The MatchCall is now fully finished and will remain as read-only.
|
||||
*/
|
||||
case Closed = 'closed';
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Matchfunding;
|
||||
|
||||
use App\Entity\Project\Project;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Matchfunding\MatchCallSubmissionRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Entity(repositoryClass: MatchCallSubmissionRepository::class)]
|
||||
class MatchCallSubmission
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'submissions')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?MatchCall $call = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'submissions')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Project $project = null;
|
||||
|
||||
#[ORM\Column(enumType: MatchCallSubmissionStatus::class)]
|
||||
private ?MatchCallSubmissionStatus $status = MatchCallSubmissionStatus::DEFAULT;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCall(): ?MatchCall
|
||||
{
|
||||
return $this->call;
|
||||
}
|
||||
|
||||
public function setCall(?MatchCall $call): static
|
||||
{
|
||||
$this->call = $call;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(?Project $project): static
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?MatchCallSubmissionStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(MatchCallSubmissionStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Matchfunding;
|
||||
|
||||
enum MatchCallSubmissionStatus: string
|
||||
{
|
||||
public const DEFAULT = self::ToReview;
|
||||
|
||||
/**
|
||||
* The MatchCallSubmission is pending review.
|
||||
*/
|
||||
case ToReview = 'to_review';
|
||||
|
||||
/**
|
||||
* The MatchCallSubmission is under review for the MatchCall.
|
||||
*/
|
||||
case InReview = 'in_review';
|
||||
|
||||
/**
|
||||
* The MatchCallSubmission was accepted into the MatchCall.
|
||||
*/
|
||||
case Accepted = 'accepted';
|
||||
|
||||
/**
|
||||
* The MatchCallSubmission was rejected out of the MatchCall.
|
||||
*/
|
||||
case Rejected = 'rejected';
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Matchfunding;
|
||||
|
||||
use App\Entity\EmbeddableMoney as Money;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Matchfunding\Formula\MultiplicationFormula;
|
||||
use App\Repository\Matchfunding\MatchStrategyRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Entity(repositoryClass: MatchStrategyRepository::class)]
|
||||
class MatchStrategy
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'strategies')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?MatchCall $call = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $ranking = null;
|
||||
|
||||
#[ORM\Column(type: Types::ARRAY)]
|
||||
private array $ruleNames = [];
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $formulaName;
|
||||
|
||||
#[ORM\Embedded(class: Money::class)]
|
||||
private ?object $limit = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?float $factor = 1.0;
|
||||
|
||||
#[ORM\Column(enumType: MatchAgainst::class)]
|
||||
private ?MatchAgainst $against = MatchAgainst::DEFAULT;
|
||||
|
||||
public static function of(
|
||||
MatchCall $call,
|
||||
?string $formulaName = null,
|
||||
): MatchStrategy {
|
||||
$strategy = new MatchStrategy();
|
||||
|
||||
$strategy->setCall($call);
|
||||
$strategy->setFormulaName($formulaName ?? MultiplicationFormula::getName());
|
||||
$strategy->setLimit(new Money(
|
||||
2147483647,
|
||||
$call->getAccounting()->getCurrency()
|
||||
));
|
||||
|
||||
return $strategy;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCall(): ?MatchCall
|
||||
{
|
||||
return $this->call;
|
||||
}
|
||||
|
||||
public function setCall(?MatchCall $call): static
|
||||
{
|
||||
$this->call = $call;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRanking(): ?int
|
||||
{
|
||||
return $this->ranking;
|
||||
}
|
||||
|
||||
public function setRanking(int $ranking): static
|
||||
{
|
||||
$this->ranking = $ranking;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRuleNames(): array
|
||||
{
|
||||
return $this->ruleNames;
|
||||
}
|
||||
|
||||
public function setRuleNames(array $ruleNames): static
|
||||
{
|
||||
$this->ruleNames = $ruleNames;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFormulaName(): ?string
|
||||
{
|
||||
return $this->formulaName;
|
||||
}
|
||||
|
||||
public function setFormulaName(string $formulaName): static
|
||||
{
|
||||
$this->formulaName = $formulaName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLimit(): ?Money
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
public function setLimit(Money $limit): static
|
||||
{
|
||||
$this->limit = $limit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFactor(): ?float
|
||||
{
|
||||
return $this->factor;
|
||||
}
|
||||
|
||||
public function setFactor(float $factor): static
|
||||
{
|
||||
$this->factor = $factor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAgainst(): ?MatchAgainst
|
||||
{
|
||||
return $this->against;
|
||||
}
|
||||
|
||||
public function setAgainst(MatchAgainst $against): static
|
||||
{
|
||||
$this->against = $against;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* NOT FINANCIALLY SAFE.\
|
||||
* Use `Brick\Money` to perform monetary calculations.
|
||||
*
|
||||
* @see \Brick\Money
|
||||
*/
|
||||
#[ORM\Embeddable]
|
||||
class Money
|
||||
{
|
||||
/**
|
||||
* An amount of currency.\
|
||||
* Expressed as the minor unit, e.g: cents, pennies, etc.
|
||||
*/
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
public readonly ?int $amount;
|
||||
|
||||
/**
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[ORM\Column(type: 'string', nullable: true)]
|
||||
public readonly ?string $currency;
|
||||
|
||||
public function __construct(
|
||||
?int $amount = null,
|
||||
?string $currency = null,
|
||||
) {
|
||||
$this->amount = $amount;
|
||||
$this->currency = $currency;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace App\Entity\Project;
|
||||
|
||||
use App\Entity\EmbeddableMoney as Money;
|
||||
use App\Entity\Interface\LocalizedEntityInterface;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\Trait\LocalizedEntityTrait;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Project\BudgetItemRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
@@ -17,6 +18,7 @@ use Gedmo\Mapping\Annotation as Gedmo;
|
||||
#[ORM\Entity(repositoryClass: BudgetItemRepository::class)]
|
||||
class BudgetItem implements LocalizedEntityInterface
|
||||
{
|
||||
use MigratedEntity;
|
||||
use LocalizedEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
|
||||
@@ -6,6 +6,9 @@ use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Interface\LocalizedEntityInterface;
|
||||
use App\Entity\Interface\UserOwnedInterface;
|
||||
use App\Entity\Matchfunding\MatchCallSubmission;
|
||||
use App\Entity\Matchfunding\MatchCallSubmissionStatus;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\Trait\LocalizedEntityTrait;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
@@ -22,6 +25,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Index(fields: ['migratedId'])]
|
||||
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
|
||||
class Project implements UserOwnedInterface, AccountingOwnerInterface, LocalizedEntityInterface
|
||||
{
|
||||
@@ -66,8 +70,8 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
/**
|
||||
* Project's territory of interest.
|
||||
*/
|
||||
#[ORM\Embedded(class: ProjectTerritory::class)]
|
||||
private ?ProjectTerritory $territory;
|
||||
#[ORM\Embedded(class: Territory::class)]
|
||||
private ?Territory $territory;
|
||||
|
||||
/**
|
||||
* The description body for the Project.
|
||||
@@ -109,6 +113,12 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
#[ORM\OneToMany(targetEntity: Reward::class, mappedBy: 'project')]
|
||||
private Collection $rewards;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MatchCallSubmission>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'project', targetEntity: MatchCallSubmission::class)]
|
||||
private Collection $matchCallSubmissions;
|
||||
|
||||
/**
|
||||
* @var Collection<int, BudgetItem>
|
||||
*/
|
||||
@@ -131,6 +141,7 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
{
|
||||
$this->accounting = Accounting::of($this);
|
||||
$this->rewards = new ArrayCollection();
|
||||
$this->matchCallSubmissions = new ArrayCollection();
|
||||
$this->budgetItems = new ArrayCollection();
|
||||
$this->updates = new ArrayCollection();
|
||||
$this->supports = new ArrayCollection();
|
||||
@@ -220,12 +231,12 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTerritory(): ?ProjectTerritory
|
||||
public function getTerritory(): ?Territory
|
||||
{
|
||||
return $this->territory;
|
||||
}
|
||||
|
||||
public function setTerritory(ProjectTerritory $territory): static
|
||||
public function setTerritory(Territory $territory): static
|
||||
{
|
||||
$this->territory = $territory;
|
||||
|
||||
@@ -310,6 +321,44 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MatchCallSubmission>
|
||||
*/
|
||||
public function getMatchCallSubmissions(): Collection
|
||||
{
|
||||
return $this->matchCallSubmissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MatchCallSubmission>
|
||||
*/
|
||||
public function getMatchCallSubmissionsBy(MatchCallSubmissionStatus $status): Collection
|
||||
{
|
||||
return $this->matchCallSubmissions->filter(fn($s) => $s->getStatus() === $status);
|
||||
}
|
||||
|
||||
public function addMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static
|
||||
{
|
||||
if (!$this->matchCallSubmissions->contains($MatchCallSubmission)) {
|
||||
$this->matchCallSubmissions->add($MatchCallSubmission);
|
||||
$MatchCallSubmission->setProject($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMatchCallSubmission(MatchCallSubmission $MatchCallSubmission): static
|
||||
{
|
||||
if ($this->matchCallSubmissions->removeElement($MatchCallSubmission)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($MatchCallSubmission->getProject() === $this) {
|
||||
$MatchCallSubmission->setProject(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, BudgetItem>
|
||||
*/
|
||||
@@ -348,6 +397,16 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
return $this->updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Update> $updates
|
||||
*/
|
||||
public function setUpdates(Collection $updates): static
|
||||
{
|
||||
$this->updates = $updates;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUpdate(Update $update): static
|
||||
{
|
||||
if (!$this->updates->contains($update)) {
|
||||
|
||||
@@ -32,6 +32,11 @@ enum ProjectStatus: string
|
||||
*/
|
||||
case Rejected = 'rejected';
|
||||
|
||||
/**
|
||||
* An admin reviewed it and deemed worthy to campaign.
|
||||
*/
|
||||
case ToCampaign = 'to_campaign';
|
||||
|
||||
/**
|
||||
* Project was reviewed and is in campaign for funding.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Entity\Project;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\Entity\EmbeddableMoney as Money;
|
||||
use App\Entity\Interface\LocalizedEntityInterface;
|
||||
use App\Entity\Trait\LocalizedEntityTrait;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Project\RewardRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
@@ -19,8 +21,9 @@ use Gedmo\Mapping\Annotation as Gedmo;
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Table(name: 'project_reward')]
|
||||
#[ORM\Entity(repositoryClass: RewardRepository::class)]
|
||||
class Reward
|
||||
class Reward implements LocalizedEntityInterface
|
||||
{
|
||||
use MigratedEntity;
|
||||
use LocalizedEntityTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
@@ -50,24 +53,30 @@ class Reward
|
||||
* Rewards might be finite, i.e: has a limited amount of existing unitsTotal.
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private ?bool $hasUnits = null;
|
||||
private ?bool $isFinite = null;
|
||||
|
||||
/**
|
||||
* For finite rewards, the total amount of existing unitsTotal.
|
||||
*/
|
||||
#[ORM\Column]
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $unitsTotal = null;
|
||||
|
||||
/**
|
||||
* The total amount of claims on this Reward.
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $unitsClaimed = null;
|
||||
|
||||
/**
|
||||
* For finite rewards, the currently available amount of unitsTotal that can be claimed.
|
||||
*/
|
||||
#[ORM\Column]
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $unitsAvailable = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, RewardClaim>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'reward', targetEntity: RewardClaim::class)]
|
||||
#[ORM\OneToMany(mappedBy: 'reward', targetEntity: RewardClaim::class, cascade: ['persist'])]
|
||||
private Collection $claims;
|
||||
|
||||
public function __construct()
|
||||
@@ -128,14 +137,14 @@ class Reward
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasUnits(): bool
|
||||
public function isFinite(): bool
|
||||
{
|
||||
return $this->hasUnits;
|
||||
return $this->isFinite;
|
||||
}
|
||||
|
||||
public function setHasUnits(bool $hasUnits): static
|
||||
public function setIsFinite(bool $isFinite): static
|
||||
{
|
||||
$this->hasUnits = $hasUnits;
|
||||
$this->isFinite = $isFinite;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -152,6 +161,18 @@ class Reward
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUnitsClaimed(): ?int
|
||||
{
|
||||
return $this->unitsClaimed;
|
||||
}
|
||||
|
||||
public function setUnitsClaimed(int $unitsClaimed): static
|
||||
{
|
||||
$this->unitsClaimed = $unitsClaimed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUnitsAvailable(): ?int
|
||||
{
|
||||
return $this->unitsAvailable;
|
||||
@@ -177,6 +198,8 @@ class Reward
|
||||
if (!$this->claims->contains($claim)) {
|
||||
$this->claims->add($claim);
|
||||
$claim->setReward($this);
|
||||
|
||||
$this->calcUnits();
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -189,8 +212,19 @@ class Reward
|
||||
if ($claim->getReward() === $this) {
|
||||
$claim->setReward(null);
|
||||
}
|
||||
|
||||
$this->calcUnits();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function calcUnits()
|
||||
{
|
||||
$this->unitsClaimed = \count($this->claims);
|
||||
|
||||
if ($this->isFinite()) {
|
||||
$this->unitsAvailable = $this->unitsTotal - $this->unitsClaimed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,35 +23,23 @@ class RewardClaim implements UserOwnedInterface
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Charge $charge = null;
|
||||
private ?User $owner = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'claims')]
|
||||
#[ORM\ManyToOne(inversedBy: 'claims', cascade: ['persist'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Reward $reward = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $owner = null;
|
||||
private ?Charge $charge = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCharge(): ?Charge
|
||||
{
|
||||
return $this->charge;
|
||||
}
|
||||
|
||||
public function setCharge(Charge $charge): static
|
||||
{
|
||||
$this->charge = $charge;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReward(): ?Reward
|
||||
{
|
||||
return $this->reward;
|
||||
@@ -63,4 +51,16 @@ class RewardClaim implements UserOwnedInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCharge(): ?Charge
|
||||
{
|
||||
return $this->charge;
|
||||
}
|
||||
|
||||
public function setCharge(?Charge $charge): static
|
||||
{
|
||||
$this->charge = $charge;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
|
||||
namespace App\Entity\Project;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\User\User;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Project\SupportRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Embedded;
|
||||
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Table(name: 'project_support')]
|
||||
#[ORM\UniqueConstraint(fields: ['project', 'origin'], name: 'project_origin_unique')]
|
||||
#[ORM\Entity(repositoryClass: SupportRepository::class)]
|
||||
class Support
|
||||
{
|
||||
@@ -21,32 +25,35 @@ class Support
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'supports')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $owner = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'supports')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Project $project = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $origin = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Charge>
|
||||
* @var Collection<int, Transaction>
|
||||
*/
|
||||
#[ORM\JoinTable(name: 'project_support_charges')]
|
||||
#[ORM\JoinTable(name: 'project_support_trxs')]
|
||||
#[ORM\JoinColumn(name: 'support_id', referencedColumnName: 'id')]
|
||||
#[ORM\InverseJoinColumn(name: 'charge_id', referencedColumnName: 'id')]
|
||||
#[ORM\ManyToMany(targetEntity: Charge::class)]
|
||||
private Collection $charges;
|
||||
#[ORM\InverseJoinColumn(name: 'transaction_id', referencedColumnName: 'id')]
|
||||
#[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])]
|
||||
private Collection $transactions;
|
||||
|
||||
#[Embedded(class: EmbeddableMoney::class)]
|
||||
private ?EmbeddableMoney $money = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $anonymous = null;
|
||||
private bool $anonymous = false;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $message = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->charges = new ArrayCollection();
|
||||
$this->transactions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -54,14 +61,14 @@ class Support
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOwner(): ?User
|
||||
public function getOrigin(): ?Accounting
|
||||
{
|
||||
return $this->owner;
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function setOwner(?User $owner): static
|
||||
public function setOrigin(?Accounting $origin): static
|
||||
{
|
||||
$this->owner = $owner;
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -79,30 +86,42 @@ class Support
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Charge>
|
||||
* @return Collection<int, Transaction>
|
||||
*/
|
||||
public function getCharges(): Collection
|
||||
public function getTransactions(): Collection
|
||||
{
|
||||
return $this->charges;
|
||||
return $this->transactions;
|
||||
}
|
||||
|
||||
public function addCharge(Charge $charge): static
|
||||
public function addTransaction(Transaction $transaction): static
|
||||
{
|
||||
if (!$this->charges->contains($charge)) {
|
||||
$this->charges->add($charge);
|
||||
if (!$this->transactions->contains($transaction)) {
|
||||
$this->transactions->add($transaction);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCharge(Charge $charge): static
|
||||
public function removeTransaction(Transaction $transaction): static
|
||||
{
|
||||
$this->charges->removeElement($charge);
|
||||
$this->transactions->removeElement($transaction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAnonymous(): ?bool
|
||||
public function getMoney(): ?EmbeddableMoney
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(EmbeddableMoney $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAnonymous(): bool
|
||||
{
|
||||
return $this->anonymous;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Project;
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Validator\CountrySubdivision;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Represents the territory of interest for a Project.
|
||||
*/
|
||||
#[ORM\Embeddable]
|
||||
class ProjectTerritory
|
||||
class Territory
|
||||
{
|
||||
public const COUNTRY_UNKNOWN_CODE = 'ZZ';
|
||||
|
||||
/**
|
||||
* ISO 3166-1 alpha-2 two-letter country code.\
|
||||
* e.g: ES (Spain).
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Country(alpha3: false)]
|
||||
#[ORM\Column(type: Types::STRING, nullable: false)]
|
||||
public readonly string $country;
|
||||
|
||||
/**
|
||||
* First-level sub-division of the country.
|
||||
* ISO 3166-2 first level subdivision code.\
|
||||
* e.g: ES-AN (Andalucía, Spain).
|
||||
*/
|
||||
#[CountrySubdivision()]
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
public readonly ?string $subLvl1;
|
||||
|
||||
/**
|
||||
* Second-level sub-division of the country.
|
||||
* ISO 3166-2 second level subdivision code.\
|
||||
* e.g: ES-GR (Granada, Andalucía, Spain).
|
||||
*/
|
||||
#[CountrySubdivision()]
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
public readonly ?string $subLvl2;
|
||||
|
||||
@@ -38,8 +47,8 @@ class ProjectTerritory
|
||||
$this->subLvl2 = $subLvl2 ?? null;
|
||||
}
|
||||
|
||||
public static function unknown(): ProjectTerritory
|
||||
public static function unknown(): Territory
|
||||
{
|
||||
return new ProjectTerritory(self::COUNTRY_UNKNOWN_CODE);
|
||||
return new Territory(self::COUNTRY_UNKNOWN_CODE);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ namespace App\Entity\User;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
@@ -112,12 +111,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $avatar = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Support>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Support::class, mappedBy: 'owner')]
|
||||
private Collection $supports;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->accounting = Accounting::of($this);
|
||||
@@ -128,7 +121,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
|
||||
$this->emailConfirmed = false;
|
||||
$this->active = false;
|
||||
$this->supports = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -377,34 +369,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Support>
|
||||
*/
|
||||
public function getSupports(): Collection
|
||||
{
|
||||
return $this->supports;
|
||||
}
|
||||
|
||||
public function addSupport(Support $support): static
|
||||
{
|
||||
if (!$this->supports->contains($support)) {
|
||||
$this->supports->add($support);
|
||||
$support->setOwner($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSupport(Support $support): static
|
||||
{
|
||||
if ($this->supports->removeElement($support)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($support->getOwner() === $this) {
|
||||
$support->setOwner(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Entity\Wallet;
|
||||
|
||||
use App\Repository\WalletFinancementRepository;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Repository\Wallet\WalletFinancementRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
@@ -19,8 +20,8 @@ class WalletFinancement
|
||||
/**
|
||||
* The monetary value used in this Financement.
|
||||
*/
|
||||
#[ORM\Embedded(class: Money::class)]
|
||||
private ?Money $money = null;
|
||||
#[ORM\Embedded(class: EmbeddableMoney::class)]
|
||||
private ?EmbeddableMoney $money = null;
|
||||
|
||||
/**
|
||||
* An incoming Statement that originally saved the money in this Financement.
|
||||
@@ -41,12 +42,12 @@ class WalletFinancement
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getMoney(): ?Money
|
||||
public function getMoney(): ?EmbeddableMoney
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(Money $money): static
|
||||
public function setMoney(EmbeddableMoney $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Entity\Wallet;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Gateway\Wallet\StatementDirection;
|
||||
use App\Repository\WalletStatementRepository;
|
||||
use App\Repository\Wallet\WalletStatementRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -40,8 +41,8 @@ class WalletStatement
|
||||
* 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;
|
||||
#[ORM\Embedded(class: EmbeddableMoney::class)]
|
||||
private ?EmbeddableMoney $balance = null;
|
||||
|
||||
/**
|
||||
* A collection of Financements for outgoing Statements that were made using money from this Statement.
|
||||
@@ -103,12 +104,12 @@ class WalletStatement
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBalance(): ?Money
|
||||
public function getBalance(): ?EmbeddableMoney
|
||||
{
|
||||
return $this->balance;
|
||||
}
|
||||
|
||||
public function setBalance(Money $balance): static
|
||||
public function setBalance(EmbeddableMoney $balance): static
|
||||
{
|
||||
$this->balance = $balance;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Money\MoneyService;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
entity: Accounting::class,
|
||||
method: 'prePersist',
|
||||
event: Events::prePersist
|
||||
)]
|
||||
class AccountingDefaultsListener
|
||||
{
|
||||
public function __construct(
|
||||
private MoneyService $moneyService,
|
||||
) {}
|
||||
|
||||
public function prePersist(Accounting $accounting): void
|
||||
{
|
||||
if ($accounting->getCurrency() === null) {
|
||||
$accounting->setCurrency($this->moneyService->getDefaultCurrency());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,74 +2,57 @@
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Entity\User\User;
|
||||
use App\Money\Money;
|
||||
use App\Money\MoneyService;
|
||||
use App\Repository\Project\SupportRepository;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Event\PostUpdateEventArgs;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\PreFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
event: Events::preUpdate,
|
||||
method: 'preUpdate',
|
||||
entity: Checkout::class
|
||||
)]
|
||||
#[AsEntityListener(
|
||||
event: Events::postUpdate,
|
||||
method: 'postUpdate',
|
||||
event: Events::preFlush,
|
||||
method: 'preFlush',
|
||||
entity: Checkout::class
|
||||
)]
|
||||
class GatewayCheckoutSupportsListener
|
||||
{
|
||||
/** @var array<int, Support> */
|
||||
private array $supports;
|
||||
public function __construct(
|
||||
private SupportRepository $supportRepository,
|
||||
private MoneyService $moneyService,
|
||||
) {}
|
||||
|
||||
public function preUpdate(Checkout $checkout, PreUpdateEventArgs $args): void
|
||||
public function preFlush(Checkout $checkout, PreFlushEventArgs $args): void
|
||||
{
|
||||
if (!$args->hasChangedField('status')) {
|
||||
return;
|
||||
/** @var EntityManagerInterface */
|
||||
$em = $args->getObjectManager();
|
||||
$uow = $em->getUnitOfWork();
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Checkout) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$changes = $uow->getEntityChangeSet($entity);
|
||||
|
||||
if (!isset($changes['status']) || !$entity->isCharged()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$supports = $this->prepareSupports($entity);
|
||||
|
||||
foreach ($supports as $support) {
|
||||
$em->persist($support);
|
||||
$uow->computeChangeSet($em->getClassMetadata(Support::class), $support);
|
||||
}
|
||||
}
|
||||
|
||||
if ($checkout->isCharged()) {
|
||||
$this->supports = $this->prepareSupports($checkout, $args->getObjectManager());
|
||||
}
|
||||
}
|
||||
|
||||
public function postUpdate(Checkout $checkout, PostUpdateEventArgs $args): void
|
||||
{
|
||||
if (empty($this->supports)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->supports as $key => $support) {
|
||||
$args->getObjectManager()->persist($support);
|
||||
|
||||
unset($this->supports[$key]);
|
||||
}
|
||||
|
||||
$args->getObjectManager()->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ProjectSupport for the given data.
|
||||
*
|
||||
* @param Charge[] $charges
|
||||
*/
|
||||
private function createSupport(Project $project, User $owner, array $charges): Support
|
||||
{
|
||||
$projectSupport = new Support();
|
||||
$projectSupport->setProject($project);
|
||||
$projectSupport->setOwner($owner);
|
||||
$projectSupport->setAnonymous(false);
|
||||
|
||||
foreach ($charges as $charge) {
|
||||
$projectSupport->addCharge($charge);
|
||||
}
|
||||
|
||||
return $projectSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,26 +60,66 @@ class GatewayCheckoutSupportsListener
|
||||
*/
|
||||
public function prepareSupports(Checkout $checkout): array
|
||||
{
|
||||
/** @var Charge[] */
|
||||
$charges = $checkout->getCharges()->toArray();
|
||||
$owner = $checkout->getOrigin()->getUser();
|
||||
$origin = $checkout->getOrigin();
|
||||
|
||||
// Group charges for projects
|
||||
$chargesInProjectMap = [];
|
||||
$projects = [];
|
||||
$transactionsByProject = [];
|
||||
foreach ($charges as $charge) {
|
||||
$project = $charge->getTarget()->getProject();
|
||||
if ($project === null) {
|
||||
|
||||
if (!$project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$chargesInProjectMap[$project->getId()][] = $charge;
|
||||
$projectId = $project->getId();
|
||||
|
||||
$projects[$projectId] = $project;
|
||||
$transactionsByProject[$projectId] = [
|
||||
...$transactionsByProject[$projectId] ?? [],
|
||||
...$charge->getTransactions(),
|
||||
];
|
||||
}
|
||||
|
||||
$supports = [];
|
||||
foreach ($chargesInProjectMap as $chargesInProject) {
|
||||
$project = $chargesInProject[0]->getTarget()->getProject();
|
||||
$supports[] = $this->createSupport($project, $owner, $chargesInProject);
|
||||
foreach ($transactionsByProject as $projectId => $transactions) {
|
||||
$supports[] = $this->getSupport($projects[$projectId], $origin, $transactions);
|
||||
}
|
||||
|
||||
return $supports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ProjectSupport for the given data.
|
||||
*
|
||||
* @param Transaction[] $transactions
|
||||
*/
|
||||
private function getSupport(Project $project, Accounting $origin, array $transactions): Support
|
||||
{
|
||||
/** @var Support|null */
|
||||
$projectSupport = $this->supportRepository->findOneBy([
|
||||
'project' => $project,
|
||||
'origin' => $origin,
|
||||
]);
|
||||
|
||||
if (!$projectSupport) {
|
||||
$projectSupport = new Support();
|
||||
}
|
||||
|
||||
$projectSupport->setProject($project);
|
||||
$projectSupport->setOrigin($origin);
|
||||
$projectSupport->setAnonymous(false);
|
||||
|
||||
$money = new Money(0, $project->getAccounting()->getCurrency());
|
||||
foreach ($transactions as $transaction) {
|
||||
$money = $this->moneyService->add($transaction->getMoney(), $money);
|
||||
|
||||
$projectSupport->addTransaction($transaction);
|
||||
}
|
||||
|
||||
$projectSupport->setMoney(EmbeddableMoney::of($money));
|
||||
|
||||
return $projectSupport;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Gateway\ChargeStatus;
|
||||
use App\Service\Matchfunding\MatchfundingService;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
event: Events::preUpdate,
|
||||
method: 'preUpdate',
|
||||
entity: Charge::class
|
||||
)]
|
||||
#[AsEntityListener(
|
||||
event: Events::postUpdate,
|
||||
method: 'postUpdate',
|
||||
entity: Charge::class
|
||||
)]
|
||||
final class MatchfundingListener
|
||||
{
|
||||
private ?Charge $charge = null;
|
||||
|
||||
public function __construct(
|
||||
private MatchfundingService $matchfunding,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function preUpdate(Charge $charge, PreUpdateEventArgs $event)
|
||||
{
|
||||
$this->charge = null;
|
||||
|
||||
if (count($event->getEntityChangeSet()) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($charge->getStatus() !== ChargeStatus::Charged) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transactions = $this->matchfunding->match($charge);
|
||||
|
||||
if (\count($transactions) > 0) {
|
||||
foreach ($transactions as $transaction) {
|
||||
$charge->addTransaction($transaction);
|
||||
}
|
||||
|
||||
$this->charge = $charge;
|
||||
}
|
||||
}
|
||||
|
||||
public function postUpdate()
|
||||
{
|
||||
if ($this->charge === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->entityManager->persist($this->charge);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->charge = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
entity: Transaction::class,
|
||||
event: Events::prePersist
|
||||
)]
|
||||
class TransactionCurrencyListener
|
||||
{
|
||||
public function prePersist(Transaction $transaction): void
|
||||
{
|
||||
$money = $transaction->getMoney();
|
||||
$target = $transaction->getTarget();
|
||||
|
||||
if ($money->getCurrency() !== $target->getCurrency()) {
|
||||
throw new \LogicException(\sprintf(
|
||||
'The Accounting with ID %s can only receive Transactions in %s currency. Please do a conversion operation before targeting an Accounting in a different currency.',
|
||||
$target->getId(),
|
||||
$target->getCurrency()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use ApiPlatform\Symfony\EventListener\EventPriorities;
|
||||
use App\ApiResource\User\UserTokenApiResource;
|
||||
use App\Service\Auth\AuthService;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ViewEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
final class AuthorizationTokenSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AuthService $authService,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::VIEW => ['onView', EventPriorities::POST_WRITE],
|
||||
KernelEvents::RESPONSE => ['onResponse', EventPriorities::POST_RESPOND],
|
||||
];
|
||||
}
|
||||
|
||||
public function onView(ViewEvent $event): void
|
||||
{
|
||||
$resourceClass = $event->getRequest()->attributes->get('_api_resource_class');
|
||||
|
||||
if ($resourceClass !== UserTokenApiResource::class) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getRequest()->attributes->set('authTokenMethod', $event->getRequest()->getMethod());
|
||||
|
||||
if ($event->getRequest()->getMethod() === Request::METHOD_POST) {
|
||||
$token = $event->getControllerResult();
|
||||
|
||||
$event->getRequest()->attributes->set('authTokenValue', $token->token);
|
||||
}
|
||||
}
|
||||
|
||||
public function onResponse(ResponseEvent $event): void
|
||||
{
|
||||
$token = $event->getRequest()->attributes->get('authTokenValue');
|
||||
$method = $event->getRequest()->attributes->get('authTokenMethod');
|
||||
|
||||
if (!$method) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case Request::METHOD_DELETE:
|
||||
$event->getResponse()->headers->clearCookie(AuthService::AUTH_COOKIE_NAME);
|
||||
break;
|
||||
case Request::METHOD_POST:
|
||||
$event->getResponse()->headers->setCookie($this->authService->generateCookie($token));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectCategory;
|
||||
use App\Entity\Project\ProjectDeadline;
|
||||
use App\Entity\Project\ProjectStatus;
|
||||
use App\Entity\Project\ProjectTerritory;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\User\User;
|
||||
use App\Factory\User\UserFactory;
|
||||
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
|
||||
@@ -44,7 +44,7 @@ final class ProjectFactory extends PersistentProxyObjectFactory
|
||||
'category' => ProjectCategory::Design,
|
||||
'deadline' => ProjectDeadline::Minimum,
|
||||
'description' => '',
|
||||
'territory' => new ProjectTerritory('ES'),
|
||||
'territory' => new Territory('ES'),
|
||||
'owner' => new User(),
|
||||
'status' => ProjectStatus::InEditing,
|
||||
'subtitle' => 'Subtitle',
|
||||
@@ -58,7 +58,7 @@ final class ProjectFactory extends PersistentProxyObjectFactory
|
||||
'category' => self::faker()->randomElement(ProjectCategory::cases()),
|
||||
'deadline' => self::faker()->randomElement(ProjectDeadline::cases()),
|
||||
'description' => self::faker()->text(),
|
||||
'territory' => new ProjectTerritory('ES'),
|
||||
'territory' => new Territory('ES'),
|
||||
'owner' => UserFactory::createWithMode(),
|
||||
'status' => self::faker()->randomElement(ProjectStatus::cases()),
|
||||
'subtitle' => self::faker()->text(255),
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filter;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use ApiPlatform\Metadata\IriConverterInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use App\ApiResource\Gateway\GatewayApiResource;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
|
||||
|
||||
final class GatewayFilter extends AbstractFilter
|
||||
{
|
||||
public function __construct(
|
||||
private IriConverterInterface $iriConverter,
|
||||
protected ?ManagerRegistry $managerRegistry,
|
||||
?LoggerInterface $logger = null,
|
||||
protected ?array $properties = null,
|
||||
protected ?NameConverterInterface $nameConverter = null,
|
||||
) {
|
||||
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
|
||||
}
|
||||
|
||||
protected function filterProperty(
|
||||
string $property,
|
||||
$value,
|
||||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = [],
|
||||
): void {
|
||||
if (
|
||||
!$this->isPropertyEnabled($property, $resourceClass)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$values = (array) $value;
|
||||
$rootAlias = $queryBuilder->getRootAliases()[0];
|
||||
$parameterName = ':'.$queryNameGenerator->generateParameterName($property);
|
||||
|
||||
if ($this->isPropertyNested($property, $resourceClass)) {
|
||||
[$alias] = $this->addJoinsForNestedProperty($property, $rootAlias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN);
|
||||
}
|
||||
|
||||
$aliasedField = \sprintf('%s.gatewayName', $alias);
|
||||
|
||||
if (\count($values) === 1) {
|
||||
$queryBuilder
|
||||
->andWhere($queryBuilder->expr()->eq($aliasedField, $parameterName))
|
||||
->setParameter($parameterName, $this->getGatewayName($values[0]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->andWhere($queryBuilder->expr()->in($aliasedField, $parameterName))
|
||||
->setParameter($parameterName, array_map(fn($v) => $this->getGatewayName($v), $values));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private function getGatewayName(mixed $value): string
|
||||
{
|
||||
/** @var GatewayApiResource */
|
||||
$gateway = $this->iriConverter->getResourceFromIri($value);
|
||||
|
||||
return $gateway->name;
|
||||
}
|
||||
|
||||
public function getDescription(string $resourceClass): array
|
||||
{
|
||||
$description = [];
|
||||
|
||||
foreach ($this->properties as $property => $strategy) {
|
||||
$description[$property] = [
|
||||
'property' => $property,
|
||||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'is_collection' => false,
|
||||
];
|
||||
|
||||
$description[$property.'[]'] = [
|
||||
'property' => $property,
|
||||
'type' => Type::BUILTIN_TYPE_ARRAY,
|
||||
'required' => false,
|
||||
'is_collection' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,42 @@
|
||||
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class CashGateway implements GatewayInterface
|
||||
class CashGateway implements GatewayInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'cash';
|
||||
}
|
||||
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
public function handleRedirect(Request $request): RedirectResponse
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request): Response
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
|
||||
public function processRefund(Charge $charge): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,47 @@
|
||||
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class CecaGateway implements GatewayInterface
|
||||
class CecaGateway implements GatewayInterface
|
||||
{
|
||||
public const LEGACY_MESSAGE = 'This Gateway implementation is only meant to give support to legacy-records. DO NOT USE. DO NOT IMPLEMENT.';
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'tpv';
|
||||
}
|
||||
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
return [
|
||||
ChargeType::Single,
|
||||
];
|
||||
}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
throw new \Exception(self::LEGACY_MESSAGE);
|
||||
}
|
||||
|
||||
public function handleRedirect(Request $request): RedirectResponse
|
||||
{
|
||||
throw new \Exception(self::LEGACY_MESSAGE);
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request): Response
|
||||
{
|
||||
throw new \Exception(self::LEGACY_MESSAGE);
|
||||
}
|
||||
|
||||
public function processRefund(Charge $charge): void
|
||||
{
|
||||
throw new \Exception(self::LEGACY_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ namespace App\Gateway\Paypal;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Gateway\Tracking;
|
||||
use App\Gateway\AbstractGateway;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\LinkType;
|
||||
use App\Gateway\Tracking;
|
||||
use Brick\Money\Money as BrickMoney;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
@@ -64,9 +64,7 @@ class PaypalGateway extends AbstractGateway
|
||||
'payment_source' => $this->getPaypalPaymentSource($checkout),
|
||||
]);
|
||||
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_ORDER;
|
||||
$tracking->value = $order['id'];
|
||||
$tracking = new Tracking(self::TRACKING_TITLE_ORDER, $order['id']);
|
||||
|
||||
$checkout->addTracking($tracking);
|
||||
|
||||
@@ -111,9 +109,10 @@ class PaypalGateway extends AbstractGateway
|
||||
}
|
||||
|
||||
foreach ($capture['purchase_units'] as $purchaseUnit) {
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$tracking->value = $purchaseUnit['payments']['captures'][0]['id'];
|
||||
$tracking = new Tracking(
|
||||
self::TRACKING_TITLE_TRANSACTION,
|
||||
$purchaseUnit['payments']['captures'][0]['id']
|
||||
);
|
||||
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
@@ -189,9 +188,10 @@ class PaypalGateway extends AbstractGateway
|
||||
}
|
||||
|
||||
foreach ($event['resource']['purchase_units'] as $purchaseUnit) {
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$tracking->value = $purchaseUnit['payments']['captures'][0]['id'];
|
||||
$tracking = new Tracking(
|
||||
self::TRACKING_TITLE_TRANSACTION,
|
||||
$purchaseUnit['payments']['captures'][0]['id']
|
||||
);
|
||||
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
@@ -263,16 +263,15 @@ class PaypalGateway extends AbstractGateway
|
||||
public function processRefund(Charge $charge): void
|
||||
{
|
||||
$trackings = $charge->getCheckout()->getTrackings();
|
||||
$captureTracking = current(array_filter(
|
||||
$trackings,
|
||||
fn(Tracking $tracking) => $tracking->title === self::TRACKING_TITLE_TRANSACTION && $tracking->value
|
||||
));
|
||||
$captureTracking = $trackings->filter(function (Tracking $t) {
|
||||
return $t->getTitle() === self::TRACKING_TITLE_TRANSACTION && $t->getValue();
|
||||
})->current();
|
||||
|
||||
if (!$captureTracking) {
|
||||
throw new \Exception('PayPal capture ID not found in tracking.');
|
||||
throw new \Exception('Tracking for PayPal capture ID not found.');
|
||||
}
|
||||
|
||||
$captureId = $captureTracking->value;
|
||||
$captureId = $captureTracking->getValue();
|
||||
|
||||
$response = $this->paypal->refundCapture($captureId, [
|
||||
'amount' => $this->getPaypalMoney($charge),
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Gateway\Stripe;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Gateway\Tracking;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\User\User;
|
||||
use App\Gateway\AbstractGateway;
|
||||
@@ -11,7 +12,6 @@ use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\LinkType;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Stripe\Checkout\Session as StripeSession;
|
||||
use Stripe\StripeClient;
|
||||
@@ -71,12 +71,7 @@ class StripeGateway extends AbstractGateway
|
||||
$link->type = LinkType::Payment;
|
||||
|
||||
$checkout->addLink($link);
|
||||
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_CHECKOUT;
|
||||
$tracking->value = $session->id;
|
||||
|
||||
$checkout->addTracking($tracking);
|
||||
$checkout->addTracking(new Tracking(self::TRACKING_TITLE_CHECKOUT, $session->id));
|
||||
|
||||
return $checkout;
|
||||
}
|
||||
@@ -209,22 +204,15 @@ class StripeGateway extends AbstractGateway
|
||||
public function processRefund(Charge $charge): void
|
||||
{
|
||||
$trackings = $charge->getCheckout()->getTrackings();
|
||||
$filteredTrackings = array_filter(
|
||||
$trackings,
|
||||
fn(Tracking $tracking) => $tracking->title === self::TRACKING_TITLE_CHECKOUT
|
||||
);
|
||||
$sessionTracking = $trackings->filter(function (Tracking $t) {
|
||||
return $t->getTitle() === self::TRACKING_TITLE_CHECKOUT;
|
||||
})->current();
|
||||
|
||||
$firstTracking = $filteredTrackings[0] ?? null;
|
||||
|
||||
if (!$firstTracking || $firstTracking->title !== self::TRACKING_TITLE_CHECKOUT) {
|
||||
throw new \Exception('Tracking not found or not related to Stripe checkout.');
|
||||
if (!$sessionTracking) {
|
||||
throw new \Exception('Tracking for Stripe Session ID not found.');
|
||||
}
|
||||
|
||||
$sessionId = $firstTracking->value;
|
||||
|
||||
if (!$sessionId) {
|
||||
throw new \Exception('Session ID not found for the charge.');
|
||||
}
|
||||
$sessionId = $sessionTracking->getValue();
|
||||
|
||||
$session = $this->stripe->checkout->sessions->retrieve($sessionId);
|
||||
|
||||
|
||||
@@ -2,32 +2,15 @@
|
||||
|
||||
namespace App\Gateway;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class Tracking
|
||||
{
|
||||
/**
|
||||
* A descriptive title for the tracking number.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public string $title;
|
||||
|
||||
/**
|
||||
* The tracking number given by the Gateway.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public string $value;
|
||||
|
||||
public static function tryFrom($value): Tracking
|
||||
{
|
||||
if ($value instanceof Tracking) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = $value['title'];
|
||||
$tracking->value = $value['value'];
|
||||
|
||||
return $tracking;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ namespace App\Gateway\Wallet;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Money;
|
||||
use App\Gateway\ChargeStatus;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use App\Library\Economy\MoneyService;
|
||||
use App\Money\Money;
|
||||
use App\Money\MoneyService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user