Merge branch 'main' into feature/prod_dockerfile
This commit is contained in:
@@ -34,5 +34,20 @@ DATABASE_URL="mysql://goteo:goteo@mariadb:3306/goteo?serverVersion=10.11.2-Maria
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> doctrineencryptbundle/doctrine-encrypt-bundle ###
|
||||
DOCTRINE_ENCRYPT_KEY=""
|
||||
CRYPTO_SECRET=""
|
||||
###< doctrineencryptbundle/doctrine-encrypt-bundle ###
|
||||
|
||||
###> App\Gateway\Stripe\StripeGateway ###
|
||||
STRIPE_API_KEY=""
|
||||
STRIPE_WEBHOOK_SECRET=""
|
||||
###< App\Gateway\Stripe\StripeGateway ###
|
||||
|
||||
###> App\Gateway\Paypal\PaypalGateway ###
|
||||
# PAYPAL_API_ADDRESS="https://api-m.paypal.com"
|
||||
PAYPAL_API_ADDRESS="https://api-m.sandbox.paypal.com"
|
||||
PAYPAL_CLIENT_ID=""
|
||||
PAYPAL_CLIENT_SECRET=""
|
||||
PAYPAL_WEBHOOK_ID="WEBHOOK_ID"
|
||||
###< App\Gateway\Paypal\PaypalGateway ###
|
||||
|
||||
@@ -4,3 +4,4 @@ APP_SECRET='$ecretf0rt3st'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
CRYPTO_SECRET="CryptoSecret"
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
tools: php-cs-fixer
|
||||
tools: php-cs-fixer:3.64
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
+10
@@ -22,3 +22,13 @@
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> XDebug ###
|
||||
/cachegrind*
|
||||
###< XDebug ###
|
||||
|
||||
###> doctrineencryptbundle/doctrine-encrypt-bundle
|
||||
.Halite.key
|
||||
###< doctrineencryptbundle/doctrine-encrypt-bundle
|
||||
|
||||
.php-version
|
||||
|
||||
@@ -8,6 +8,9 @@ return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
|
||||
'function_declaration' => ['closure_fn_spacing' => 'none'],
|
||||
'single_line_throw' => false,
|
||||
'single_line_empty_body' => true,
|
||||
'yoda_style' => [
|
||||
'equal' => false,
|
||||
'identical' => false,
|
||||
|
||||
+3
-2
@@ -15,6 +15,7 @@
|
||||
"doctrine/doctrine-bundle": "^2.12",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^2.19",
|
||||
"doctrineencryptbundle/doctrine-encrypt-bundle": "^5.4",
|
||||
"nelmio/cors-bundle": "^2.3",
|
||||
"phpdocumentor/reflection-docblock": "^5.3",
|
||||
"phpstan/phpdoc-parser": "^1.24",
|
||||
@@ -28,6 +29,7 @@
|
||||
"symfony/expression-language": "6.4.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "6.4.*",
|
||||
"symfony/http-client": "6.4.*",
|
||||
"symfony/intl": "6.4.*",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/property-access": "6.4.*",
|
||||
@@ -88,11 +90,10 @@
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.58",
|
||||
"friendsofphp/php-cs-fixer": "3.64",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
"symfony/http-client": "6.4.*",
|
||||
"symfony/maker-bundle": "^1.51",
|
||||
"symfony/phpunit-bridge": "^7.0",
|
||||
"zenstruck/foundry": "^1.36"
|
||||
|
||||
Generated
+1049
-587
File diff suppressed because it is too large
Load Diff
@@ -12,4 +12,5 @@ return [
|
||||
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Ambta\DoctrineEncryptBundle\AmbtaDoctrineEncryptBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
ambta_doctrine_encrypt:
|
||||
# If you want, you can use your own Encryptor. Encryptor must implements EncryptorInterface interface
|
||||
# Default: Halite
|
||||
# encryptor_class: Halite
|
||||
|
||||
# Path where to store the keyfiles
|
||||
# Default: '%kernel.project_dir%'
|
||||
# secret_directory_path: '%kernel.project_dir%'
|
||||
|
||||
# Secret string
|
||||
secret: '%env(CRYPTO_SECRET)%'
|
||||
+18
-11
@@ -12,12 +12,12 @@ services:
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
_instanceof:
|
||||
App\Gateway\GatewayInterface:
|
||||
tags: ['app.gateway.gateway']
|
||||
App\Library\Benzina\Pump\PumpInterface:
|
||||
tags: ['app.lib.benzina.pump.pump']
|
||||
App\Library\Economy\Currency\ExchangeInterface:
|
||||
tags: ['app.lib.economy.currency.exchange']
|
||||
App\Library\Economy\Payment\GatewayInterface:
|
||||
tags: ['app.lib.economy.payment.gateway']
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
@@ -37,6 +37,22 @@ services:
|
||||
arguments:
|
||||
- '@App\OpenApi\OpenApiFactory.inner'
|
||||
|
||||
App\Gateway\GatewayLocator:
|
||||
arguments:
|
||||
- !tagged 'app.gateway.gateway'
|
||||
|
||||
App\Gateway\Paypal\PaypalService:
|
||||
arguments:
|
||||
- '%env(PAYPAL_API_ADDRESS)%'
|
||||
- '%env(PAYPAL_CLIENT_ID)%'
|
||||
- '%env(PAYPAL_CLIENT_SECRET)%'
|
||||
- '%env(PAYPAL_WEBHOOK_ID)%'
|
||||
|
||||
App\Gateway\Stripe\StripeGateway:
|
||||
arguments:
|
||||
- '%env(STRIPE_API_KEY)%'
|
||||
- '%env(STRIPE_WEBHOOK_SECRET)%'
|
||||
|
||||
App\Service\Auth\AuthService:
|
||||
arguments:
|
||||
- '%env(APP_SECRET)%'
|
||||
@@ -49,14 +65,5 @@ services:
|
||||
arguments:
|
||||
- !tagged 'app.lib.economy.currency.exchange'
|
||||
|
||||
App\Library\Economy\Payment\GatewayLocator:
|
||||
arguments:
|
||||
- !tagged 'app.lib.economy.payment.gateway'
|
||||
|
||||
App\Library\Economy\Payment\StripeGateway:
|
||||
arguments:
|
||||
- '%env(STRIPE_API_KEY)%'
|
||||
- '%env(STRIPE_WEBHOOK_SECRET)%'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
@@ -7,7 +7,6 @@ services:
|
||||
container_name: goteo-v4-php
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./docker/php/conf.d/apc.ini:/usr/local/etc/php/conf.d/apc.ini:ro
|
||||
- ./docker/php/conf.d/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro
|
||||
|
||||
nginx:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[apc]
|
||||
apc.enabled=1
|
||||
apc.enable_cli=1
|
||||
apc.shm_size=32M
|
||||
apc.ttl=0
|
||||
apc.gc_ttl=3600
|
||||
apc.mmap_file_mask=/tmp/apc.XXXXXX
|
||||
@@ -1,8 +1,7 @@
|
||||
[opcache]
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=1
|
||||
opcache.revalidate_freq=0
|
||||
opcache.validate_timestamps=1
|
||||
opcache.preload=/app/config/preload.php
|
||||
opcache.memory_consumption=256
|
||||
opcache.max_accelerated_files=20000
|
||||
opcache.max_wasted_percentage=10
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[opcache]
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=1
|
||||
opcache.preload=/app/config/preload.php
|
||||
opcache.preload_user=www-data
|
||||
opcache.memory_consumption=256
|
||||
opcache.max_accelerated_files=20000
|
||||
opcache.validate_timestamps=0
|
||||
|
||||
@@ -4,3 +4,4 @@ xdebug.client_host=host.docker.internal
|
||||
xdebug.client_port=9003
|
||||
xdebug.discover_client_host=0
|
||||
xdebug.start_with_request=trigger
|
||||
xdebug.output_dir=/app
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Accounting;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Accounting as Entity;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Money;
|
||||
use App\State\Accounting\AccountingStateProcessor;
|
||||
use App\State\Accounting\AccountingStateProvider;
|
||||
|
||||
/**
|
||||
* Accountings represent payment services used to perform the necessary payments for Transactions between Accounts.\
|
||||
* \
|
||||
* For each Accounting there is an internal implementation that handles the creation and validation of Transactions between Accounts.
|
||||
* These implementations make use of external or internal services to gather the funds that are inside a Transaction,
|
||||
* perform corroboration of funds and store the Transactions into the system.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
stateOptions: new Options(entityClass: Entity\Accounting::class),
|
||||
provider: AccountingStateProvider::class,
|
||||
processor: AccountingStateProcessor::class,
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Get()]
|
||||
#[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')]
|
||||
class Accounting
|
||||
{
|
||||
public ?int $id = null;
|
||||
|
||||
public ?string $currency = null;
|
||||
|
||||
public ?AccountingOwnerInterface $owner = null;
|
||||
|
||||
public ?Money $balance = null;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Gateway;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\Accounting;
|
||||
use App\Entity\Gateway\Charge as EntityCharge;
|
||||
use App\Entity\Money;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\State\Gateway\ChargeStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A Charge represents a payment item to be included in a Checkout for payment at a Gateway.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'GatewayCharge',
|
||||
stateOptions: new Options(entityClass: EntityCharge::class),
|
||||
provider: ChargeStateProvider::class
|
||||
)]
|
||||
#[API\Get()]
|
||||
class Charge
|
||||
{
|
||||
#[API\ApiProperty(writable: false, identifier: true)]
|
||||
public ?int $id = null;
|
||||
|
||||
/**
|
||||
* How this item should be processed by the Gateway.\
|
||||
* \
|
||||
* `single` is for one time payments.\
|
||||
* `recurring` is for payments repeated over time.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public ChargeType $type = ChargeType::Single;
|
||||
|
||||
/**
|
||||
* A short, descriptive string for this charge item.\
|
||||
* May be displayed to the payer.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public string $title;
|
||||
|
||||
/**
|
||||
* Detailed information about the charge item.\
|
||||
* May be displayed to the payer.
|
||||
*/
|
||||
public ?string $description = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the money after a successful payment.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Accounting $target;
|
||||
|
||||
/**
|
||||
* The money to-be-paid for this item at the Gateway.
|
||||
*
|
||||
* It is money before fees and taxes, not accountable.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Money $money;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource\Gateway;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\Accounting;
|
||||
use App\Entity\Gateway as Entity;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\Tracking;
|
||||
use App\State\Gateway\CheckoutStateProcessor;
|
||||
use App\State\Gateway\CheckoutStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCheckout represents a payment session with a Gateway.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'GatewayCheckout',
|
||||
stateOptions: new Options(entityClass: Entity\Checkout::class),
|
||||
provider: CheckoutStateProvider::class,
|
||||
processor: CheckoutStateProcessor::class,
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post()]
|
||||
#[API\Get()]
|
||||
class Checkout
|
||||
{
|
||||
#[API\ApiProperty(writable: false, identifier: true)]
|
||||
public ?int $id = null;
|
||||
|
||||
/**
|
||||
* The desired Gateway to checkout with.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Gateway $gateway;
|
||||
|
||||
/**
|
||||
* The Accounting paying for the charges.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
public Accounting $origin;
|
||||
|
||||
/**
|
||||
* A list of the payment items to be charged to the origin.
|
||||
*
|
||||
* @var Charge[]
|
||||
*/
|
||||
#[API\ApiProperty(readableLink: true, writableLink: true)]
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Count(min: 1)]
|
||||
public array $charges = [];
|
||||
|
||||
/**
|
||||
* The status of this Checkout, as confirmed by the Gateway.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public CheckoutStatus $status = CheckoutStatus::Pending;
|
||||
|
||||
/**
|
||||
* A list of related hyperlinks, as provided by the Gateway.
|
||||
*
|
||||
* @var Link[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $links = [];
|
||||
|
||||
/**
|
||||
* A list of related tracking codes and numbers, as provided by the Gateway.
|
||||
*
|
||||
* @var Tracking[]
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
public array $trackings = [];
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource;
|
||||
namespace App\ApiResource\Gateway;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Library\Economy\Payment\GatewayInterface;
|
||||
use App\State\GatewayStateProvider;
|
||||
use App\State\Gateway\GatewayStateProvider;
|
||||
|
||||
/**
|
||||
* Gateways represent payment services used to perform the necessary payments for Transactions between Accounts.\
|
||||
@@ -13,17 +12,16 @@ use App\State\GatewayStateProvider;
|
||||
* These implementations make use of external or internal services to gather the funds that are inside a Transaction,
|
||||
* perform corroboration of funds and store the Transactions into the system.
|
||||
*/
|
||||
#[API\ApiResource()]
|
||||
#[API\GetCollection(provider: GatewayStateProvider::class)]
|
||||
#[API\Get(provider: GatewayStateProvider::class)]
|
||||
class Gateway
|
||||
{
|
||||
public function __construct(private readonly GatewayInterface $gateway)
|
||||
{
|
||||
}
|
||||
|
||||
#[API\ApiProperty(identifier: true)]
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->gateway->getName();
|
||||
}
|
||||
public ?string $name = null;
|
||||
|
||||
/**
|
||||
* @var array<int, \App\Gateway\ChargeType>
|
||||
*/
|
||||
public ?array $supports = null;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace App\ApiResource;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Filter\ResourceVersionResourceFilter;
|
||||
use App\Filter\ResourceVersionResourceIdFilter;
|
||||
use App\Service\ApiResourceNormalizer;
|
||||
use App\Service\ApiService;
|
||||
use App\State\ResourceVersionStateProvider;
|
||||
use Gedmo\Loggable\Entity\LogEntry;
|
||||
|
||||
@@ -23,9 +23,8 @@ class Version
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LogEntry $log,
|
||||
private readonly object $entity
|
||||
) {
|
||||
}
|
||||
private readonly object $entity,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The ID of the version record.
|
||||
@@ -56,7 +55,7 @@ class Version
|
||||
*/
|
||||
public function getResource(): string
|
||||
{
|
||||
return ApiResourceNormalizer::toResource($this->log->getObjectClass());
|
||||
return ApiService::toResource($this->log->getObjectClass());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
namespace App\Command;
|
||||
|
||||
use App\Library\Benzina\Benzina;
|
||||
use App\Library\Benzina\Pump\PumpInterface;
|
||||
use App\Library\Benzina\Stream\PdoStream;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
@@ -19,7 +22,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
class BenzinaPumpCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private Benzina $benzina
|
||||
private Benzina $benzina,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -28,6 +31,14 @@ class BenzinaPumpCommand extends Command
|
||||
{
|
||||
$this->addArgument('table', InputArgument::REQUIRED);
|
||||
|
||||
$this->addOption(
|
||||
'database',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The address of the database to read from',
|
||||
'mysql://goteo:goteo@mariadb:3306/benzina'
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
'batch-size',
|
||||
null,
|
||||
@@ -37,18 +48,11 @@ class BenzinaPumpCommand extends Command
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
'no-progress',
|
||||
'skip-pumped',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'For progressive pumps, disables the override of already pumped progress'
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
'database',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The address of the database to read from',
|
||||
'mysql://goteo:goteo@mariadb:3306/benzina'
|
||||
InputOption::VALUE_NEGATABLE,
|
||||
'Skips feeding already pumped records in a batch',
|
||||
true
|
||||
);
|
||||
|
||||
$this->addUsage('app:benzina:pump --no-debug user');
|
||||
@@ -65,61 +69,99 @@ EOF
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ConsoleOutputInterface $output
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$batchSize = $input->getOption('batch-size');
|
||||
|
||||
$stream = new PdoStream(
|
||||
$input->getOption('database'),
|
||||
$input->getArgument('table'),
|
||||
$input->getOption('batch-size')
|
||||
$batchSize
|
||||
);
|
||||
$streamSize = $stream->size();
|
||||
$streamBatches = $streamSize / $batchSize;
|
||||
$streamSection = new SymfonyStyle($input, $output->section());
|
||||
|
||||
if ($streamSize < 1) {
|
||||
$io->writeln('No data found at the given source. Skipping execution.');
|
||||
$streamSection->writeln('No data found at the given source. Skipping execution.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$pumps = $this->benzina->getPumps($stream);
|
||||
$pumpsCount = count($pumps);
|
||||
$streamSection->writeln(sprintf('Streaming %d records (%d batches).', $streamSize, $streamBatches));
|
||||
|
||||
$pumps = $this->benzina->getPumpsFor($stream);
|
||||
$pumpsCount = \count($pumps);
|
||||
$pumpsSection = new SymfonyStyle($input, $output->section());
|
||||
|
||||
if ($pumpsCount < 1) {
|
||||
$io->writeln('No pumps available for the data. Skipping execution.');
|
||||
$pumpsSection->writeln('No pumps support the streamed sample. Skipping execution.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->writeln(sprintf('Found %d pumps.', $pumpsCount));
|
||||
$io->listing(\array_map(function ($pump) {
|
||||
$pumpsSection->writeln(sprintf('Streaming to %d pumps.', $pumpsCount));
|
||||
$pumpsSection->listing(\array_map(function (PumpInterface $pump) {
|
||||
return $pump::class;
|
||||
}, $pumps));
|
||||
|
||||
$io->writeln(sprintf('Processing %d records.', $streamSize));
|
||||
$progress = $io->createProgressBar();
|
||||
$progress->start($streamSize);
|
||||
$streamedBatchesSection = $output->section();
|
||||
$streamedBatchesSection->writeln("\tStreamed batches:");
|
||||
$streamedBatches = new ProgressBar($streamedBatchesSection);
|
||||
$streamedBatches->start($streamBatches);
|
||||
|
||||
$streamedRecordsSection = $output->section();
|
||||
$streamedRecordsSection->writeln("\tStreamed records:");
|
||||
$streamedRecords = new ProgressBar($streamedRecordsSection);
|
||||
$streamedRecords->start($streamSize);
|
||||
|
||||
$memUsageSection = $output->section();
|
||||
$memUsageSection->writeln("\tMemory usage:");
|
||||
$memUsage = new ProgressBar($memUsageSection);
|
||||
$memUsage->start($this->getMemoryLimit());
|
||||
|
||||
while (!$stream->eof()) {
|
||||
$data = $stream->read();
|
||||
\memory_reset_peak_usage();
|
||||
|
||||
foreach ($pumps as $pump) {
|
||||
$pump->configure([
|
||||
'progress' => !$input->getOption('no-progress'),
|
||||
$batch = $stream->read();
|
||||
|
||||
$streamBatches = $streamBatches - 1;
|
||||
$streamedBatches->advance();
|
||||
|
||||
foreach ($pumps as $key => $pump) {
|
||||
$pump->setConfig([
|
||||
'skip-pumped' => $input->getOption('skip-pumped'),
|
||||
]);
|
||||
|
||||
$pump->process($data);
|
||||
$pump->pump($batch);
|
||||
|
||||
$memUsage->setProgress(\memory_get_peak_usage(false));
|
||||
}
|
||||
|
||||
$progress->setProgress($stream->tell());
|
||||
$streamed = $stream->tell();
|
||||
$streamedRecords->setProgress($streamed);
|
||||
}
|
||||
|
||||
$stream->close();
|
||||
$progress->finish();
|
||||
|
||||
$io->writeln("\n");
|
||||
$io->success('Data processed successfully!');
|
||||
$endSection = new SymfonyStyle($input, $output->section());
|
||||
$endSection->success('Data processed successfully!');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getMemoryLimit(): int
|
||||
{
|
||||
$limit = \ini_get('memory_limit');
|
||||
if (preg_match('/^(\d+)(.)$/', $limit, $matches)) {
|
||||
if ($matches[2] == 'M') {
|
||||
$limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB
|
||||
} elseif ($matches[2] == 'K') {
|
||||
$limit = $matches[1] * 1024; // nnnK -> nnn KB
|
||||
}
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\DependencyInjection\Compiler\GatewaysCompilerPass;
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:gateways:setup',
|
||||
description: 'Post install setup for Gateways.',
|
||||
)]
|
||||
class GatewaysSetupCommand extends Command
|
||||
{
|
||||
private const SUCCESS_MESSAGE = 'Gateways setup completed successfully!';
|
||||
private const FAILURE_MESSAGE = 'Could not setup Gateways. Please review the error.';
|
||||
|
||||
public function __construct(
|
||||
private GatewayLocator $gatewayLocator
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$classes = $this->gatewayLocator->getClasses();
|
||||
GatewaysCompilerPass::validateGatewayNames($classes);
|
||||
|
||||
$names = $this->gatewayLocator->getNames();
|
||||
GatewaysCompilerPass::compileGatewayNames($names);
|
||||
|
||||
$io->success(self::SUCCESS_MESSAGE);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$io->error(self::FAILURE_MESSAGE);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,12 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
class SystemVarsListCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private SystemVariableRepository $systemVarRepository
|
||||
private SystemVariableRepository $systemVarRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
}
|
||||
protected function configure(): void {}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Library\Economy\Payment\GatewayLocator;
|
||||
use App\Gateway\GatewayLocator;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -19,13 +19,12 @@ class GatewaysController extends AbstractController
|
||||
|
||||
public function __construct(
|
||||
private GatewayLocator $gatewayLocator,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
#[Route('/gateway_redirects', name: self::REDIRECT)]
|
||||
public function handleRedirect(Request $request): Response
|
||||
{
|
||||
$gateway = $this->gatewayLocator->getGateway($request->query->get('gateway'));
|
||||
$gateway = $this->gatewayLocator->get($request->query->get('gateway'));
|
||||
|
||||
return $gateway->handleRedirect($request);
|
||||
}
|
||||
@@ -33,7 +32,7 @@ class GatewaysController extends AbstractController
|
||||
#[Route('/gateway_webhooks', name: self::WEBHOOKS)]
|
||||
public function handleWebhook(Request $request): Response
|
||||
{
|
||||
$gateway = $this->gatewayLocator->getGateway($request->query->get('gateway'));
|
||||
$gateway = $this->gatewayLocator->get($request->query->get('gateway'));
|
||||
|
||||
return $gateway->handleWebhook($request);
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Library\Economy\Payment\GatewayInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class GatewaysCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public const GATEWAYS_DIR = 'gateways';
|
||||
public const GATEWAY_NAMES_LOCK = 'gateway_names_compiled.lock';
|
||||
|
||||
/**
|
||||
* @return string The path to the gateways dir inside the project's var dir
|
||||
*/
|
||||
public static function getCompileDir(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%svar%s%s',
|
||||
\dirname(__DIR__, 3),
|
||||
DIRECTORY_SEPARATOR,
|
||||
DIRECTORY_SEPARATOR,
|
||||
self::GATEWAYS_DIR,
|
||||
);
|
||||
}
|
||||
|
||||
private static function makeCompileDir()
|
||||
{
|
||||
$compileDir = self::getCompileDir();
|
||||
|
||||
if (!\is_dir($compileDir)) {
|
||||
\mkdir($compileDir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The path to the gateway names lock file
|
||||
*/
|
||||
public static function getLockFile(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%s%s',
|
||||
self::getCompileDir(),
|
||||
DIRECTORY_SEPARATOR,
|
||||
self::GATEWAY_NAMES_LOCK
|
||||
);
|
||||
}
|
||||
|
||||
private static function writeLockFile(array $lines)
|
||||
{
|
||||
self::makeCompileDir();
|
||||
|
||||
\file_put_contents(
|
||||
self::getLockFile(),
|
||||
implode(PHP_EOL, $lines)
|
||||
);
|
||||
}
|
||||
|
||||
private static function getGatewaysNamespace(): string
|
||||
{
|
||||
return join('\\', \array_slice(explode('\\', GatewayInterface::class), 0, -1));
|
||||
}
|
||||
|
||||
private function getGatewayNames(array $gatewayClasses): array
|
||||
{
|
||||
$names = [];
|
||||
foreach ($gatewayClasses as $class) {
|
||||
$names[] = $class::getName();
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
private static function getGatewayClasses(string $classesDir): array
|
||||
{
|
||||
$namespace = self::getGatewaysNamespace();
|
||||
$economyDirPaths = \scandir($classesDir);
|
||||
|
||||
$classes = [];
|
||||
foreach ($economyDirPaths as $path) {
|
||||
if ($path === '.' || $path === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = \rtrim($path, '.php');
|
||||
if (!\str_ends_with($className, 'Gateway')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass(sprintf("%s\%s", $namespace, $className));
|
||||
if ($reflection->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classes[] = $reflection->getName();
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the gateway names are unique for each gateway.
|
||||
*
|
||||
* @param array $gatewayClasses Fully-qualified Gateway class names
|
||||
*
|
||||
* @throws \Exception If there are two different Gateway classes with the same name
|
||||
*
|
||||
* @see \App\Library\Economy\Payment\GatewayInterface::getName() To see the names returned by the implementations
|
||||
*/
|
||||
public static function validateGatewayNames(array $gatewayClasses): void
|
||||
{
|
||||
$gatewaysValidated = [];
|
||||
foreach ($gatewayClasses as $gatewayClass) {
|
||||
$gatewayName = $gatewayClass::getName();
|
||||
|
||||
if (\array_key_exists($gatewayName, $gatewaysValidated)) {
|
||||
$exceptionMessage = sprintf(
|
||||
"Duplicate Gateway name '%s' from class %s, name is already in use by class %s",
|
||||
$gatewayName,
|
||||
$gatewayClass,
|
||||
$gatewaysValidated[$gatewayName]
|
||||
);
|
||||
|
||||
throw new \Exception($exceptionMessage);
|
||||
}
|
||||
|
||||
$gatewaysValidated[$gatewayName] = $gatewayClass;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the gateway names in disk.
|
||||
*
|
||||
* @param array $names The names returned by the gateways
|
||||
*
|
||||
* @see \App\Library\Economy\Payment\GatewayInterface::getName() To see the names returned by the implementations
|
||||
*/
|
||||
public static function compileGatewayNames(array $names): void
|
||||
{
|
||||
self::writeLockFile($names);
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$classesDir = join(DIRECTORY_SEPARATOR, [
|
||||
$container->getParameter('kernel.project_dir'),
|
||||
'src',
|
||||
...\array_slice(explode('\\', self::getGatewaysNamespace()), 1),
|
||||
]);
|
||||
|
||||
$gatewayClasses = self::getGatewayClasses($classesDir);
|
||||
self::validateGatewayNames($gatewayClasses);
|
||||
|
||||
$gatewayNames = $this->getGatewayNames($gatewayClasses);
|
||||
self::compileGatewayNames($gatewayNames);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Service\ApiResourceNormalizer;
|
||||
use App\Service\ApiService;
|
||||
use App\Service\VersionedResourceService;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
@@ -28,7 +28,7 @@ class VersionedResourcePass implements CompilerPassInterface
|
||||
|
||||
$versionedResourceNames = [
|
||||
...$versionedResourceNames,
|
||||
ApiResourceNormalizer::toResource($entityClass),
|
||||
ApiService::toResource($entityClass),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceEx
|
||||
final class SystemVariablesLoader implements EnvVarLoaderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private SystemVariableRepository $systemVarRepository
|
||||
) {
|
||||
}
|
||||
private SystemVariableRepository $systemVarRepository,
|
||||
) {}
|
||||
|
||||
public function loadEnvVars(): array
|
||||
{
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Repository\AccountingRepository;
|
||||
use App\Service\ApiResourceNormalizer;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Accountings are the receivers and the issuers of Transactions.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: AccountingRepository::class)]
|
||||
#[API\ApiResource]
|
||||
#[API\Get()]
|
||||
#[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')]
|
||||
class Accounting
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The preferred currency for monetary operations.\
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[Assert\Currency()]
|
||||
#[ORM\Column(length: 3)]
|
||||
private ?string $currency = null;
|
||||
|
||||
#[API\ApiProperty(writable: false, readable: false)]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $ownerClass = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])]
|
||||
private ?User $user = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])]
|
||||
private ?Project $project = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])]
|
||||
private ?Tipjar $tipjar = null;
|
||||
|
||||
#[API\ApiProperty(writable: false, readableLink: false)]
|
||||
#[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)]
|
||||
private Collection $transactionsIssued;
|
||||
|
||||
#[API\ApiProperty(writable: false, readableLink: false)]
|
||||
#[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)]
|
||||
private Collection $transactionsReceived;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
/*
|
||||
* TO-DO: This property must be loaded from App's configuration,
|
||||
* ideally a configuration that can be updated via a frontend, not env var only
|
||||
*/
|
||||
$this->currency = 'EUR';
|
||||
$this->transactionsIssued = new ArrayCollection();
|
||||
$this->transactionsReceived = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCurrency(): ?string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function setCurrency(string $currency): static
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwnerClass(): ?string
|
||||
{
|
||||
return $this->ownerClass;
|
||||
}
|
||||
|
||||
public function setOwnerClass(string $ownerClass): static
|
||||
{
|
||||
// ensure ownership does not change
|
||||
if (
|
||||
$this->ownerClass !== null
|
||||
&& $this->ownerClass !== $ownerClass
|
||||
) {
|
||||
throw new \Exception('Are you trying to commit fraud? Cannot change ownership of an Accounting.');
|
||||
}
|
||||
|
||||
$this->ownerClass = $ownerClass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwnerResource(): string
|
||||
{
|
||||
return ApiResourceNormalizer::toResource($this->ownerClass);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($user->getAccounting() !== $this) {
|
||||
$user->setAccounting($this);
|
||||
}
|
||||
|
||||
// set the owner class
|
||||
$this->setOwnerClass($user::class);
|
||||
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(Project $project): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($project->getAccounting() !== $this) {
|
||||
$project->setAccounting($this);
|
||||
}
|
||||
|
||||
// set the owner class
|
||||
$this->setOwnerClass($project::class);
|
||||
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTipjar(): ?Tipjar
|
||||
{
|
||||
return $this->tipjar;
|
||||
}
|
||||
|
||||
public function setTipjar(Tipjar $tipjar): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($tipjar->getAccounting() !== $this) {
|
||||
$tipjar->setAccounting($this);
|
||||
}
|
||||
|
||||
// set the owner class
|
||||
$this->setOwnerClass($tipjar::class);
|
||||
|
||||
$this->tipjar = $tipjar;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, AccountingTransaction>
|
||||
*/
|
||||
#[API\ApiProperty(readableLink: false)]
|
||||
public function getTransactions(): Collection
|
||||
{
|
||||
$transactions = [
|
||||
...$this->getTransactionsIssued()->toArray(),
|
||||
...$this->getTransactionsReceived()->toArray(),
|
||||
];
|
||||
|
||||
\usort($transactions, function ($a, $b) {
|
||||
return $a->getId() - $b->getId();
|
||||
});
|
||||
|
||||
return new ArrayCollection($transactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, AccountingTransaction>
|
||||
*/
|
||||
public function getTransactionsIssued(): Collection
|
||||
{
|
||||
return $this->transactionsIssued;
|
||||
}
|
||||
|
||||
public function addTransactionsIssued(AccountingTransaction $transaction): static
|
||||
{
|
||||
if (!$this->transactionsIssued->contains($transaction)) {
|
||||
$this->transactionsIssued->add($transaction);
|
||||
$transaction->setOrigin($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTransactionsIssued(AccountingTransaction $transaction): static
|
||||
{
|
||||
if ($this->transactionsIssued->removeElement($transaction)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($transaction->getOrigin() === $this) {
|
||||
$transaction->setOrigin(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, AccountingTransaction>
|
||||
*/
|
||||
public function getTransactionsReceived(): Collection
|
||||
{
|
||||
return $this->transactionsReceived;
|
||||
}
|
||||
|
||||
public function addTransactionsReceived(AccountingTransaction $transaction): static
|
||||
{
|
||||
if (!$this->transactionsReceived->contains($transaction)) {
|
||||
$this->transactionsReceived->add($transaction);
|
||||
$transaction->setTarget($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTransactionsReceived(AccountingTransaction $transaction): static
|
||||
{
|
||||
if ($this->transactionsReceived->removeElement($transaction)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($transaction->getTarget() === $this) {
|
||||
$transaction->setTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Accounting;
|
||||
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Accounting\AccountingRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Accountings are the receivers and the issuers of Transactions.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: AccountingRepository::class)]
|
||||
class Accounting
|
||||
{
|
||||
public const OWNER_CHANGE_NOT_ALLOWED = 'Are you trying to commit fraud? Cannot change ownership of an Accounting.';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The preferred currency for monetary operations.\
|
||||
* 3-letter ISO 4217 currency code.
|
||||
*/
|
||||
#[Assert\Currency()]
|
||||
#[ORM\Column(length: 3)]
|
||||
private ?string $currency = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $owner = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?Project $project = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist'])]
|
||||
private ?Tipjar $tipjar = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
/*
|
||||
* TO-DO: This property must be loaded from App's configuration,
|
||||
* ideally a configuration that can be updated via a frontend, not env var only
|
||||
*/
|
||||
$this->currency = 'EUR';
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCurrency(): ?string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function setCurrency(string $currency): static
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwner(): ?AccountingOwnerInterface
|
||||
{
|
||||
$owner = $this->owner;
|
||||
|
||||
return $this->$owner;
|
||||
}
|
||||
|
||||
public function setOwner(AccountingOwnerInterface $owner): static
|
||||
{
|
||||
if ($owner instanceof User) {
|
||||
$this->owner = 'user';
|
||||
|
||||
return $this->setUser($owner);
|
||||
}
|
||||
|
||||
if ($owner instanceof Project) {
|
||||
$this->owner = 'project';
|
||||
|
||||
return $this->setProject($owner);
|
||||
}
|
||||
|
||||
if ($owner instanceof Tipjar) {
|
||||
$this->owner = 'tipjar';
|
||||
|
||||
return $this->setTipjar($owner);
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('%s is not a recognized AccountingOwnerInterface', $owner::class));
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
// unset the owning side of the relation if necessary
|
||||
if ($user === null && $this->user !== null) {
|
||||
$this->user->setAccounting(null);
|
||||
}
|
||||
|
||||
// set the owning side of the relation if necessary
|
||||
if ($user !== null && $user->getAccounting() !== $this) {
|
||||
$user->setAccounting($this);
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(?Project $project): static
|
||||
{
|
||||
// unset the owning side of the relation if necessary
|
||||
if ($project === null && $this->project !== null) {
|
||||
$this->project->setAccounting(null);
|
||||
}
|
||||
|
||||
// set the owning side of the relation if necessary
|
||||
if ($project !== null && $project->getAccounting() !== $this) {
|
||||
$project->setAccounting($this);
|
||||
}
|
||||
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTipjar(): ?Tipjar
|
||||
{
|
||||
return $this->tipjar;
|
||||
}
|
||||
|
||||
public function setTipjar(?Tipjar $tipjar): static
|
||||
{
|
||||
// unset the owning side of the relation if necessary
|
||||
if ($tipjar === null && $this->tipjar !== null) {
|
||||
$this->tipjar->setAccounting(null);
|
||||
}
|
||||
|
||||
// set the owning side of the relation if necessary
|
||||
if ($tipjar !== null && $tipjar->getAccounting() !== $this) {
|
||||
$tipjar->setAccounting($this);
|
||||
}
|
||||
|
||||
$this->tipjar = $tipjar;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Entity\Accounting;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Repository\AccountingTransactionRepository;
|
||||
use App\Entity\Money;
|
||||
use App\Repository\Accounting\TransactionRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -16,8 +17,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* AccountingTransactions are generated for each GatewayCharge in a GatewayCheckout once it becomes successful.
|
||||
*/
|
||||
#[API\Get()]
|
||||
#[ORM\Entity(repositoryClass: AccountingTransactionRepository::class)]
|
||||
class AccountingTransaction
|
||||
#[ORM\Entity(repositoryClass: TransactionRepository::class)]
|
||||
class Transaction
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -34,20 +35,16 @@ class AccountingTransaction
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* The Accounting issuing the money of this Transaction.
|
||||
* The Accounting from which the Transaction comes from.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\ManyToOne(inversedBy: 'transactionsIssued', cascade: ['persist'])]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $origin = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the money of this Transaction.
|
||||
* The Accounting where the Transaction goes to.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\ManyToOne(inversedBy: 'transactionsReceived', cascade: ['persist'])]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $target = null;
|
||||
|
||||
@@ -75,8 +72,6 @@ class AccountingTransaction
|
||||
|
||||
public function setOrigin(?Accounting $origin): static
|
||||
{
|
||||
$origin->addTransactionsIssued($this);
|
||||
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
@@ -89,8 +84,6 @@ class AccountingTransaction
|
||||
|
||||
public function setTarget(?Accounting $target): static
|
||||
{
|
||||
$target->addTransactionsReceived($this);
|
||||
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Gateway;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Money;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Repository\Gateway\ChargeRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCharge represents a monetary payment that can be done by an issuer at checkout with the Gateway.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: ChargeRepository::class)]
|
||||
class Charge
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'charges')]
|
||||
private ?Checkout $checkout = null;
|
||||
|
||||
/**
|
||||
* The type represents the kind of payment for the charged money.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column()]
|
||||
private ?ChargeType $type = null;
|
||||
|
||||
/**
|
||||
* A short, descriptive text for this charge operation.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
/**
|
||||
* Detailed message about this charge operation.
|
||||
*/
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the consequent Transaction for this GatewayCharge.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $target = null;
|
||||
|
||||
/**
|
||||
* The charged monetary sum.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Embedded(Money::class)]
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Transaction>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Transaction::class, cascade: ['persist'])]
|
||||
private Collection $transactions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->transactions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCheckout(): ?Checkout
|
||||
{
|
||||
return $this->checkout;
|
||||
}
|
||||
|
||||
public function setCheckout(?Checkout $checkout): static
|
||||
{
|
||||
$this->checkout = $checkout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): ?ChargeType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(ChargeType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTarget(): ?Accounting
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function setTarget(?Accounting $target): static
|
||||
{
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMoney(): ?Money
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(Money $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Transaction>
|
||||
*/
|
||||
public function getTransactions(): Collection
|
||||
{
|
||||
return $this->transactions;
|
||||
}
|
||||
|
||||
public function addTransaction(Transaction $transaction): static
|
||||
{
|
||||
if (!$this->transactions->contains($transaction)) {
|
||||
$this->transactions->add($transaction);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTransaction(Transaction $transaction): static
|
||||
{
|
||||
$this->transactions->removeElement($transaction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Gateway;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use App\Validator\SupportedChargeTypes;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCheckout bundles the data to perform a payment operation with a Gateway.\
|
||||
* \
|
||||
* Once the Gateway validates the payment as successful the GatewayCheckout will be updated
|
||||
* and respective AccountingTransactions will be generated for each Charge.
|
||||
*/
|
||||
#[Gedmo\Loggable()]
|
||||
#[ORM\Entity(repositoryClass: CheckoutRepository::class)]
|
||||
#[ORM\Index(fields: ['migratedId'])]
|
||||
class Checkout
|
||||
{
|
||||
use MigratedEntity;
|
||||
use TimestampedCreationEntity;
|
||||
use TimestampedUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The Accounting that will issue the Transactions of the GatewayCharges after a successful checkout.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $origin = null;
|
||||
|
||||
/**
|
||||
* The status of the checkout with the Gateway.
|
||||
*/
|
||||
#[Gedmo\Versioned]
|
||||
#[ORM\Column()]
|
||||
private ?CheckoutStatus $status = null;
|
||||
|
||||
/**
|
||||
* The GatewayCharges to be charged at checkout with the gateway.
|
||||
*
|
||||
* @var Collection<int, Charge>
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Count(min: 1)]
|
||||
#[SupportedChargeTypes()]
|
||||
#[ORM\OneToMany(mappedBy: 'checkout', targetEntity: Charge::class, cascade: ['persist'])]
|
||||
private Collection $charges;
|
||||
|
||||
/**
|
||||
* The name of the Gateway implementation to checkout with.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $gatewayName = null;
|
||||
|
||||
/**
|
||||
* A list of URLs provided by the Gateway for this checkout.\
|
||||
* e.g: Fulfill payment, API resource address.
|
||||
*
|
||||
* @var Link[]
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $links = [];
|
||||
|
||||
/**
|
||||
* A list of tracking codes provided by the Gateway for this checkout.\
|
||||
* e.g: Order ID, Payment Capture ID, Checkout Session Token.
|
||||
*
|
||||
* @var Tracking[]
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $trackings = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->status = CheckoutStatus::Pending;
|
||||
$this->charges = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOrigin(): ?Accounting
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function setOrigin(?Accounting $origin): static
|
||||
{
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?CheckoutStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(CheckoutStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Charge>
|
||||
*/
|
||||
public function getCharges(): Collection
|
||||
{
|
||||
return $this->charges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Charge> $charges
|
||||
*/
|
||||
public function setCharges(Collection $charges): static
|
||||
{
|
||||
$this->charges = new ArrayCollection();
|
||||
|
||||
foreach ($charges as $charge) {
|
||||
$this->charges->add($charge);
|
||||
$charge->setCheckout($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addCharge(Charge $charge): static
|
||||
{
|
||||
if (!$this->charges->contains($charge)) {
|
||||
$this->charges->add($charge);
|
||||
$charge->setCheckout($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCharge(Charge $charge): static
|
||||
{
|
||||
if ($this->charges->removeElement($charge)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($charge->getCheckout() === $this) {
|
||||
$charge->setCheckout(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGatewayName(): ?string
|
||||
{
|
||||
return $this->gatewayName;
|
||||
}
|
||||
|
||||
public function setGatewayName(string $gatewayName): static
|
||||
{
|
||||
$this->gatewayName = $gatewayName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Link[]
|
||||
*/
|
||||
public function getLinks(): array
|
||||
{
|
||||
return $this->links;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Link[] $links
|
||||
*/
|
||||
public function setLinks(array $links): static
|
||||
{
|
||||
$this->links = $links;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addLink(Link $link): static
|
||||
{
|
||||
$this->links = [...$this->links, $link];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeLink(Link $link): static
|
||||
{
|
||||
$this->links = \array_filter(
|
||||
$this->links,
|
||||
function (Link $existingLink) use ($link) {
|
||||
return $existingLink->href !== $link->href;
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tracking[]
|
||||
*/
|
||||
public function getTrackings(): array
|
||||
{
|
||||
return $this->trackings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tracking[] $trackings
|
||||
*/
|
||||
public function setTrackings(array $trackings): static
|
||||
{
|
||||
$this->trackings = $trackings;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTracking(Tracking $tracking): static
|
||||
{
|
||||
$this->trackings = [...$this->trackings, $tracking];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTracking(Tracking $tracking): static
|
||||
{
|
||||
$this->trackings = \array_filter(
|
||||
$this->trackings,
|
||||
function (Tracking $existingTracking) use ($tracking) {
|
||||
return $existingTracking->title !== $tracking->title
|
||||
&& $existingTracking->value !== $tracking->value;
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Repository\GatewayChargeRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCharge represents a monetary payment that can be done by an issuer at checkout with the Gateway.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: GatewayChargeRepository::class)]
|
||||
class GatewayCharge
|
||||
{
|
||||
// TO-DO: This message should be translatable to the User's language
|
||||
public const MESSAGE_STATEMENT = 'PAGO EN GOTEO.ORG';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The type of a GatewayCharge represents the kind of payment.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Column()]
|
||||
private ?GatewayChargeType $type = null;
|
||||
|
||||
/**
|
||||
* The charged monetary sum.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\Embedded(Money::class)]
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* The Accounting receiving the consequent Transaction for this GatewayCharge.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $target = null;
|
||||
|
||||
/**
|
||||
* The AccountingTransaction generated for this charge after the checkout with the Gateway.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false, readableLink: false)]
|
||||
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
|
||||
private ?AccountingTransaction $transaction = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): ?GatewayChargeType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(GatewayChargeType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMoney(): ?Money
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(Money $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTarget(): ?Accounting
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function setTarget(?Accounting $target): static
|
||||
{
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTransaction(): ?AccountingTransaction
|
||||
{
|
||||
return $this->transaction;
|
||||
}
|
||||
|
||||
public function setTransaction(?AccountingTransaction $transaction): static
|
||||
{
|
||||
$this->transaction = $transaction;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampableUpdationEntity;
|
||||
use App\Repository\GatewayCheckoutRepository;
|
||||
use App\State\GatewayCheckoutProcessor;
|
||||
use App\Validator\GatewayName;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A GatewayCheckout bundles the data to perform a payment operation with a Gateway.\
|
||||
* \
|
||||
* Once the Gateway validates the payment as successful the GatewayCheckout will be updated
|
||||
* and respective AccountingTransactions will be generated for each GatewayCharge.
|
||||
*/
|
||||
#[Gedmo\Loggable()]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post(processor: GatewayCheckoutProcessor::class)]
|
||||
#[API\Get()]
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, properties: ['origin' => 'exact', 'charges.target' => 'exact'])]
|
||||
#[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)]
|
||||
class GatewayCheckout
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampableUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The Accounting that will issue the Transactions of the GatewayCharges after a successful checkout.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Accounting $origin = null;
|
||||
|
||||
/**
|
||||
* The GatewayCharges to be charged at checkout with the Gateway.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[Assert\Count(min: 1)]
|
||||
#[API\ApiProperty(readableLink: true, writableLink: true)]
|
||||
#[ORM\ManyToMany(targetEntity: GatewayCharge::class, cascade: ['persist'])]
|
||||
private Collection $charges;
|
||||
|
||||
/**
|
||||
* The status of the checkout with the Gateway.
|
||||
*/
|
||||
#[Gedmo\Versioned]
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(SearchFilter::class)]
|
||||
#[ORM\Column()]
|
||||
private ?GatewayCheckoutStatus $status = null;
|
||||
|
||||
/**
|
||||
* The name of the Gateway implementation to checkout with.
|
||||
*/
|
||||
#[GatewayName]
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiFilter(SearchFilter::class)]
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $gateway = null;
|
||||
|
||||
/**
|
||||
* An external identifier provided by the Gateway for the payment.\
|
||||
* Required when a GatewayCheckout is completed.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(SearchFilter::class)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $gatewayReference = null;
|
||||
|
||||
/**
|
||||
* The URL where the checkout with the Gateway is available.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $checkoutUrl = null;
|
||||
|
||||
/**
|
||||
* GatewayCheckout was migrated from an invest record in Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private ?bool $migrated = false;
|
||||
|
||||
/**
|
||||
* The id of the original invest record in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $migratedReference = null;
|
||||
|
||||
/**
|
||||
* A free-form collection of additional data associated with this checkout operation.
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?array $metadata = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->charges = new ArrayCollection();
|
||||
$this->status = GatewayCheckoutStatus::Pending;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOrigin(): ?Accounting
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function setOrigin(?Accounting $origin): static
|
||||
{
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, GatewayCharge>
|
||||
*/
|
||||
public function getCharges(): Collection
|
||||
{
|
||||
return $this->charges;
|
||||
}
|
||||
|
||||
public function addCharge(GatewayCharge $charge): static
|
||||
{
|
||||
if (!$this->charges->contains($charge)) {
|
||||
$this->charges->add($charge);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCharge(GatewayCharge $charge): static
|
||||
{
|
||||
$this->charges->removeElement($charge);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?GatewayCheckoutStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(GatewayCheckoutStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGateway(): ?string
|
||||
{
|
||||
return $this->gateway;
|
||||
}
|
||||
|
||||
public function setGateway(string $gateway): static
|
||||
{
|
||||
$this->gateway = $gateway;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGatewayReference(): ?string
|
||||
{
|
||||
return $this->gatewayReference;
|
||||
}
|
||||
|
||||
public function setGatewayReference(string $gatewayReference): static
|
||||
{
|
||||
$this->gatewayReference = $gatewayReference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCheckoutUrl(): ?string
|
||||
{
|
||||
return $this->checkoutUrl;
|
||||
}
|
||||
|
||||
public function setCheckoutUrl(string $checkoutUrl): static
|
||||
{
|
||||
$this->checkoutUrl = $checkoutUrl;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedReference(): ?string
|
||||
{
|
||||
return $this->migratedReference;
|
||||
}
|
||||
|
||||
public function setMigratedReference(?string $migratedReference): static
|
||||
{
|
||||
$this->migratedReference = $migratedReference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMetadata(): ?array
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
public function setMetadata(?array $metadata): static
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Interface;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
|
||||
interface AccountingOwnerInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getAccounting(): ?Accounting;
|
||||
|
||||
public function setAccounting(Accounting $accounting): static;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class Money
|
||||
|
||||
public function __construct(
|
||||
int $amount,
|
||||
string $currency
|
||||
string $currency,
|
||||
) {
|
||||
$this->amount = $amount;
|
||||
$this->currency = $currency;
|
||||
|
||||
+15
-47
@@ -4,8 +4,11 @@ namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampableUpdationEntity;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
@@ -23,10 +26,11 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
'owner' => 'exact',
|
||||
])]
|
||||
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
|
||||
class Project
|
||||
class Project implements AccountingOwnerInterface
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampableUpdationEntity;
|
||||
use MigratedEntity;
|
||||
use TimestampedCreationEntity;
|
||||
use TimestampedUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -44,8 +48,7 @@ class Project
|
||||
* A Project's Accounting represents how much money the Project has raised from the community.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'project', cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'project', cascade: ['persist'])]
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
/**
|
||||
@@ -64,23 +67,12 @@ class Project
|
||||
#[ORM\Column(type: 'string', enumType: ProjectStatus::class)]
|
||||
private ProjectStatus $status;
|
||||
|
||||
/**
|
||||
* Project was migrated from Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private ?bool $migrated = null;
|
||||
|
||||
/**
|
||||
* The previous id of this Project in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $migratedReference = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->migrated = false;
|
||||
$accounting = new Accounting();
|
||||
$accounting->setOwner($this);
|
||||
|
||||
$this->accounting = $accounting;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -105,7 +97,7 @@ class Project
|
||||
return $this->accounting;
|
||||
}
|
||||
|
||||
public function setAccounting(Accounting $accounting): static
|
||||
public function setAccounting(?Accounting $accounting): static
|
||||
{
|
||||
$this->accounting = $accounting;
|
||||
|
||||
@@ -135,28 +127,4 @@ class Project
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedReference(): ?string
|
||||
{
|
||||
return $this->migratedReference;
|
||||
}
|
||||
|
||||
public function setMigratedReference(?string $migratedReference): static
|
||||
{
|
||||
$this->migratedReference = $migratedReference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -3,6 +3,8 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Repository\TipjarRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
@@ -19,7 +21,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
#[API\Delete()]
|
||||
#[API\Patch()]
|
||||
#[ORM\Entity(repositoryClass: TipjarRepository::class)]
|
||||
class Tipjar
|
||||
class Tipjar implements AccountingOwnerInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -33,10 +35,17 @@ class Tipjar
|
||||
private ?string $name = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'tipjar', cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'tipjar', cascade: ['persist'])]
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$accounting = new Accounting();
|
||||
$accounting->setOwner($this);
|
||||
|
||||
$this->accounting = $accounting;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -59,7 +68,7 @@ class Tipjar
|
||||
return $this->accounting;
|
||||
}
|
||||
|
||||
public function setAccounting(Accounting $accounting): static
|
||||
public function setAccounting(?Accounting $accounting): static
|
||||
{
|
||||
$this->accounting = $accounting;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\Trait;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait MigratedEntity
|
||||
{
|
||||
/**
|
||||
* Entity was migrated from the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column()]
|
||||
protected ?bool $migrated = false;
|
||||
|
||||
/**
|
||||
* Previous ID of the entity in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
protected ?string $migratedId = null;
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedId(): ?string
|
||||
{
|
||||
return $this->migratedId;
|
||||
}
|
||||
|
||||
public function setMigratedId(string $migratedId): static
|
||||
{
|
||||
$this->migratedId = $migratedId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
trait TimestampableCreationEntity
|
||||
trait TimestampedCreationEntity
|
||||
{
|
||||
/**
|
||||
* Entity creation timestamp.
|
||||
+1
-1
@@ -7,7 +7,7 @@ use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
trait TimestampableUpdationEntity
|
||||
trait TimestampedUpdationEntity
|
||||
{
|
||||
/**
|
||||
* Entity last update timestamp.
|
||||
+39
-47
@@ -3,9 +3,12 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Interface\UserOwnedInterface;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampableUpdationEntity;
|
||||
use App\Entity\Trait\MigratedEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Entity\Trait\TimestampedUpdationEntity;
|
||||
use App\Filter\OrderedLikeFilter;
|
||||
use App\Filter\UserQueryFilter;
|
||||
use App\Repository\UserRepository;
|
||||
@@ -37,10 +40,12 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')]
|
||||
#[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')]
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Index(fields: ['migratedId'])]
|
||||
class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampableUpdationEntity;
|
||||
use MigratedEntity;
|
||||
use TimestampedCreationEntity;
|
||||
use TimestampedUpdationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -94,8 +99,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
private ?bool $emailConfirmed = null;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'user', cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\OneToOne(inversedBy: 'user', cascade: ['persist'])]
|
||||
private ?Accounting $accounting = null;
|
||||
|
||||
/**
|
||||
@@ -124,20 +128,6 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $name = null;
|
||||
|
||||
/**
|
||||
* User was migrated from Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column]
|
||||
private ?bool $migrated = null;
|
||||
|
||||
/**
|
||||
* The previous id of this User in the Goteo v3 platform.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $migratedReference = null;
|
||||
|
||||
/**
|
||||
* The projects owned by this User.
|
||||
*/
|
||||
@@ -145,9 +135,18 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: Project::class)]
|
||||
private Collection $projects;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])]
|
||||
private ?UserPersonal $personalData = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->migrated = false;
|
||||
$accounting = new Accounting();
|
||||
$accounting->setOwner($this);
|
||||
|
||||
$this->accounting = $accounting;
|
||||
|
||||
$this->emailConfirmed = false;
|
||||
$this->active = false;
|
||||
|
||||
$this->tokens = new ArrayCollection();
|
||||
$this->projects = new ArrayCollection();
|
||||
@@ -285,7 +284,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
return $this->accounting;
|
||||
}
|
||||
|
||||
public function setAccounting(Accounting $accounting): static
|
||||
public function setAccounting(?Accounting $accounting): static
|
||||
{
|
||||
$this->accounting = $accounting;
|
||||
|
||||
@@ -358,30 +357,6 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMigrated(): ?bool
|
||||
{
|
||||
return $this->migrated;
|
||||
}
|
||||
|
||||
public function setMigrated(bool $migrated): static
|
||||
{
|
||||
$this->migrated = $migrated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMigratedReference(): ?string
|
||||
{
|
||||
return $this->migratedReference;
|
||||
}
|
||||
|
||||
public function setMigratedReference(?string $migratedReference): static
|
||||
{
|
||||
$this->migratedReference = $migratedReference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Project>
|
||||
*/
|
||||
@@ -411,4 +386,21 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPersonalData(): ?UserPersonal
|
||||
{
|
||||
return $this->personalData;
|
||||
}
|
||||
|
||||
public function setPersonalData(UserPersonal $personalData): static
|
||||
{
|
||||
// set the owning side of the relation if necessary
|
||||
if ($personalData->getUser() !== $this) {
|
||||
$personalData->setUser($this);
|
||||
}
|
||||
|
||||
$this->personalData = $personalData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Ambta\DoctrineEncryptBundle\Configuration\Encrypted;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\UserPersonalRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* UserPersonal is the detailed data of a User.
|
||||
*
|
||||
* The personal data of a user will be kept encrypted
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: UserPersonalRepository::class)]
|
||||
#[ApiResource]
|
||||
class UserPersonal
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\OneToOne(inversedBy: 'personalData', cascade: ['persist', 'remove'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $user = null;
|
||||
|
||||
/**
|
||||
* The Identity Document number of the User.
|
||||
*/
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Encrypted]
|
||||
private ?string $identityDocument = null;
|
||||
|
||||
/**
|
||||
* The type of indentity document that the User has, ie: Passport, DNI, NIE, etc.
|
||||
*/
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Encrypted]
|
||||
private ?string $identityDocumentType = null;
|
||||
|
||||
/**
|
||||
* The Postal Code of where the User lives.
|
||||
*/
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Encrypted]
|
||||
private ?string $postalCode = null;
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIdentityDocument(): ?string
|
||||
{
|
||||
return $this->identityDocument;
|
||||
}
|
||||
|
||||
public function setIdentityDocument(?string $identityDocument): static
|
||||
{
|
||||
$this->identityDocument = $identityDocument;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIndetityDocumentType(): ?string
|
||||
{
|
||||
return $this->identityDocumentType;
|
||||
}
|
||||
|
||||
public function setIndetityDocumentType(?string $identityDocumentType): static
|
||||
{
|
||||
$this->identityDocumentType = $identityDocumentType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPostalCode(): ?string
|
||||
{
|
||||
return $this->postalCode;
|
||||
}
|
||||
|
||||
public function setPostalCode(?string $postalCode): static
|
||||
{
|
||||
$this->postalCode = $postalCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace App\Entity;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Dto\UserTokenLoginDto;
|
||||
use App\Entity\Interface\UserOwnedInterface;
|
||||
use App\Entity\Trait\TimestampableCreationEntity;
|
||||
use App\Entity\Trait\TimestampedCreationEntity;
|
||||
use App\Repository\UserTokenRepository;
|
||||
use App\State\UserTokenLoginProcessor;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -25,7 +25,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\Entity(repositoryClass: UserTokenRepository::class)]
|
||||
class UserToken implements UserOwnedInterface
|
||||
{
|
||||
use TimestampableCreationEntity;
|
||||
use TimestampedCreationEntity;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\WalletFinancementRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* A Financement represents how much money was moved from incoming to outgoing Wallet Statements.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: WalletFinancementRepository::class)]
|
||||
class WalletFinancement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* The monetary value used in this Financement.
|
||||
*/
|
||||
#[ORM\Embedded(class: Money::class)]
|
||||
private ?Money $money = null;
|
||||
|
||||
/**
|
||||
* An incoming Statement that originally saved the money in this Financement.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'financesTo')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?WalletStatement $origin = null;
|
||||
|
||||
/**
|
||||
* An outgoing Statement that eventually spent the money in this Financement.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'financedBy')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?WalletStatement $target = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getMoney(): ?Money
|
||||
{
|
||||
return $this->money;
|
||||
}
|
||||
|
||||
public function setMoney(Money $money): static
|
||||
{
|
||||
$this->money = $money;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrigin(): ?WalletStatement
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function setOrigin(?WalletStatement $origin): static
|
||||
{
|
||||
$this->origin = $origin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTarget(): ?WalletStatement
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function setTarget(?WalletStatement $target): static
|
||||
{
|
||||
$this->target = $target;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Gateway\Wallet\StatementDirection;
|
||||
use App\Repository\WalletStatementRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* WalletStatements are used by the Wallet Gateway to track available balances and money movements.\
|
||||
* \
|
||||
* Statements are relative to a Transaction, which either:\
|
||||
* \
|
||||
* a. Targets an User's Accounting, for which an incoming Statement will be created and which will hold the money until it is spent.\
|
||||
* b. Originates from an User's Accounting via the Wallet Gateway, which will create an outgoing Statement with money financed from previous incoming statements.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: WalletStatementRepository::class)]
|
||||
class WalletStatement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\OneToOne(cascade: ['persist'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Transaction $transaction = null;
|
||||
|
||||
/**
|
||||
* An `incoming` direction Statement means the Transaction saved money into the wallet.\
|
||||
* An `outgoing` direction Statement means the Transaction spent money from the wallet.
|
||||
*/
|
||||
#[ORM\Column(enumType: StatementDirection::class)]
|
||||
private ?StatementDirection $direction = null;
|
||||
|
||||
/**
|
||||
* For incoming Statements, the balance tells the available money.\
|
||||
* i.e: money not used yet by outgoing Statements.
|
||||
*/
|
||||
#[ORM\Embedded(class: Money::class)]
|
||||
private ?Money $balance = null;
|
||||
|
||||
/**
|
||||
* A collection of Financements for outgoing Statements that were made using money from this Statement.
|
||||
*
|
||||
* Only not empty for incoming Statements.
|
||||
*
|
||||
* @var Collection<int, WalletFinancement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'origin', targetEntity: WalletFinancement::class, cascade: ['persist'])]
|
||||
private Collection $financesTo;
|
||||
|
||||
/**
|
||||
* A collection of Financements from incoming Statements that provided money for this Statement.
|
||||
*
|
||||
* Only not empty for outgoing Statements.
|
||||
*
|
||||
* @var Collection<int, WalletFinancement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'target', targetEntity: WalletFinancement::class, cascade: ['persist'])]
|
||||
private Collection $financedBy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->financesTo = new ArrayCollection();
|
||||
$this->financedBy = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTransaction(): ?Transaction
|
||||
{
|
||||
return $this->transaction;
|
||||
}
|
||||
|
||||
public function setTransaction(Transaction $transaction): static
|
||||
{
|
||||
$this->transaction = $transaction;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDirection(): ?StatementDirection
|
||||
{
|
||||
return $this->direction;
|
||||
}
|
||||
|
||||
public function hasDirection(StatementDirection $direction): bool
|
||||
{
|
||||
return $this->direction === $direction;
|
||||
}
|
||||
|
||||
public function setDirection(StatementDirection $direction): static
|
||||
{
|
||||
$this->direction = $direction;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBalance(): ?Money
|
||||
{
|
||||
return $this->balance;
|
||||
}
|
||||
|
||||
public function setBalance(Money $balance): static
|
||||
{
|
||||
$this->balance = $balance;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WalletFinancement>
|
||||
*/
|
||||
public function getFinancesTo(): Collection
|
||||
{
|
||||
return $this->financesTo;
|
||||
}
|
||||
|
||||
public function addFinancesTo(WalletFinancement $financesTo): static
|
||||
{
|
||||
if (!$this->financesTo->contains($financesTo)) {
|
||||
$this->financesTo->add($financesTo);
|
||||
$financesTo->setOrigin($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeFinancesTo(WalletFinancement $financesTo): static
|
||||
{
|
||||
if ($this->financesTo->removeElement($financesTo)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($financesTo->getOrigin() === $this) {
|
||||
$financesTo->setOrigin(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WalletFinancement>
|
||||
*/
|
||||
public function getFinancedBy(): Collection
|
||||
{
|
||||
return $this->financedBy;
|
||||
}
|
||||
|
||||
public function addFinancedBy(WalletFinancement $financedBy): static
|
||||
{
|
||||
if (!$this->financedBy->contains($financedBy)) {
|
||||
$this->financedBy->add($financedBy);
|
||||
$financedBy->setTarget($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeFinancedBy(WalletFinancement $financedBy): static
|
||||
{
|
||||
if ($this->financedBy->removeElement($financedBy)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($financedBy->getTarget() === $this) {
|
||||
$financedBy->setTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\User;
|
||||
use App\Gateway\Wallet\WalletService;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
event: Events::postPersist,
|
||||
method: 'processTransaction',
|
||||
entity: Transaction::class
|
||||
)]
|
||||
final class WalletTransactionsListener
|
||||
{
|
||||
public function __construct(
|
||||
private WalletService $wallet,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates an income statement for User-received Transactions.
|
||||
*/
|
||||
public function processTransaction(
|
||||
Transaction $transaction,
|
||||
PostPersistEventArgs $event,
|
||||
) {
|
||||
if (!$transaction->getTarget()->getOwner() instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
$income = $this->wallet->save($transaction);
|
||||
|
||||
if ($income->getId() !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getObjectManager()->persist($income);
|
||||
$event->getObjectManager()->flush();
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ final class OrderedLikeFilter extends AbstractFilter
|
||||
protected ManagerRegistry $managerRegistry,
|
||||
?LoggerInterface $logger = null,
|
||||
protected ?array $properties = null,
|
||||
protected ?NameConverterInterface $nameConverter = null
|
||||
protected ?NameConverterInterface $nameConverter = null,
|
||||
) {
|
||||
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ final class OrderedLikeFilter extends AbstractFilter
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
array $context = [],
|
||||
): void {
|
||||
if (
|
||||
!$this->isPropertyEnabled($property, $resourceClass)
|
||||
|
||||
@@ -31,7 +31,7 @@ final class ResourceVersionResourceFilter extends AbstractFilter
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
array $context = [],
|
||||
): void {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ final class ResourceVersionResourceIdFilter extends AbstractFilter
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
array $context = [],
|
||||
): void {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ final class UserQueryFilter extends AbstractFilter
|
||||
protected ManagerRegistry $managerRegistry,
|
||||
?LoggerInterface $logger = null,
|
||||
protected ?array $properties = null,
|
||||
protected ?NameConverterInterface $nameConverter = null
|
||||
protected ?NameConverterInterface $nameConverter = null,
|
||||
) {
|
||||
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ final class UserQueryFilter extends AbstractFilter
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
array $context = [],
|
||||
): void {
|
||||
if (
|
||||
!$this->isPropertyEnabled($property, $resourceClass)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Gateway;
|
||||
|
||||
enum GatewayChargeType: string
|
||||
enum ChargeType: string
|
||||
{
|
||||
/**
|
||||
* A one-time charge to the money payer.
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Gateway;
|
||||
|
||||
enum GatewayCheckoutStatus: string
|
||||
enum CheckoutStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Exception;
|
||||
|
||||
class DuplicateGatewayException extends \Exception
|
||||
{
|
||||
public const MESSAGE_DUPLICATE = 'Duplicate Gateway name \'%s\' from class %s, name is already in use by class %s';
|
||||
|
||||
public function __construct(
|
||||
string $duplicateName,
|
||||
string $duplicateClass,
|
||||
string $existingClass,
|
||||
) {
|
||||
parent::__construct(sprintf(
|
||||
self::MESSAGE_DUPLICATE,
|
||||
$duplicateName,
|
||||
$duplicateClass,
|
||||
$existingClass
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Exception;
|
||||
|
||||
class MissingGatewayException extends \Exception
|
||||
{
|
||||
public const MESSAGE_NOT_FOUND = 'Could not match \'%s\' to the name of any available Gateway';
|
||||
|
||||
public function __construct(string $missingName)
|
||||
{
|
||||
parent::__construct(\sprintf(self::MESSAGE_NOT_FOUND, $missingName));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
abstract class CashGateway implements GatewayInterface
|
||||
{
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
abstract class CecaGateway implements GatewayInterface
|
||||
{
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Gateway;
|
||||
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
abstract class DropGateway implements GatewayInterface
|
||||
{
|
||||
+15
-10
@@ -1,20 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway;
|
||||
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Gateway interfaces are in charge of connecting with payment gateways to send them the data in `GatewayCheckout` instances.\
|
||||
* Gateway interfaces are in charge of connecting with payment gateways to send them the data in `Checkout` instances.\
|
||||
* \
|
||||
* The flow is:
|
||||
* 1. A client app creates a `GatewayCheckout` record
|
||||
* 2. `App\State\GatewayCheckoutProcessor` calls the `sendData` method of the respective interface for the new `GatewayCheckout`
|
||||
* 3. The interface connects with the gateway and sends the data for the `GatewayCheckout`
|
||||
* 4. The interface updates the `GatewayCheckout` with the checkout url and reference given by the gateway for the checkout session
|
||||
* 1. A client app creates a `Checkout` record
|
||||
* 2. `App\State\GatewayCheckoutProcessor` calls the `sendData` method of the respective interface for the new `Checkout`
|
||||
* 3. The interface connects with the gateway and sends the data for the `Checkout`
|
||||
* 4. The interface updates the `Checkout` with the checkout url and reference given by the gateway for the checkout session
|
||||
* 5. `App\State\GatewayCheckoutProcessor` returns the response data to the client app
|
||||
* 6. The client app redirects the user to the payment with the gateway
|
||||
* 7. The gateway processes the payment and redirects the user back to v4
|
||||
@@ -28,14 +28,19 @@ interface GatewayInterface
|
||||
*/
|
||||
public static function getName(): string;
|
||||
|
||||
/**
|
||||
* @return \App\Gateway\ChargeType[] The charge types that can be processed by this Gateway
|
||||
*/
|
||||
public static function getSupportedChargeTypes(): array;
|
||||
|
||||
/**
|
||||
* Connects with the payment gateway and creates a checkout session so the gateway can process the payment.
|
||||
*
|
||||
* @param GatewayCheckout $checkout The GatewayCheckout with the data for the payment to be charged
|
||||
* @param Checkout $checkout The Checkout with the data for the payment to be charged
|
||||
*
|
||||
* @return GatewayCheckout The GatewayCheckout updated with the data given by the gateway
|
||||
* @return Checkout The Checkout updated with the data given by the gateway
|
||||
*/
|
||||
public function sendData(GatewayCheckout $checkout): GatewayCheckout;
|
||||
public function process(Checkout $checkout): Checkout;
|
||||
|
||||
/**
|
||||
* When a user is redirected by the gateway we must handle the redirection
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway;
|
||||
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Gateway\Exception\DuplicateGatewayException;
|
||||
use App\Gateway\Exception\MissingGatewayException;
|
||||
|
||||
class GatewayLocator
|
||||
{
|
||||
/** @var GatewayInterface[] */
|
||||
private array $gatewaysByName = [];
|
||||
|
||||
/** @var GatewayInterface[] */
|
||||
private array $gatewaysByClass = [];
|
||||
|
||||
public function __construct(iterable $instanceof)
|
||||
{
|
||||
foreach (\iterator_to_array($instanceof) as $key => $gateway) {
|
||||
$this->gatewaysByClass[$gateway::class] = $gateway;
|
||||
}
|
||||
|
||||
self::validateGatewayNames($this->gatewaysByClass);
|
||||
|
||||
foreach ($this->gatewaysByClass as $class => $gateway) {
|
||||
$this->gatewaysByName[$gateway::getName()] = $gateway;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <string, GatewayInterface>
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->gatewaysByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the Gateway interface implementation
|
||||
*
|
||||
* @throws \Exception When the `$name` does not match to that of an implemented Gateway
|
||||
*/
|
||||
public function get(string $name): GatewayInterface
|
||||
{
|
||||
if (!\array_key_exists($name, $this->gatewaysByName)) {
|
||||
throw new MissingGatewayException($name);
|
||||
}
|
||||
|
||||
return $this->gatewaysByName[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway
|
||||
*/
|
||||
public function getForCheckout(Checkout $checkout): GatewayInterface
|
||||
{
|
||||
return $this->get($checkout->getGatewayName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the gateway names are unique for each gateway.
|
||||
*
|
||||
* @param array $gatewayClasses Fully-qualified Gateway class names
|
||||
*
|
||||
* @throws \Exception If there are two different Gateway classes that return the same name string
|
||||
*/
|
||||
private static function validateGatewayNames(array $gatewayClasses): void
|
||||
{
|
||||
$gatewaysValidated = [];
|
||||
foreach ($gatewayClasses as $gatewayClass) {
|
||||
$gatewayName = $gatewayClass::getName();
|
||||
|
||||
if (\array_key_exists($gatewayName, $gatewaysValidated)) {
|
||||
throw new DuplicateGatewayException(
|
||||
$gatewayName,
|
||||
$gatewayClass::class,
|
||||
$gatewaysValidated[$gatewayName]::class
|
||||
);
|
||||
}
|
||||
|
||||
$gatewaysValidated[$gatewayName] = $gatewayClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway;
|
||||
|
||||
class Link
|
||||
{
|
||||
/**
|
||||
* The complete target URL.
|
||||
*/
|
||||
public string $href;
|
||||
|
||||
/**
|
||||
* The link relation type, which serves as an ID for a link that unambiguously describes the semantics of the link.
|
||||
*
|
||||
* @see https://www.iana.org/assignments/link-relations/link-relations.xhtml
|
||||
*/
|
||||
public string $rel;
|
||||
|
||||
/**
|
||||
* The HTTP method required to make the related call.
|
||||
*/
|
||||
public string $method;
|
||||
|
||||
/**
|
||||
* The type of the link indicates who is the intended user of a link.\
|
||||
* `debug` links are for developers and platform maintainers to get useful information about the checkout.\
|
||||
* `payment` links are for end-users who must visit this link to complete the checkout.
|
||||
*/
|
||||
public LinkType $type;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway;
|
||||
|
||||
enum LinkType: string
|
||||
{
|
||||
/**
|
||||
* A 'debug' type link indicates a link that is helpful to developers and platform maintainers to get info about the checkout.
|
||||
*/
|
||||
case Debug = 'debug';
|
||||
|
||||
/**
|
||||
* A 'payment' type link indicates a link where the checkout is available to an end user for payment.
|
||||
*/
|
||||
case Payment = 'payment';
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Paypal;
|
||||
|
||||
use ApiPlatform\Api\IriConverterInterface;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\LinkType;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Brick\Money\Money as BrickMoney;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @see https://developer.paypal.com/studio/checkout/standard/integrate
|
||||
*/
|
||||
class PaypalGateway implements GatewayInterface
|
||||
{
|
||||
public const PAYPAL_API_ADDRESS_LIVE = 'https://api-m.paypal.com';
|
||||
public const PAYPAL_API_ADDRESS_SANDBOX = 'https://api-m.sandbox.paypal.com';
|
||||
|
||||
/** @see https://developer.paypal.com/docs/api/orders/v2/#orders_get!c=200&path=status&t=response */
|
||||
public const PAYPAL_STATUS_APPROVED = 'APPROVED';
|
||||
public const PAYPAL_STATUS_COMPLETED = 'COMPLETED';
|
||||
|
||||
/** @see https://developer.paypal.com/api/rest/webhooks/event-names/#orders */
|
||||
public const PAYPAL_EVENT_ORDER_COMPLETED = 'CHECKOUT.ORDER.COMPLETED';
|
||||
|
||||
/**
|
||||
* @see https://developer.paypal.com/docs/api/orders/v2/
|
||||
*/
|
||||
private const PAYPAL_ORDER_INTENT = 'CAPTURE';
|
||||
|
||||
public const TRACKING_TITLE_ORDER = 'PayPal Order ID';
|
||||
public const TRACKING_TITLE_TRANSACTION = 'PayPal Transaction ID';
|
||||
|
||||
public function __construct(
|
||||
private PaypalService $paypal,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private IriConverterInterface $iriConverter,
|
||||
private CheckoutService $checkoutService,
|
||||
private CheckoutRepository $checkoutRepository,
|
||||
) {}
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'paypal';
|
||||
}
|
||||
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
return [
|
||||
ChargeType::Single,
|
||||
];
|
||||
}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
$order = $this->paypal->postOrder([
|
||||
'intent' => self::PAYPAL_ORDER_INTENT,
|
||||
'purchase_units' => $this->getPaypalPurchaseUnits($checkout),
|
||||
'payment_source' => $this->getPaypalPaymentSource($checkout),
|
||||
]);
|
||||
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_ORDER;
|
||||
$tracking->value = $order['id'];
|
||||
|
||||
$checkout->addTracking($tracking);
|
||||
|
||||
foreach ($order['links'] as $linkData) {
|
||||
$linkType = \in_array($linkData['rel'], ['approve', 'payer-action'])
|
||||
? LinkType::Payment
|
||||
: LinkType::Debug;
|
||||
|
||||
$link = new Link();
|
||||
$link->href = $linkData['href'];
|
||||
$link->rel = $linkData['rel'];
|
||||
$link->method = $linkData['method'];
|
||||
$link->type = $linkType;
|
||||
|
||||
$checkout->addLink($link);
|
||||
}
|
||||
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
public function handleRedirect(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) {
|
||||
throw new \Exception(sprintf('Checkout was not completed successfully.'));
|
||||
}
|
||||
|
||||
$checkoutId = $request->query->get('checkoutId');
|
||||
|
||||
$checkout = $this->checkoutRepository->find($checkoutId);
|
||||
if ($checkout === null) {
|
||||
throw new \Exception(sprintf("Checkout '%s' could not be found.", $checkoutId));
|
||||
}
|
||||
|
||||
if ($checkout->getStatus() === CheckoutStatus::Charged) {
|
||||
return new RedirectResponse($this->iriConverter->getIriFromResource($checkout));
|
||||
}
|
||||
|
||||
$orderId = $request->query->get('token');
|
||||
if (!$orderId) {
|
||||
throw new \Exception(sprintf('PayPal checkout order ID not provided by the gateway.'));
|
||||
}
|
||||
|
||||
$order = $this->paypal->getOrder($orderId);
|
||||
if ($order['status'] !== self::PAYPAL_STATUS_APPROVED) {
|
||||
throw new \Exception(sprintf("PayPal checkout '%s' has not yet been processed successfully by the gateway.", $orderId));
|
||||
}
|
||||
|
||||
$capture = $this->paypal->captureOrderPayment($order);
|
||||
if ($capture['status'] !== self::PAYPAL_STATUS_COMPLETED) {
|
||||
throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was not completed.", $orderId));
|
||||
}
|
||||
|
||||
foreach ($capture['purchase_units'] as $purchaseUnit) {
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$tracking->value = $purchaseUnit['payments']['captures'][0]['id'];
|
||||
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
|
||||
$this->entityManager->persist($checkout);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// TO-DO: This should redirect the user to a GUI
|
||||
return new RedirectResponse($this->iriConverter->getIriFromResource($checkout));
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$event = $this->paypal->verifyWebhook($request);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse([
|
||||
'status' => 'ERROR',
|
||||
'message' => $e->getMessage(),
|
||||
'requestBody' => $request->getContent(),
|
||||
'requestHeaders' => $request->headers->all(),
|
||||
], Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
switch ($event['event_type']) {
|
||||
case self::PAYPAL_EVENT_ORDER_COMPLETED:
|
||||
return $this->handleOrderCompleted($event);
|
||||
default:
|
||||
return new Response('Event not supported', Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
private function handleOrderCompleted(array $event)
|
||||
{
|
||||
$orderId = $event['resource']['id'];
|
||||
|
||||
$checkout = $this->checkoutRepository->findOneByTracking(self::TRACKING_TITLE_ORDER, $orderId);
|
||||
|
||||
if ($checkout === null) {
|
||||
throw new \Exception(sprintf("Could not find any Checkout by the Tracking '%s'", $orderId), 1);
|
||||
}
|
||||
|
||||
foreach ($event['resource']['purchase_units'] as $purchaseUnit) {
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$tracking->value = $purchaseUnit['payments']['captures'][0]['id'];
|
||||
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
}
|
||||
|
||||
private function getPaypalMoney(Charge $charge): array
|
||||
{
|
||||
$brick = BrickMoney::ofMinor(
|
||||
$charge->getMoney()->amount,
|
||||
$charge->getMoney()->currency
|
||||
);
|
||||
|
||||
return [
|
||||
'value' => $brick->getAmount()->__toString(),
|
||||
'currency_code' => $brick->getCurrency()->getCurrencyCode(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getPaypalPurchaseUnits(Checkout $checkout): array
|
||||
{
|
||||
$units = [];
|
||||
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
$money = $this->getPaypalMoney($charge);
|
||||
$reference = $this->checkoutService->generateTracking($checkout, $charge);
|
||||
|
||||
$units[] = [
|
||||
'reference_id' => $reference,
|
||||
'custom_id' => $reference,
|
||||
'items' => [
|
||||
[
|
||||
'name' => $charge->getTitle(),
|
||||
'description' => $charge->getDescription(),
|
||||
'quantity' => '1',
|
||||
'unit_amount' => [
|
||||
...$money,
|
||||
],
|
||||
],
|
||||
],
|
||||
'amount' => [
|
||||
...$money,
|
||||
'breakdown' => [
|
||||
'item_total' => [
|
||||
...$money,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $units;
|
||||
}
|
||||
|
||||
private function getPaypalPaymentSource(Checkout $checkout): array
|
||||
{
|
||||
return [
|
||||
'paypal' => [
|
||||
'experience_context' => [
|
||||
'return_url' => $this->checkoutService->generateRedirectUrl($checkout),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Paypal;
|
||||
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class PaypalService
|
||||
{
|
||||
private CacheInterface $cache;
|
||||
|
||||
public function __construct(
|
||||
private string $paypalApiAddress,
|
||||
private string $paypalClientId,
|
||||
private string $paypalClientSecret,
|
||||
private string $paypalWebhookId,
|
||||
private HttpClientInterface $httpClient,
|
||||
) {
|
||||
$this->cache = new FilesystemAdapter(\preg_replace(
|
||||
'/[^-+_\.A-Za-z0-9]/',
|
||||
'-',
|
||||
$this->paypalApiAddress
|
||||
));
|
||||
|
||||
$httpOptions = new HttpOptions();
|
||||
$httpOptions->setBaseUri($paypalApiAddress);
|
||||
|
||||
$this->setHttpClient($httpClient->withOptions($httpOptions->toArray()));
|
||||
}
|
||||
|
||||
public function setHttpClient(HttpClientInterface $httpClient): static
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the PayPal API to generate an OAuth2 token.
|
||||
*
|
||||
* @return array The API response body
|
||||
*/
|
||||
public function generateAuthToken(): array
|
||||
{
|
||||
$response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [
|
||||
'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret],
|
||||
'body' => 'grant_type=client_credentials',
|
||||
]);
|
||||
|
||||
return $response->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one.
|
||||
*
|
||||
* @return array The OAuth2 token data
|
||||
*/
|
||||
public function getAuthToken(): array
|
||||
{
|
||||
return $this->cache->get(urlencode('/v1/oauth2/token'), function (ItemInterface $item): array {
|
||||
$tokenData = $this->generateAuthToken();
|
||||
|
||||
$item->expiresAfter($tokenData['expires_in']);
|
||||
|
||||
return $tokenData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PayPal Order resource with the given data.
|
||||
*
|
||||
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_create
|
||||
*/
|
||||
public function postOrder(array $order): array
|
||||
{
|
||||
$response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [
|
||||
'auth_bearer' => $this->getAuthToken()['access_token'],
|
||||
'json' => $order,
|
||||
]);
|
||||
|
||||
$content = \json_decode($response->getContent(), true);
|
||||
if (!\in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_CREATED])) {
|
||||
throw new \Exception($content['message']);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a PayPal Order resource.
|
||||
*
|
||||
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_get
|
||||
*/
|
||||
public function getOrder(string $orderId): array
|
||||
{
|
||||
$request = $this->httpClient->request(
|
||||
Request::METHOD_GET,
|
||||
sprintf('/v2/checkout/orders/%s', $orderId),
|
||||
[
|
||||
'auth_bearer' => $this->getAuthToken()['access_token'],
|
||||
]
|
||||
);
|
||||
|
||||
if ($request->getStatusCode() !== Response::HTTP_OK) {
|
||||
throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId));
|
||||
}
|
||||
|
||||
return \json_decode($request->getContent(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process post user-approval payment capture for a given Order.
|
||||
*
|
||||
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture
|
||||
*/
|
||||
public function captureOrderPayment(array $order): array
|
||||
{
|
||||
$link = \array_filter($order['links'], function ($order) {
|
||||
return $order['rel'] === 'capture';
|
||||
});
|
||||
|
||||
if (empty($link)) {
|
||||
throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id']));
|
||||
}
|
||||
|
||||
$link = \array_pop($link);
|
||||
$request = $this->httpClient->request(
|
||||
$link['method'],
|
||||
$link['href'],
|
||||
[
|
||||
'auth_bearer' => $this->getAuthToken()['access_token'],
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
]
|
||||
);
|
||||
|
||||
if ($request->getStatusCode() !== Response::HTTP_CREATED) {
|
||||
throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id']));
|
||||
}
|
||||
|
||||
return \json_decode($request->getContent(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a PayPal webhook request.
|
||||
*
|
||||
* @param Request $request The webhook request
|
||||
*
|
||||
* @return array The webhook event data
|
||||
*
|
||||
* @throws \Exception If the verification fails
|
||||
*/
|
||||
public function verifyWebhook(Request $request): array
|
||||
{
|
||||
$headers = $request->headers;
|
||||
|
||||
$isSignatureValid = \openssl_verify(
|
||||
implode('|', [
|
||||
$headers->get('paypal-transmission-id'),
|
||||
$headers->get('paypal-transmission-type'),
|
||||
$this->paypalWebhookId,
|
||||
\crc32($request->getContent()),
|
||||
]),
|
||||
\base64_decode($headers->get('paypal-transmission-sig')),
|
||||
\openssl_pkey_get_public(\file_get_contents($headers->get('paypal-cert-url')))
|
||||
) === 1;
|
||||
|
||||
if (!$isSignatureValid) {
|
||||
throw new \Exception('Could not verify PayPal webhook signature.');
|
||||
}
|
||||
|
||||
return \json_decode($request->getContent(), true);
|
||||
}
|
||||
}
|
||||
+64
-57
@@ -1,14 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
namespace App\Gateway\Stripe;
|
||||
|
||||
use ApiPlatform\Api\IriConverterInterface;
|
||||
use App\Controller\GatewaysController;
|
||||
use App\Entity\AccountingTransaction;
|
||||
use App\Entity\GatewayChargeType;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\GatewayCheckoutStatus;
|
||||
use App\Repository\GatewayCheckoutRepository;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\User;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use App\Gateway\Link;
|
||||
use App\Gateway\LinkType;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Stripe\Checkout\Session as StripeSession;
|
||||
use Stripe\StripeClient;
|
||||
@@ -17,21 +21,20 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class StripeGateway implements GatewayInterface
|
||||
{
|
||||
private const RESPONSE_TYPE_SUCCESS = 'success';
|
||||
public const TRACKING_TITLE_CHECKOUT = 'Stripe Checkout Session ID';
|
||||
|
||||
private StripeClient $stripe;
|
||||
|
||||
public function __construct(
|
||||
private string $stripeApiKey,
|
||||
private string $stripeWebhookSecret,
|
||||
private RouterInterface $router,
|
||||
private GatewayCheckoutRepository $gatewayCheckoutRepository,
|
||||
private CheckoutService $checkoutService,
|
||||
private CheckoutRepository $checkoutRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private IriConverterInterface $iriConverter
|
||||
private IriConverterInterface $iriConverter,
|
||||
) {
|
||||
$this->stripe = new StripeClient($stripeApiKey);
|
||||
}
|
||||
@@ -41,52 +44,41 @@ class StripeGateway implements GatewayInterface
|
||||
return 'stripe';
|
||||
}
|
||||
|
||||
public function sendData(GatewayCheckout $checkout): GatewayCheckout
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
$successUrl = $this->router->generate(
|
||||
GatewaysController::REDIRECT,
|
||||
[
|
||||
'type' => self::RESPONSE_TYPE_SUCCESS,
|
||||
'gateway' => $this->getName(),
|
||||
],
|
||||
RouterInterface::ABSOLUTE_URL
|
||||
);
|
||||
return [
|
||||
ChargeType::Single,
|
||||
ChargeType::Recurring,
|
||||
];
|
||||
}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
$session = $this->stripe->checkout->sessions->create([
|
||||
'customer_email' => $checkout->getOrigin()->getUser()->getEmail(),
|
||||
'customer_email' => $this->getStripeCustomer($checkout),
|
||||
'mode' => $this->getStripeMode($checkout),
|
||||
'line_items' => $this->getStripeLineItems($checkout),
|
||||
// Because Symfony's Router encodes query parameters, the value {CHECKOUT_SESSION_ID}
|
||||
// is not properly sent to Stripe and the redirection fails,
|
||||
// that's why we add the session_id template variable like this.
|
||||
// https://docs.stripe.com/payments/checkout/custom-success-page?lang=php#modify-the-success-url
|
||||
'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $successUrl),
|
||||
'success_url' => sprintf('%s&session_id={CHECKOUT_SESSION_ID}', $this->checkoutService->generateRedirectUrl($checkout)),
|
||||
]);
|
||||
|
||||
$checkout->setCheckoutUrl($session->url);
|
||||
$checkout->setGatewayReference($session->id);
|
||||
$link = new Link();
|
||||
|
||||
return $checkout;
|
||||
}
|
||||
$link->href = $session->url;
|
||||
$link->rel = 'approve';
|
||||
$link->method = Request::METHOD_GET;
|
||||
$link->type = LinkType::Payment;
|
||||
|
||||
private function handleSuccess(GatewayCheckout $checkout): GatewayCheckout
|
||||
{
|
||||
$checkout->setStatus(GatewayCheckoutStatus::Charged);
|
||||
$checkout->addLink($link);
|
||||
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
$transaction = new AccountingTransaction();
|
||||
$transaction->setMoney($charge->getMoney());
|
||||
$transaction->setOrigin($checkout->getOrigin());
|
||||
$transaction->setTarget($charge->getTarget());
|
||||
$tracking = new Tracking();
|
||||
$tracking->title = self::TRACKING_TITLE_CHECKOUT;
|
||||
$tracking->value = $session->id;
|
||||
|
||||
$this->entityManager->persist($transaction);
|
||||
|
||||
$charge->setTransaction($transaction);
|
||||
|
||||
$this->entityManager->persist($charge);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
$checkout->addTracking($tracking);
|
||||
|
||||
return $checkout;
|
||||
}
|
||||
@@ -96,15 +88,17 @@ class StripeGateway implements GatewayInterface
|
||||
$sessionId = $request->query->get('session_id');
|
||||
|
||||
$session = $this->stripe->checkout->sessions->retrieve($sessionId);
|
||||
$checkout = $this->gatewayCheckoutRepository->findOneBy(
|
||||
['gatewayReference' => $sessionId]
|
||||
);
|
||||
$checkout = $this->checkoutRepository->find($request->query->get('checkoutId'));
|
||||
|
||||
if ($checkout === null) {
|
||||
throw new \Exception(sprintf("Stripe checkout '%s' exists but no GatewayCheckout with that reference was found.", $sessionId));
|
||||
throw new \Exception(sprintf("Stripe checkout '%s' exists but no Checkout with that reference was found.", $sessionId));
|
||||
}
|
||||
|
||||
if ($request->query->get('type') !== self::RESPONSE_TYPE_SUCCESS) {
|
||||
if ($checkout->getStatus() === CheckoutStatus::Charged) {
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
if ($request->query->get('type') !== CheckoutService::RESPONSE_TYPE_SUCCESS) {
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
@@ -112,7 +106,10 @@ class StripeGateway implements GatewayInterface
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
$checkout = $this->handleSuccess($checkout);
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
|
||||
$this->entityManager->persist($checkout);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// TO-DO: This should redirect the user to a GUI
|
||||
return new RedirectResponse($this->iriConverter->getIriFromResource($checkout));
|
||||
@@ -135,10 +132,20 @@ class StripeGateway implements GatewayInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function getStripeMode(GatewayCheckout $checkout): string
|
||||
private function getStripeCustomer(Checkout $checkout): string
|
||||
{
|
||||
$owner = $checkout->getOrigin()->getOwner();
|
||||
if (!$owner instanceof User) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $owner->getEmail();
|
||||
}
|
||||
|
||||
private function getStripeMode(Checkout $checkout): string
|
||||
{
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
if ($charge->getType() === GatewayChargeType::Recurring) {
|
||||
if ($charge->getType() === ChargeType::Recurring) {
|
||||
return StripeSession::MODE_SUBSCRIPTION;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +153,7 @@ class StripeGateway implements GatewayInterface
|
||||
return StripeSession::MODE_PAYMENT;
|
||||
}
|
||||
|
||||
private function getStripeLineItems(GatewayCheckout $checkout): array
|
||||
private function getStripeLineItems(Checkout $checkout): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
@@ -154,13 +161,13 @@ class StripeGateway implements GatewayInterface
|
||||
$price = [
|
||||
'currency' => $charge->getMoney()->currency,
|
||||
'unit_amount' => $charge->getMoney()->amount,
|
||||
'product_data' => [
|
||||
'name' => $charge::MESSAGE_STATEMENT,
|
||||
'statement_descriptor' => $charge::MESSAGE_STATEMENT,
|
||||
],
|
||||
'product_data' => \array_filter([
|
||||
'name' => $charge->getTitle(),
|
||||
'statement_descriptor' => $charge->getDescription(),
|
||||
], fn($v) => $v !== null),
|
||||
];
|
||||
|
||||
if ($charge->getType() === GatewayChargeType::Recurring) {
|
||||
if ($charge->getType() === ChargeType::Recurring) {
|
||||
$price['recurring'] = ['interval' => 'month'];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Wallet;
|
||||
|
||||
enum StatementDirection: string
|
||||
{
|
||||
/**
|
||||
* The Transaction was received by the Accounting.
|
||||
*/
|
||||
case Incoming = 'incoming';
|
||||
|
||||
/**
|
||||
* The Transaction was issued by the Accounting.
|
||||
*/
|
||||
case Outgoing = 'outgoing';
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Wallet;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Money;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\GatewayInterface;
|
||||
use App\Library\Economy\MoneyService;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class WalletGateway implements GatewayInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'wallet';
|
||||
}
|
||||
|
||||
public static function getSupportedChargeTypes(): array
|
||||
{
|
||||
return [
|
||||
ChargeType::Single,
|
||||
];
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
private WalletService $wallet,
|
||||
private MoneyService $money,
|
||||
private CheckoutService $checkoutService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(Checkout $checkout): Checkout
|
||||
{
|
||||
$origin = $checkout->getOrigin();
|
||||
$available = $this->wallet->getBalance($origin);
|
||||
|
||||
if ($this->money->isLess($available, $this->getChargeTotal($checkout))) {
|
||||
throw new \Exception("Can't spend more than what you have!");
|
||||
}
|
||||
|
||||
$charges = $checkout->getCharges();
|
||||
foreach ($charges as $charge) {
|
||||
$transaction = new Transaction();
|
||||
$transaction->setMoney($charge->getMoney());
|
||||
$transaction->setOrigin($origin);
|
||||
$transaction->setTarget($charge->getTarget());
|
||||
|
||||
$expenditure = $this->wallet->spend($transaction);
|
||||
|
||||
$this->entityManager->persist($expenditure);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
|
||||
return $checkout;
|
||||
}
|
||||
|
||||
public function handleRedirect(Request $request): RedirectResponse
|
||||
{
|
||||
return new RedirectResponse('');
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request): Response
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
|
||||
private function getChargeTotal(Checkout $checkout): Money
|
||||
{
|
||||
$total = new Money(0, $checkout->getOrigin()->getCurrency());
|
||||
|
||||
$charges = $checkout->getCharges();
|
||||
foreach ($charges as $charge) {
|
||||
$total = $this->money->add($charge->getMoney(), $total);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Gateway\Wallet;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\WalletFinancement;
|
||||
use App\Entity\WalletStatement;
|
||||
use App\Library\Economy\MoneyService;
|
||||
use App\Repository\WalletStatementRepository;
|
||||
use Brick\Money as Brick;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class WalletService
|
||||
{
|
||||
public function __construct(
|
||||
private MoneyService $money,
|
||||
private WalletStatementRepository $statementRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return \App\Entity\WalletStatement[]
|
||||
*/
|
||||
public function getStatements(Accounting $accounting): array
|
||||
{
|
||||
return $this->statementRepository->findByAccounting($accounting);
|
||||
}
|
||||
|
||||
public function getBalance(Accounting $accounting): Money
|
||||
{
|
||||
$total = Brick\Money::ofMinor(0, $accounting->getCurrency());
|
||||
$statements = $this->statementRepository->findByTarget($accounting);
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
$balance = $this->money->toBrick($statement->getBalance());
|
||||
|
||||
$total = $total->plus($balance);
|
||||
}
|
||||
|
||||
return $this->money->toMoney($total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an `incoming` WalletStatement for the Transaction.
|
||||
*
|
||||
* @param Transaction $transaction The Transaction targetting a wallet
|
||||
*
|
||||
* @return WalletStatement An incoming statement for the transaction
|
||||
*/
|
||||
public function save(Transaction $transaction): WalletStatement
|
||||
{
|
||||
$statement = $this->statementRepository->findByTransaction($transaction);
|
||||
|
||||
if ($statement) {
|
||||
return $statement;
|
||||
}
|
||||
|
||||
$statement = new WalletStatement();
|
||||
$statement->setTransaction($transaction);
|
||||
$statement->setDirection(StatementDirection::Incoming);
|
||||
$statement->setBalance($transaction->getMoney());
|
||||
|
||||
return $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the money of a Transaction from the origin wallet.
|
||||
*
|
||||
* @param Transaction $transaction The Transaction originating from a wallet
|
||||
*
|
||||
* @return WalletStatement An outgoing statement financed by previous incoming statements
|
||||
*/
|
||||
public function spend(Transaction $transaction): WalletStatement
|
||||
{
|
||||
$origin = $transaction->getOrigin();
|
||||
|
||||
$spendGoal = $transaction->getMoney();
|
||||
$spentTotal = new Money(0, $spendGoal->currency);
|
||||
|
||||
$outgoing = new WalletStatement();
|
||||
$outgoing->setTransaction($transaction);
|
||||
$outgoing->setDirection(StatementDirection::Outgoing);
|
||||
|
||||
$statements = $this->getStatements($origin);
|
||||
foreach ($statements as $statement) {
|
||||
if ($spendGoal->amount === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$statement->hasDirection(StatementDirection::Incoming)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$incoming = $statement;
|
||||
$balance = $incoming->getBalance();
|
||||
|
||||
if ($balance->amount === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->money->isLess($spendGoal, $balance)) {
|
||||
$balanceSpent = $spendGoal;
|
||||
} else {
|
||||
$balanceSpent = $balance;
|
||||
}
|
||||
|
||||
$financement = new WalletFinancement();
|
||||
$financement->setMoney($balanceSpent);
|
||||
|
||||
$incoming->setBalance($this->money->substract($balanceSpent, $balance));
|
||||
$incoming->addFinancesTo($financement);
|
||||
|
||||
$outgoing->setBalance($this->money->add($balanceSpent, $spentTotal));
|
||||
$outgoing->addFinancedBy($financement);
|
||||
|
||||
$this->entityManager->persist($incoming);
|
||||
|
||||
$spendGoal = $this->money->substract($balanceSpent, $spendGoal);
|
||||
$spentTotal = $this->money->add($balanceSpent, $spentTotal);
|
||||
}
|
||||
|
||||
return $outgoing;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\DependencyInjection\Compiler\GatewaysCompilerPass;
|
||||
use App\DependencyInjection\Compiler\VersionedResourcePass;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
@@ -15,11 +14,6 @@ class Kernel extends BaseKernel
|
||||
|
||||
protected function build(ContainerBuilder $container): void
|
||||
{
|
||||
$container->addCompilerPass(
|
||||
new GatewaysCompilerPass(),
|
||||
PassConfig::TYPE_AFTER_REMOVING
|
||||
);
|
||||
|
||||
$container->addCompilerPass(
|
||||
new VersionedResourcePass(),
|
||||
PassConfig::TYPE_AFTER_REMOVING
|
||||
|
||||
@@ -11,7 +11,7 @@ class Benzina
|
||||
private array $availablePumps = [];
|
||||
|
||||
public function __construct(
|
||||
iterable $instanceof
|
||||
iterable $instanceof,
|
||||
) {
|
||||
$this->availablePumps = \iterator_to_array($instanceof);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class Benzina
|
||||
*
|
||||
* @return PumpInterface[]
|
||||
*/
|
||||
public function getPumps(StreamInterface $stream, int $sampleSize = 1): array
|
||||
public function getPumpsFor(StreamInterface $stream, int $sampleSize = 1): array
|
||||
{
|
||||
$sample = $stream->read($sampleSize);
|
||||
$stream->rewind();
|
||||
|
||||
@@ -4,10 +4,21 @@ namespace App\Library\Benzina\Pump;
|
||||
|
||||
abstract class AbstractPump implements PumpInterface
|
||||
{
|
||||
protected array $config;
|
||||
protected array $config = [];
|
||||
|
||||
public function configure(array $config = []): void
|
||||
public function setConfig(array $config = []): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if ($key !== null) {
|
||||
return \array_key_exists($key, $this->config)
|
||||
? $this->config[$key]
|
||||
: $default;
|
||||
}
|
||||
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,34 +2,42 @@
|
||||
|
||||
namespace App\Library\Benzina\Pump;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\AccountingTransaction;
|
||||
use App\Entity\GatewayCharge;
|
||||
use App\Entity\GatewayChargeType;
|
||||
use App\Entity\GatewayCheckout;
|
||||
use App\Entity\GatewayCheckoutStatus;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\Tipjar;
|
||||
use App\Gateway\ChargeType;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Gateway\Gateway\CashGateway;
|
||||
use App\Gateway\Gateway\CecaGateway;
|
||||
use App\Gateway\Gateway\DropGateway;
|
||||
use App\Gateway\Paypal\PaypalGateway;
|
||||
use App\Gateway\Stripe\StripeGateway;
|
||||
use App\Gateway\Tracking;
|
||||
use App\Gateway\Wallet\WalletGateway;
|
||||
use App\Library\Benzina\Pump\Trait\ArrayPumpTrait;
|
||||
use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait;
|
||||
use App\Library\Economy\Payment\CashGateway;
|
||||
use App\Library\Economy\Payment\CecaGateway;
|
||||
use App\Library\Economy\Payment\DropGateway;
|
||||
use App\Library\Economy\Payment\PaypalGateway;
|
||||
use App\Library\Economy\Payment\StripeGateway;
|
||||
use App\Library\Economy\Payment\WalletGateway;
|
||||
use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TipjarRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\Gateway\CheckoutService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use ProgressivePumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
|
||||
public const TRACKING_TITLE_V3 = 'v3 Invest ID';
|
||||
public const TRACKING_TITLE_PAYMENT = 'v3 Invest Payment';
|
||||
public const TRACKING_TITLE_TRANSACTION = 'v3 Invest Transaction';
|
||||
public const TRACKING_TITLE_PREAPPROVAL = 'v3 Invest Preapproval';
|
||||
|
||||
public const CHARGE_TITLE_PROJECT = 'Pago en Goteo v3 - Donación a proyecto';
|
||||
public const CHARGE_TITLE_POOL = 'Pago en Goteo v3 - Carga de monedero';
|
||||
public const CHARGE_TITLE_TIP = 'Pago en Goteo v3 - Propina a la plataforma';
|
||||
|
||||
private const PLATFORM_TIPJAR_NAME = 'platform';
|
||||
private const CHECKOUT_URL_DEFAULT = 'https://www.goteo.org/invest';
|
||||
|
||||
private const MAX_INT = 2147483647;
|
||||
|
||||
@@ -69,34 +77,30 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
private UserRepository $userRepository,
|
||||
private ProjectRepository $projectRepository,
|
||||
private TipjarRepository $tipjarRepository,
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
}
|
||||
private EntityManagerInterface $entityManager,
|
||||
private CheckoutService $checkoutService,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $data): bool
|
||||
public function supports(mixed $batch): bool
|
||||
{
|
||||
if (!\is_array($data) || !\array_key_exists(0, $data)) {
|
||||
if (!\is_array($batch) || !\array_key_exists(0, $batch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasAllKeys($data[0], self::INVEST_KEYS);
|
||||
return $this->hasAllKeys($batch[0], self::INVEST_KEYS);
|
||||
}
|
||||
|
||||
public function process(mixed $data): void
|
||||
public function pump(mixed $batch): void
|
||||
{
|
||||
$users = $this->getUsers($data);
|
||||
$projects = $this->getProjects($data);
|
||||
$batch = $this->skipPumped($batch, 'id', Checkout::class, 'migratedId');
|
||||
|
||||
$tipjar = $this->getPlatformTipjar();
|
||||
|
||||
$pumped = $this->getPumped(GatewayCheckout::class, $data, ['migratedReference' => 'id']);
|
||||
$users = $this->getPumpedUsers($batch);
|
||||
$projects = $this->getPumpedProjects($batch);
|
||||
|
||||
foreach ($data as $key => $record) {
|
||||
if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\array_key_exists($record['project'], $projects)) {
|
||||
foreach ($batch as $key => $record) {
|
||||
if (!$record['user'] || empty($record['user'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -109,54 +113,52 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
}
|
||||
|
||||
$user = $users[$record['user']];
|
||||
$project = $projects[$record['project']];
|
||||
|
||||
$checkout = new GatewayCheckout();
|
||||
$checkout = new Checkout();
|
||||
$checkout->setOrigin($user->getAccounting());
|
||||
$checkout->setStatus($this->getCheckoutStatus($record));
|
||||
$checkout->setGateway($this->getCheckoutGateway($record));
|
||||
$checkout->setGatewayReference($this->getCheckoutReference($record));
|
||||
$checkout->setCheckoutUrl(self::CHECKOUT_URL_DEFAULT);
|
||||
$checkout->setGatewayName($this->getCheckoutGateway($record));
|
||||
|
||||
foreach ($this->getCheckoutTrackings($record) as $tracking) {
|
||||
$checkout->addTracking($tracking);
|
||||
}
|
||||
|
||||
$checkout->setMigrated(true);
|
||||
$checkout->setMigratedReference($record['id']);
|
||||
$checkout->setMetadata([
|
||||
'payment' => $record['payment'],
|
||||
'transaction' => $record['transaction'],
|
||||
'preapproval' => $record['preapproval'],
|
||||
]);
|
||||
$checkout->setMigratedId($record['id']);
|
||||
|
||||
$checkout->setDateCreated(new \DateTime($record['invested']));
|
||||
$checkout->setDateUpdated(new \DateTime());
|
||||
|
||||
$charge = new GatewayCharge();
|
||||
$charge = new Charge();
|
||||
$charge->setType($this->getChargeType($record));
|
||||
$charge->setMoney($this->getChargeMoney($record['amount'], $record['currency']));
|
||||
$charge->setTarget($project->getAccounting());
|
||||
|
||||
if (empty($record['project'])) {
|
||||
$charge->setTitle(self::CHARGE_TITLE_POOL);
|
||||
$charge->setTarget($user->getAccounting());
|
||||
}
|
||||
|
||||
if (!empty($record['project'])) {
|
||||
$project = $projects[$record['project']];
|
||||
|
||||
$charge->setTitle(self::CHARGE_TITLE_PROJECT);
|
||||
$charge->setTarget($project->getAccounting());
|
||||
}
|
||||
|
||||
$checkout->addCharge($charge);
|
||||
|
||||
if ($record['donate_amount'] > 0) {
|
||||
$tip = new GatewayCharge();
|
||||
$tip->setType(GatewayChargeType::Single);
|
||||
$tip = new Charge();
|
||||
$tip->setType(ChargeType::Single);
|
||||
$tip->setTitle(self::CHARGE_TITLE_TIP);
|
||||
$tip->setMoney($this->getChargeMoney($record['donate_amount'], $record['currency']));
|
||||
$tip->setTarget($tipjar->getAccounting());
|
||||
|
||||
$this->entityManager->persist($tip);
|
||||
$checkout->addCharge($tip);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($charge);
|
||||
$checkout->addCharge($charge);
|
||||
|
||||
if ($checkout->getStatus() === GatewayCheckoutStatus::Charged) {
|
||||
foreach ($checkout->getCharges() as $charge) {
|
||||
$transaction = new AccountingTransaction();
|
||||
$transaction->setMoney($charge->getMoney());
|
||||
$transaction->setOrigin($checkout->getOrigin());
|
||||
$transaction->setTarget($charge->getTarget());
|
||||
|
||||
$charge->setTransaction($transaction);
|
||||
|
||||
$this->entityManager->persist($transaction);
|
||||
$this->entityManager->persist($charge);
|
||||
}
|
||||
if ($checkout->getStatus() === CheckoutStatus::Charged) {
|
||||
$checkout = $this->checkoutService->chargeCheckout($checkout);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($checkout);
|
||||
@@ -166,32 +168,38 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
|
||||
private function getUsers(array $data): array
|
||||
/**
|
||||
* @return array<string, \App\Entity\User>
|
||||
*/
|
||||
private function getPumpedUsers(array $batch): array
|
||||
{
|
||||
$users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($data) {
|
||||
return $data['user'];
|
||||
}, $data)]);
|
||||
$users = $this->userRepository->findBy(['migratedId' => \array_map(function ($batch) {
|
||||
return $batch['user'];
|
||||
}, $batch)]);
|
||||
|
||||
$usersByMigratedReference = [];
|
||||
$usersByMigratedId = [];
|
||||
foreach ($users as $user) {
|
||||
$usersByMigratedReference[$user->getMigratedReference()] = $user;
|
||||
$usersByMigratedId[$user->getMigratedId()] = $user;
|
||||
}
|
||||
|
||||
return $usersByMigratedReference;
|
||||
return $usersByMigratedId;
|
||||
}
|
||||
|
||||
private function getProjects(array $data): array
|
||||
/**
|
||||
* @return array<string, \App\Entity\Project>
|
||||
*/
|
||||
private function getPumpedProjects(array $batch): array
|
||||
{
|
||||
$projects = $this->projectRepository->findBy(['migratedReference' => \array_map(function ($data) {
|
||||
return $data['project'];
|
||||
}, $data)]);
|
||||
$projects = $this->projectRepository->findBy(['migratedId' => \array_map(function ($batch) {
|
||||
return $batch['project'];
|
||||
}, $batch)]);
|
||||
|
||||
$projectsByMigratedReference = [];
|
||||
$projectsByMigratedId = [];
|
||||
foreach ($projects as $project) {
|
||||
$projectsByMigratedReference[$project->getMigratedReference()] = $project;
|
||||
$projectsByMigratedId[$project->getMigratedId()] = $project;
|
||||
}
|
||||
|
||||
return $projectsByMigratedReference;
|
||||
return $projectsByMigratedId;
|
||||
}
|
||||
|
||||
private function getPlatformTipjar(): Tipjar
|
||||
@@ -205,23 +213,19 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
$tipjar = new Tipjar();
|
||||
$tipjar->setName(self::PLATFORM_TIPJAR_NAME);
|
||||
|
||||
$accounting = new Accounting();
|
||||
$accounting->setTipjar($tipjar);
|
||||
|
||||
$this->entityManager->persist($tipjar);
|
||||
$this->entityManager->persist($accounting);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $tipjar;
|
||||
}
|
||||
|
||||
private function getChargeType(array $record): GatewayChargeType
|
||||
private function getChargeType(array $record): ChargeType
|
||||
{
|
||||
if (\in_array($record['method'], ['stripe_subscription'])) {
|
||||
return GatewayChargeType::Recurring;
|
||||
return ChargeType::Recurring;
|
||||
}
|
||||
|
||||
return GatewayChargeType::Single;
|
||||
return ChargeType::Single;
|
||||
}
|
||||
|
||||
private function getChargeMoney(int $amount, string $currency): Money
|
||||
@@ -235,13 +239,13 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
return new Money($amount, $currency);
|
||||
}
|
||||
|
||||
private function getCheckoutStatus(array $record): GatewayCheckoutStatus
|
||||
private function getCheckoutStatus(array $record): CheckoutStatus
|
||||
{
|
||||
if ($record['status'] < 1) {
|
||||
return GatewayCheckoutStatus::Pending;
|
||||
return CheckoutStatus::Pending;
|
||||
}
|
||||
|
||||
return GatewayCheckoutStatus::Charged;
|
||||
return CheckoutStatus::Charged;
|
||||
}
|
||||
|
||||
private function getCheckoutGateway(array $record): string
|
||||
@@ -264,20 +268,41 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function getCheckoutReference(array $record): string
|
||||
/**
|
||||
* @return Tracking[]
|
||||
*/
|
||||
private function getCheckoutTrackings(array $record): array
|
||||
{
|
||||
$v3Tracking = new Tracking();
|
||||
$v3Tracking->title = self::TRACKING_TITLE_V3;
|
||||
$v3Tracking->value = $record['id'];
|
||||
|
||||
$trackings = [$v3Tracking];
|
||||
|
||||
if (!empty($record['payment'])) {
|
||||
return $record['payment'];
|
||||
$payment = new Tracking();
|
||||
$payment->title = self::TRACKING_TITLE_PAYMENT;
|
||||
$payment->value = $record['payment'];
|
||||
|
||||
$trackings[] = $payment;
|
||||
}
|
||||
|
||||
if (!empty($record['transaction'])) {
|
||||
return $record['transaction'];
|
||||
$transaction = new Tracking();
|
||||
$transaction->title = self::TRACKING_TITLE_TRANSACTION;
|
||||
$transaction->value = $record['transaction'];
|
||||
|
||||
$trackings[] = $transaction;
|
||||
}
|
||||
|
||||
if (!empty($record['preapproval'])) {
|
||||
return $record['preapproval'];
|
||||
$preapproval = new Tracking();
|
||||
$preapproval->title = self::TRACKING_TITLE_PREAPPROVAL;
|
||||
$preapproval->value = $record['preapproval'];
|
||||
|
||||
$trackings[] = $preapproval;
|
||||
}
|
||||
|
||||
return '';
|
||||
return $trackings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
namespace App\Library\Benzina\Pump;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectStatus;
|
||||
use App\Entity\User;
|
||||
use App\Library\Benzina\Pump\Trait\ArrayPumpTrait;
|
||||
use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait;
|
||||
use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class ProjectsPump extends AbstractPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use ProgressivePumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
|
||||
private const PROJECT_KEYS = [
|
||||
'id',
|
||||
@@ -96,31 +96,25 @@ class ProjectsPump extends AbstractPump implements PumpInterface
|
||||
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
}
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $data): bool
|
||||
public function supports(mixed $batch): bool
|
||||
{
|
||||
if (!\is_array($data) || !\array_key_exists(0, $data)) {
|
||||
if (!\is_array($batch) || !\array_key_exists(0, $batch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasAllKeys($data[0], self::PROJECT_KEYS);
|
||||
return $this->hasAllKeys($batch[0], self::PROJECT_KEYS);
|
||||
}
|
||||
|
||||
public function process(mixed $data): void
|
||||
public function pump(mixed $batch): void
|
||||
{
|
||||
$pumped = $this->getPumped(Project::class, $data, ['migratedReference' => 'id']);
|
||||
$owners = $this->getOwners($data);
|
||||
$batch = $this->skipPumped($batch, 'id', Project::class, 'migratedId');
|
||||
|
||||
foreach ($data as $key => $record) {
|
||||
$isPumped = $this->isPumped($record, $pumped, ['migratedReference' => 'id']);
|
||||
|
||||
if ($isPumped) {
|
||||
continue;
|
||||
}
|
||||
$owners = $this->getOwners($batch);
|
||||
|
||||
foreach ($batch as $key => $record) {
|
||||
if (!\array_key_exists($record['owner'], $owners)) {
|
||||
continue;
|
||||
}
|
||||
@@ -134,14 +128,11 @@ class ProjectsPump extends AbstractPump implements PumpInterface
|
||||
$project->setOwner($owners[$record['owner']]);
|
||||
$project->setStatus($this->getProjectStatus($record['status']));
|
||||
$project->setMigrated(true);
|
||||
$project->setMigratedReference($record['id']);
|
||||
$project->setMigratedId($record['id']);
|
||||
$project->setDateCreated(new \DateTime($record['created']));
|
||||
|
||||
$accounting = $this->getAccounting($record);
|
||||
$accounting->setProject($project);
|
||||
$project->setDateUpdated(new \DateTime());
|
||||
|
||||
$this->entityManager->persist($project);
|
||||
$this->entityManager->persist($accounting);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
@@ -151,15 +142,15 @@ class ProjectsPump extends AbstractPump implements PumpInterface
|
||||
/**
|
||||
* @return User[]
|
||||
*/
|
||||
private function getOwners(array $data): array
|
||||
private function getOwners(array $record): array
|
||||
{
|
||||
$users = $this->userRepository->findBy(['migratedReference' => \array_map(function ($data) {
|
||||
return $data['owner'];
|
||||
}, $data)]);
|
||||
$users = $this->userRepository->findBy(['migratedId' => \array_map(function ($record) {
|
||||
return $record['owner'];
|
||||
}, $record)]);
|
||||
|
||||
$owners = [];
|
||||
foreach ($users as $user) {
|
||||
$owners[$user->getMigratedReference()] = $user;
|
||||
$owners[$user->getMigratedId()] = $user;
|
||||
}
|
||||
|
||||
return $owners;
|
||||
|
||||
@@ -4,24 +4,34 @@ namespace App\Library\Benzina\Pump;
|
||||
|
||||
interface PumpInterface
|
||||
{
|
||||
/**
|
||||
* Determines if the data is supported by this pump.
|
||||
*
|
||||
* @param mixed $data The streamed records, e.g. rows from an user table
|
||||
*/
|
||||
public function supports(mixed $data): bool;
|
||||
|
||||
/**
|
||||
* Sets flexible configuration values for this pump.
|
||||
*
|
||||
* @param array $config the configuration array
|
||||
* @param array $config The configuration array
|
||||
*/
|
||||
public function configure(array $config = []): void;
|
||||
public function setConfig(array $config = []): void;
|
||||
|
||||
/**
|
||||
* Process the data to be pumped.
|
||||
* Read the configuration values for this pump.
|
||||
*
|
||||
* @param mixed $data The streamed records, e.g. rows from an user table
|
||||
* @param string|null $key A configuration array key to return
|
||||
* @param mixed|null $default Any value to return in case the configuration key does not exist
|
||||
*
|
||||
* @return array The configuration value at the specified key, or full keys and values if null
|
||||
*/
|
||||
public function process(mixed $data): void;
|
||||
public function getConfig(?string $key = null, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* Determines if a data batch is supported by this pump.
|
||||
*
|
||||
* @param mixed $batch A sample of the streamed records, e.g. rows from an user table
|
||||
*/
|
||||
public function supports(mixed $batch): bool;
|
||||
|
||||
/**
|
||||
* Pump a data batch into a final destination.
|
||||
*
|
||||
* @param mixed $batch The streamed records, e.g. rows from an user table
|
||||
*/
|
||||
public function pump(mixed $batch): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Benzina\Pump\Trait;
|
||||
|
||||
use App\Library\Benzina\Pump\PumpInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
trait DoctrinePumpTrait
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {
|
||||
if (!$this instanceof PumpInterface) {
|
||||
throw new \Exception('DoctrinePumpTrait can only be used with PumpInterface classes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip already pumped records from a pumping stream batch.
|
||||
*
|
||||
* @param array $batch The batch of records being pumped
|
||||
* @param array $batchKey The key in the batch records to match against the entity key
|
||||
* @param string $entityClass The class where the data is being pumped to
|
||||
* @param string $entityKey The property of the pumped to class to match against the batch key
|
||||
*
|
||||
* @return array The batch of records being pumped, sliced out of already pumped records
|
||||
*/
|
||||
public function skipPumped(
|
||||
array $batch,
|
||||
string $batchKey,
|
||||
string $entityClass,
|
||||
string $entityKey,
|
||||
): array {
|
||||
if (!$this->getConfig('skip-pumped')) {
|
||||
return $batch;
|
||||
}
|
||||
|
||||
$pumped = $this->entityManager
|
||||
->getRepository($entityClass)
|
||||
->findBy([$entityKey => \array_map(function (array $record) use ($batchKey) {
|
||||
return $record[$batchKey];
|
||||
}, $batch)]);
|
||||
|
||||
$pumpedKeys = [];
|
||||
foreach ($pumped as $key => $entity) {
|
||||
$entityKeyGetter = sprintf('get%s', \ucfirst($entityKey));
|
||||
|
||||
$pumpedKeys[] = $entity->$entityKeyGetter();
|
||||
}
|
||||
|
||||
return \array_filter($batch, function (array $record) use ($batchKey, $pumpedKeys) {
|
||||
return !\in_array($record[$batchKey], $pumpedKeys);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Benzina\Pump\Trait;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* A progressive pump behaves with idempotency with the pumping data.
|
||||
*
|
||||
* To do so the pump must check matching, already pumped, data for the currently pumping data and skip it.
|
||||
*
|
||||
* This is an example:
|
||||
* ```
|
||||
* $alreadyPumped = $this->getPumped(Entity::class, $data, ['entityId' => 'dataId']);
|
||||
* ...
|
||||
* if ($this->isPumped($record, $alreadyPumped)) continue;
|
||||
* ```
|
||||
*/
|
||||
trait ProgressivePumpTrait
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve already pumped data from the pumping data batch.
|
||||
*
|
||||
* @param string $entityClass The class to pump the data to
|
||||
* @param array $pumpingBatch The pumping data batch
|
||||
* @param array $matchCriteria An array with the matching criteria between the entity key and the data key
|
||||
*/
|
||||
public function getPumped(
|
||||
string $entityClass,
|
||||
array $pumpingBatch,
|
||||
array $matchCriteria
|
||||
): array {
|
||||
$repository = $this->entityManager->getRepository($entityClass);
|
||||
|
||||
$entityKey = \array_keys($matchCriteria)[0];
|
||||
$pumpingKey = $matchCriteria[$entityKey];
|
||||
|
||||
$pumped = $repository->findBy([$entityKey => \array_map(function ($data) use ($pumpingKey) {
|
||||
return $data[$pumpingKey];
|
||||
}, $pumpingBatch)]);
|
||||
|
||||
$pumpedByKey = [];
|
||||
foreach ($pumped as $pumped) {
|
||||
$getter = sprintf('get%s', ucfirst($entityKey));
|
||||
$pumpedByKey[$pumped->$getter()] = $pumped;
|
||||
}
|
||||
|
||||
return $pumpedByKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a pumping data record is already in a pumped data batch.
|
||||
*
|
||||
* @param array $pumpingRecord The data record to be pumped
|
||||
* @param array $pumpedBatch The pumped data batch
|
||||
* @param array $matchCriteria An array with the matching criteria between the entity key and the data key
|
||||
*/
|
||||
public function isPumped(
|
||||
array $pumpingRecord,
|
||||
array $pumpedBatch,
|
||||
array $matchCriteria
|
||||
): bool {
|
||||
if (
|
||||
isset($this->config)
|
||||
&& $this->config['progress'] === false
|
||||
&& \array_key_exists('progress', $this->config)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entityKey = \array_keys($matchCriteria)[0];
|
||||
$pumpingKey = $matchCriteria[$entityKey];
|
||||
|
||||
return \array_key_exists($pumpingRecord[$pumpingKey], $pumpedBatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the pumped record in a pumped data batch from a pumping data record.
|
||||
*
|
||||
* @param array $pumpingRecord The data record being pumped
|
||||
* @param array $pumpedBatch The pumped data batch
|
||||
* @param array $matchCriteria An array with the matching criteria between the entity key and the data key
|
||||
*/
|
||||
public function getPumpedRecord(
|
||||
array $pumpingRecord,
|
||||
array $pumpedBatch,
|
||||
array $matchCriteria
|
||||
): mixed {
|
||||
$entityKey = \array_keys($matchCriteria)[0];
|
||||
$pumpingKey = $matchCriteria[$entityKey];
|
||||
|
||||
if ($this->isPumped($pumpingRecord, $pumpedBatch, $matchCriteria)) {
|
||||
return $pumpedBatch[$pumpingRecord[$pumpingKey]];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,15 @@
|
||||
|
||||
namespace App\Library\Benzina\Pump;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\User;
|
||||
use App\Library\Benzina\Pump\Trait\ArrayPumpTrait;
|
||||
use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait;
|
||||
use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class UsersPump extends AbstractPump implements PumpInterface
|
||||
{
|
||||
use ArrayPumpTrait;
|
||||
use ProgressivePumpTrait;
|
||||
use DoctrinePumpTrait;
|
||||
|
||||
public const USER_KEYS = [
|
||||
'id',
|
||||
@@ -51,30 +50,29 @@ class UsersPump extends AbstractPump implements PumpInterface
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
}
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function supports(mixed $data): bool
|
||||
public function supports(mixed $batch): bool
|
||||
{
|
||||
if (!\is_array($data) || !\is_array($data[0])) {
|
||||
if (!\is_array($batch) || !\is_array($batch[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasAllKeys($data[0], self::USER_KEYS);
|
||||
return $this->hasAllKeys($batch[0], self::USER_KEYS);
|
||||
}
|
||||
|
||||
public function process(mixed $data): void
|
||||
public function pump(mixed $batch): void
|
||||
{
|
||||
$pumped = $this->getPumped(User::class, $data, ['migratedReference' => 'id']);
|
||||
$batch = $this->skipPumped($batch, 'id', User::class, 'migratedId');
|
||||
|
||||
foreach ($data as $key => $record) {
|
||||
if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) {
|
||||
continue;
|
||||
foreach ($batch as $key => $record) {
|
||||
$created = new \DateTime($record['created']);
|
||||
if ($created < new \DateTime('2000-01-01')) {
|
||||
$created = new \DateTime($record['modified']);
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setAccounting(new Accounting());
|
||||
$user->setUsername($this->getUsername($record));
|
||||
$user->setPassword($record['password'] ?? '');
|
||||
$user->setEmail($record['email']);
|
||||
@@ -82,13 +80,11 @@ class UsersPump extends AbstractPump implements PumpInterface
|
||||
$user->setName($record['name']);
|
||||
$user->setActive(false);
|
||||
$user->setMigrated(true);
|
||||
$user->setMigratedReference($record['id']);
|
||||
|
||||
$accounting = new Accounting();
|
||||
$accounting->setUser($user);
|
||||
$user->setMigratedId($record['id']);
|
||||
$user->setDateCreated($created);
|
||||
$user->setDateUpdated(new \DateTime());
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->persist($accounting);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
@@ -113,12 +109,12 @@ class UsersPump extends AbstractPump implements PumpInterface
|
||||
return $username;
|
||||
}
|
||||
|
||||
private function getUsername(array $data): string
|
||||
private function getUsername(array $record): string
|
||||
{
|
||||
$username = $this->normalizeUsername($data['id']);
|
||||
$username = $this->normalizeUsername($record['id']);
|
||||
|
||||
if (!$username) {
|
||||
$username = $this->normalizeUsername($data['email']);
|
||||
$username = $this->normalizeUsername($record['email']);
|
||||
}
|
||||
|
||||
return $username;
|
||||
|
||||
@@ -13,7 +13,7 @@ class PdoStream implements StreamInterface
|
||||
public function __construct(
|
||||
string $database,
|
||||
private string $tablename,
|
||||
private int $sizeOfBatch = 99
|
||||
private int $sizeOfBatch = 99,
|
||||
) {
|
||||
$parsedUrl = parse_url($database);
|
||||
$dbdata = [
|
||||
|
||||
@@ -125,11 +125,11 @@ class EuropeanCentralBankExchange implements ExchangeInterface
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function getConversion(MoneyContainer $money, string $currency): EntityMoney
|
||||
public function convert(MoneyContainer $money, string $toCurrency): EntityMoney
|
||||
{
|
||||
$converted = $this->converter->convert(
|
||||
$money,
|
||||
$currency,
|
||||
$toCurrency,
|
||||
null,
|
||||
RoundingMode::HALF_EVEN
|
||||
);
|
||||
@@ -140,14 +140,14 @@ class EuropeanCentralBankExchange implements ExchangeInterface
|
||||
);
|
||||
}
|
||||
|
||||
public function getConversionRate(string $source, string $target): float
|
||||
public function getConversionRate(string $fromCurrency, string $toCurrency): float
|
||||
{
|
||||
return $this->provider->getExchangeRate($source, $target)->toFloat();
|
||||
return $this->provider->getExchangeRate($fromCurrency, $toCurrency)->toFloat();
|
||||
}
|
||||
|
||||
public function getConversionDate(string $source, string $target): \DateTimeInterface
|
||||
public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface
|
||||
{
|
||||
$this->provider->getExchangeRate($source, $target);
|
||||
$this->provider->getExchangeRate($fromCurrency, $toCurrency);
|
||||
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Library\Economy\Currency;
|
||||
|
||||
use App\Entity\Money as EntityMoney;
|
||||
use App\Entity\Money;
|
||||
use Brick\Money\Exception\CurrencyConversionException;
|
||||
use Brick\Money\MoneyContainer;
|
||||
|
||||
@@ -19,32 +19,32 @@ interface ExchangeInterface
|
||||
public function getWeight(): int;
|
||||
|
||||
/**
|
||||
* @param MoneyContainer $money The money to be converted
|
||||
* @param string $currency The currency to convert to
|
||||
* @param MoneyContainer $money The money to be converted
|
||||
* @param string $toCurrency The currency to convert to
|
||||
*
|
||||
* @return EntityMoney The converted Money
|
||||
* @return Money The converted Money
|
||||
*
|
||||
* @throws CurrencyConversionException If the exchange rate is not available
|
||||
*/
|
||||
public function getConversion(MoneyContainer $money, string $currency): EntityMoney;
|
||||
public function convert(MoneyContainer $money, string $toCurrency): Money;
|
||||
|
||||
/**
|
||||
* @param string $source The currency to convert from
|
||||
* @param string $target The currency to convert to
|
||||
* @param string $fromCurrency The currency to convert from
|
||||
* @param string $toCurrency The currency to convert to
|
||||
*
|
||||
* @return float The rate of the conversion
|
||||
*
|
||||
* @throws CurrencyConversionException If the exchange rate is not available
|
||||
*/
|
||||
public function getConversionRate(string $source, string $target): float;
|
||||
public function getConversionRate(string $fromCurrency, string $toCurrency): float;
|
||||
|
||||
/**
|
||||
* @param string $source The currency to convert from
|
||||
* @param string $target The currency to convert to
|
||||
* @param string $fromCurrency The currency to convert from
|
||||
* @param string $toCurrency The currency to convert to
|
||||
*
|
||||
* @return \DateTimeInterface The date and time at which the rate was last updated
|
||||
*
|
||||
* @throws CurrencyConversionException If the exchange rate is not available
|
||||
*/
|
||||
public function getConversionDate(string $source, string $target): \DateTimeInterface;
|
||||
public function getConversionDate(string $fromCurrency, string $toCurrency): \DateTimeInterface;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy;
|
||||
|
||||
use App\Entity\Money;
|
||||
use App\Library\Economy\Currency\ExchangeLocator;
|
||||
use Brick\Money as Brick;
|
||||
|
||||
class MoneyService
|
||||
{
|
||||
public function __construct(
|
||||
private ExchangeLocator $exchangeLocator,
|
||||
) {}
|
||||
|
||||
public static function toMoney(Brick\Money $brick): Money
|
||||
{
|
||||
return new Money(
|
||||
$brick->getMinorAmount()->toInt(),
|
||||
$brick->getCurrency()->getCurrencyCode()
|
||||
);
|
||||
}
|
||||
|
||||
public static function toBrick(Money $money): Brick\Money
|
||||
{
|
||||
return Brick\Money::ofMinor($money->amount, $money->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `a` to `b`.
|
||||
*/
|
||||
public function add(Money $a, Money $b): Money
|
||||
{
|
||||
$a = $this->convert($a, $b->currency);
|
||||
$ab = self::toBrick($b)->plus($a);
|
||||
|
||||
return self::toMoney($ab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substracts `a` from `b`.
|
||||
*/
|
||||
public function substract(Money $a, Money $b): Money
|
||||
{
|
||||
$a = $this->convert($a, $b->currency);
|
||||
$ab = self::toBrick($b)->minus($a);
|
||||
|
||||
return self::toMoney($ab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares if one Money is of less value than other.
|
||||
*
|
||||
* @return bool `true` if `$money` is less than `$than`
|
||||
*/
|
||||
public function isLess(Money $money, Money $than): bool
|
||||
{
|
||||
$money = $this->convert($money, $than->currency);
|
||||
|
||||
return $money->isLessThan(self::toBrick($than));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares if one Money is of greater or equal value than other.
|
||||
*
|
||||
* @return bool `true` if `$money` is more than or same as `$than`
|
||||
*/
|
||||
public function isMoreOrSame(Money $money, Money $than): bool
|
||||
{
|
||||
$money = $this->convert($money, $than->currency);
|
||||
|
||||
return $money->isGreaterThanOrEqualTo(self::toBrick($than));
|
||||
}
|
||||
|
||||
private function convert(Money $money, string $toCurrency): Brick\Money
|
||||
{
|
||||
$fromCurrency = $money->currency;
|
||||
if ($fromCurrency === $toCurrency) {
|
||||
return self::toBrick($money);
|
||||
}
|
||||
|
||||
$exchange = $this->exchangeLocator->getExchangeFor($fromCurrency, $toCurrency);
|
||||
|
||||
return self::toBrick($exchange->convert(self::toBrick($money), $toCurrency));
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
|
||||
use App\DependencyInjection\Compiler\GatewaysCompilerPass;
|
||||
use App\Entity\GatewayCheckout;
|
||||
|
||||
class GatewayLocator
|
||||
{
|
||||
/** @var GatewayInterface[] */
|
||||
private array $gatewaysByName = [];
|
||||
|
||||
/** @var GatewayInterface[] */
|
||||
private array $gatewaysByClass = [];
|
||||
|
||||
public function __construct(iterable $instanceof)
|
||||
{
|
||||
foreach (\iterator_to_array($instanceof) as $key => $gateway) {
|
||||
$this->gatewaysByClass[$gateway::class] = $gateway;
|
||||
}
|
||||
|
||||
GatewaysCompilerPass::validateGatewayNames($this->gatewaysByClass);
|
||||
|
||||
foreach ($this->gatewaysByClass as $class => $gateway) {
|
||||
$this->gatewaysByName[$gateway::getName()] = $gateway;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string> List of the fully-qualified class names of the available interfaces
|
||||
*/
|
||||
public function getClasses(): array
|
||||
{
|
||||
return \array_keys($this->gatewaysByClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string> List of names of the available interfaces
|
||||
*/
|
||||
public function getNames(): array
|
||||
{
|
||||
return \array_keys($this->gatewaysByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string> List of the available Gateway names
|
||||
*
|
||||
* @see GatewaysCompilerPass::compileGatewayNames()
|
||||
*/
|
||||
public static function getNamesStatic(): array
|
||||
{
|
||||
return explode(PHP_EOL, \file_get_contents(GatewaysCompilerPass::getLockFile()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return GatewayInterface[]
|
||||
*/
|
||||
public function getGateways(): array
|
||||
{
|
||||
return $this->gatewaysByName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the Gateway interface implementation
|
||||
*
|
||||
* @throws \Exception When the $name does not match to that of an implemented Gateway
|
||||
*/
|
||||
public function getGateway(string $name): GatewayInterface
|
||||
{
|
||||
if (!\array_key_exists($name, $this->gatewaysByName)) {
|
||||
throw new \Exception("No such Gateway with the name $name");
|
||||
}
|
||||
|
||||
return $this->gatewaysByName[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception When the $checkout::gateway does not match to that of an implemented Gateway
|
||||
*/
|
||||
public function getGatewayOf(GatewayCheckout $checkout): GatewayInterface
|
||||
{
|
||||
$gateway = $checkout->getGateway();
|
||||
if (!$gateway) {
|
||||
throw new \Exception('The given GatewayCheckout does not specify a Gateway');
|
||||
}
|
||||
|
||||
return $this->getGateway($gateway);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
|
||||
abstract class PaypalGateway implements GatewayInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'paypal';
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Economy\Payment;
|
||||
|
||||
abstract class WalletGateway implements GatewayInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'wallet';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Accounting;
|
||||
|
||||
use App\ApiResource\Accounting as Resource;
|
||||
use App\Entity\Accounting as Entity;
|
||||
use App\Entity\Interface\AccountingOwnerInterface;
|
||||
use App\Entity\Money;
|
||||
use App\Entity\User;
|
||||
use App\Gateway\Wallet\WalletService;
|
||||
use App\Library\Economy\MoneyService;
|
||||
use App\Repository\Accounting\AccountingRepository;
|
||||
use App\Repository\Accounting\TransactionRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class AccountingMapper
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AccountingRepository $accountingRepository,
|
||||
private TransactionRepository $transactionRepository,
|
||||
private WalletService $wallet,
|
||||
private MoneyService $money,
|
||||
) {}
|
||||
|
||||
public function toResource(Entity\Accounting $entity): Resource\Accounting
|
||||
{
|
||||
$owner = $entity->getOwner();
|
||||
|
||||
$resource = new Resource\Accounting();
|
||||
$resource->id = $entity->getId();
|
||||
$resource->currency = $entity->getCurrency();
|
||||
$resource->owner = $owner;
|
||||
$resource->balance = $this->getBalance($owner);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function toEntity(Resource\Accounting $resource): Entity\Accounting
|
||||
{
|
||||
$entity = new Entity\Accounting();
|
||||
|
||||
if ($resource->id !== null) {
|
||||
$entity = $this->accountingRepository->find($resource->id);
|
||||
}
|
||||
|
||||
$entity->setCurrency($resource->currency);
|
||||
$entity->setOwner($resource->owner);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
private function getBalance(AccountingOwnerInterface $owner): Money
|
||||
{
|
||||
if ($owner instanceof User) {
|
||||
return $this->wallet->getBalance($owner->getAccounting());
|
||||
}
|
||||
|
||||
$accounting = $owner->getAccounting();
|
||||
|
||||
$balance = new Money(0, $accounting->getCurrency());
|
||||
$transactions = $this->transactionRepository->findByAccounting($accounting);
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
if ($transaction->getTarget() === $accounting) {
|
||||
$balance = $this->money->add($transaction->getMoney(), $balance);
|
||||
}
|
||||
|
||||
if ($transaction->getOrigin() === $accounting) {
|
||||
$balance = $this->money->substract($transaction->getMoney(), $balance);
|
||||
}
|
||||
}
|
||||
|
||||
return $balance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Gateway;
|
||||
|
||||
use App\ApiResource\Gateway as Resource;
|
||||
use App\Entity\Gateway as Entity;
|
||||
use App\Mapping\Accounting\AccountingMapper;
|
||||
use App\Repository\Gateway\ChargeRepository;
|
||||
|
||||
class ChargeMapper
|
||||
{
|
||||
public function __construct(
|
||||
private AccountingMapper $accountingMapper,
|
||||
private ChargeRepository $chargeRepository,
|
||||
) {}
|
||||
|
||||
public function toResource(Entity\Charge $entity): Resource\Charge
|
||||
{
|
||||
$resource = new Resource\Charge();
|
||||
$resource->id = $entity->getId();
|
||||
$resource->type = $entity->getType();
|
||||
$resource->title = $entity->getTitle();
|
||||
$resource->description = $entity->getDescription();
|
||||
$resource->money = $entity->getMoney();
|
||||
$resource->target = $this->accountingMapper->toResource($entity->getTarget());
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function toEntity(Resource\Charge $resource): Entity\Charge
|
||||
{
|
||||
$entity = new Entity\Charge();
|
||||
|
||||
if ($resource->id !== null) {
|
||||
$entity = $this->chargeRepository->find($resource->id);
|
||||
}
|
||||
|
||||
$entity->setType($resource->type);
|
||||
$entity->setTitle($resource->title);
|
||||
$entity->setDescription($resource->description);
|
||||
$entity->setMoney($resource->money);
|
||||
$entity->setTarget($this->accountingMapper->toEntity($resource->target));
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Gateway;
|
||||
|
||||
use App\ApiResource\Gateway as Resource;
|
||||
use App\Entity\Gateway as Entity;
|
||||
use App\Gateway\GatewayLocator;
|
||||
use App\Mapping\Accounting\AccountingMapper;
|
||||
use App\Repository\Gateway\CheckoutRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
class CheckoutMapper
|
||||
{
|
||||
public function __construct(
|
||||
private GatewayLocator $gatewayLocator,
|
||||
private GatewayMapper $gatewayMapper,
|
||||
private CheckoutRepository $checkoutRepository,
|
||||
private AccountingMapper $accountingMapper,
|
||||
private ChargeMapper $chargeMapper,
|
||||
) {}
|
||||
|
||||
public function toResource(Entity\Checkout $entity): Resource\Checkout
|
||||
{
|
||||
$resource = new Resource\Checkout();
|
||||
$resource->id = $entity->getId();
|
||||
|
||||
$gateway = $this->gatewayLocator->getForCheckout($entity);
|
||||
$resource->gateway = $this->gatewayMapper->toResource($gateway);
|
||||
|
||||
$resource->origin = $this->accountingMapper->toResource($entity->getOrigin());
|
||||
|
||||
$charges = [];
|
||||
foreach ($entity->getCharges() as $charge) {
|
||||
$charges[] = $this->chargeMapper->toResource($charge);
|
||||
}
|
||||
|
||||
$resource->charges = $charges;
|
||||
|
||||
$resource->status = $entity->getStatus();
|
||||
$resource->links = $entity->getLinks();
|
||||
$resource->trackings = $entity->getTrackings();
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function toEntity(Resource\Checkout $resource): Entity\Checkout
|
||||
{
|
||||
$entity = new Entity\Checkout();
|
||||
|
||||
if ($resource->id !== null) {
|
||||
$entity = $this->checkoutRepository->find($resource->id);
|
||||
}
|
||||
|
||||
$entity->setGatewayName($resource->gateway->name);
|
||||
$entity->setOrigin($this->accountingMapper->toEntity($resource->origin));
|
||||
|
||||
$charges = [];
|
||||
foreach ($resource->charges as $charge) {
|
||||
$charges[] = $this->chargeMapper->toEntity($charge);
|
||||
}
|
||||
|
||||
$entity->setCharges(new ArrayCollection($charges));
|
||||
|
||||
$entity->setStatus($resource->status);
|
||||
$entity->setLinks($resource->links);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Gateway;
|
||||
|
||||
use App\ApiResource\Gateway\Gateway;
|
||||
use App\Gateway\GatewayInterface;
|
||||
|
||||
class GatewayMapper
|
||||
{
|
||||
public function toResource(GatewayInterface $gateway): Gateway
|
||||
{
|
||||
$resource = new Gateway();
|
||||
$resource->name = $gateway::getName();
|
||||
$resource->supports = $gateway::getSupportedChargeTypes();
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,7 @@ use ApiPlatform\OpenApi\OpenApi;
|
||||
|
||||
class OpenApiFactory implements OpenApiFactoryInterface
|
||||
{
|
||||
public function __construct(private OpenApiFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
public function __construct(private OpenApiFactoryInterface $decorated) {}
|
||||
|
||||
private function updateOperation(Operation $operation): Operation
|
||||
{
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
namespace App\Repository\Accounting;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository\Accounting;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Transaction>
|
||||
*
|
||||
* @method Transaction|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Transaction|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Transaction[] findAll()
|
||||
* @method Transaction[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class TransactionRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Transaction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Transaction[] Transactions originated from or targeting the Accounting
|
||||
*/
|
||||
public function findByAccounting(Accounting $accounting): array
|
||||
{
|
||||
return $this->createQueryBuilder('t')
|
||||
->andWhere('t.origin = :val')
|
||||
->orWhere('t.target = :val')
|
||||
->setParameter('val', $accounting)
|
||||
->orderBy('t.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Transaction[] Transactions originated from the Accounting
|
||||
*/
|
||||
public function findByOrigin(Accounting $origin): array
|
||||
{
|
||||
return $this->createQueryBuilder('t')
|
||||
->andWhere('t.origin = :val')
|
||||
->setParameter('val', $origin)
|
||||
->orderBy('t.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Transaction[] Transactions targetting the Accounting
|
||||
*/
|
||||
public function findByTarget(Accounting $origin): array
|
||||
{
|
||||
return $this->createQueryBuilder('t')
|
||||
->andWhere('t.target = :val')
|
||||
->setParameter('val', $origin)
|
||||
->orderBy('t.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Transaction[] Returns an array of Transaction objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('t.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Transaction
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AccountingTransaction;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AccountingTransaction>
|
||||
*
|
||||
* @method AccountingTransaction|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method AccountingTransaction|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method AccountingTransaction[] findAll()
|
||||
* @method AccountingTransaction[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class AccountingTransactionRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AccountingTransaction::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return AccountingTransaction[] Returns an array of AccountingTransaction objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('t.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?AccountingTransaction
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
+11
-11
@@ -1,28 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
namespace App\Repository\Gateway;
|
||||
|
||||
use App\Entity\GatewayCharge;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<GatewayCharge>
|
||||
* @extends ServiceEntityRepository<Charge>
|
||||
*
|
||||
* @method GatewayCharge|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method GatewayCharge|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method GatewayCharge[] findAll()
|
||||
* @method GatewayCharge[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
* @method Charge|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Charge|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Charge[] findAll()
|
||||
* @method Charge[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class GatewayChargeRepository extends ServiceEntityRepository
|
||||
class ChargeRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, GatewayCharge::class);
|
||||
parent::__construct($registry, Charge::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return GatewayCharge[] Returns an array of GatewayCharge objects
|
||||
// * @return Charge[] Returns an array of Charge objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
@@ -36,7 +36,7 @@ class GatewayChargeRepository extends ServiceEntityRepository
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?GatewayCharge
|
||||
// public function findOneBySomeField($value): ?Charge
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository\Gateway;
|
||||
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Checkout>
|
||||
*
|
||||
* @method Checkout|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Checkout|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Checkout[] findAll()
|
||||
* @method Checkout[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class CheckoutRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Checkout::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Checkout|null
|
||||
*/
|
||||
public function findOneByTracking(string $title, string $value): ?array
|
||||
{
|
||||
// TO-DO: Use a JSON lookup into the 'trackings' column to see if it contains title and value
|
||||
return null;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Checkout[] Returns an array of Checkout objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('g.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Checkout
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\GatewayCheckout;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<GatewayCheckout>
|
||||
*
|
||||
* @method GatewayCheckout|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method GatewayCheckout|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method GatewayCheckout[] findAll()
|
||||
* @method GatewayCheckout[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class GatewayCheckoutRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, GatewayCheckout::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return GatewayCheckout[] Returns an array of GatewayCheckout objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('g.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?GatewayCheckout
|
||||
// {
|
||||
// return $this->createQueryBuilder('g')
|
||||
// ->andWhere('g.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\UserPersonal;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<UserPersonal>
|
||||
*
|
||||
* @method UserPersonal|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method UserPersonal|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method UserPersonal[] findAll()
|
||||
* @method UserPersonal[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class UserPersonalRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, UserPersonal::class);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Accounting;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\WalletFinancement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<WalletFinancement>
|
||||
*/
|
||||
class WalletFinancementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, WalletFinancement::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return WalletFinancement[] Returns an array of WalletFinancement objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('w')
|
||||
// ->andWhere('w.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('w.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?WalletFinancement
|
||||
// {
|
||||
// return $this->createQueryBuilder('w')
|
||||
// ->andWhere('w.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user