Merge pull request #29 from GoteoFoundation/feat/benzina-refactor

[feat] Refactor Benzina
This commit is contained in:
2024-10-10 21:13:39 +02:00
committed by GitHub
22 changed files with 452 additions and 732 deletions
+74 -32
View File
@@ -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;
@@ -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',
null,
InputOption::VALUE_NONE,
'For progressive pumps, disables the override of already pumped progress'
);
$this->addOption(
'database',
'skip-pumped',
null,
InputOption::VALUE_OPTIONAL,
'The address of the database to read from',
'mysql://goteo:goteo@mariadb:3306/benzina'
'Skips feeding already pumped records in a batch',
false
);
$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;
}
}
+74 -85
View File
@@ -31,6 +31,20 @@ class Accounting
#[ORM\Column(length: 3)]
private ?string $currency = null;
/**
* @var Collection<int, AccountingTransaction>
*/
#[API\ApiProperty(writable: false)]
#[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)]
private Collection $transactionsIssued;
/**
* @var Collection<int, AccountingTransaction>
*/
#[API\ApiProperty(writable: false)]
#[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)]
private Collection $transactionsReceived;
#[API\ApiProperty(writable: false, readable: false)]
#[ORM\Column(length: 255)]
private ?string $ownerClass = null;
@@ -47,14 +61,6 @@ class Accounting
#[ORM\OneToOne(mappedBy: 'accounting', cascade: ['persist', 'remove'])]
private ?Tipjar $tipjar = null;
#[API\ApiProperty(writable: false)]
#[ORM\OneToMany(mappedBy: 'origin', targetEntity: AccountingTransaction::class)]
private Collection $transactionsIssued;
#[API\ApiProperty(writable: false)]
#[ORM\OneToMany(mappedBy: 'target', targetEntity: AccountingTransaction::class)]
private Collection $transactionsReceived;
public function __construct()
{
/*
@@ -83,6 +89,66 @@ class Accounting
return $this;
}
/**
* @return Collection<int, AccountingTransaction>
*/
public function getTransactionsIssued(): Collection
{
return $this->transactionsIssued;
}
public function addTransactionsIssued(AccountingTransaction $transactionsIssued): static
{
if (!$this->transactionsIssued->contains($transactionsIssued)) {
$this->transactionsIssued->add($transactionsIssued);
$transactionsIssued->setOrigin($this);
}
return $this;
}
public function removeTransactionsIssued(AccountingTransaction $transactionsIssued): static
{
if ($this->transactionsIssued->removeElement($transactionsIssued)) {
// set the owning side to null (unless already changed)
if ($transactionsIssued->getOrigin() === $this) {
$transactionsIssued->setOrigin(null);
}
}
return $this;
}
/**
* @return Collection<int, AccountingTransaction>
*/
public function getTransactionsReceived(): Collection
{
return $this->transactionsReceived;
}
public function addTransactionsReceived(AccountingTransaction $transactionsReceived): static
{
if (!$this->transactionsReceived->contains($transactionsReceived)) {
$this->transactionsReceived->add($transactionsReceived);
$transactionsReceived->setTarget($this);
}
return $this;
}
public function removeTransactionsReceived(AccountingTransaction $transactionsReceived): static
{
if ($this->transactionsReceived->removeElement($transactionsReceived)) {
// set the owning side to null (unless already changed)
if ($transactionsReceived->getTarget() === $this) {
$transactionsReceived->setTarget(null);
}
}
return $this;
}
public function getOwnerClass(): ?string
{
return $this->ownerClass;
@@ -167,81 +233,4 @@ class Accounting
return $this;
}
/**
* @return Collection<int, AccountingTransaction>
*/
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;
}
}
-8
View File
@@ -36,8 +36,6 @@ class AccountingTransaction
/**
* The Accounting issuing the money of this Transaction.
*/
#[Assert\NotBlank()]
#[API\ApiProperty(writable: false)]
#[ORM\ManyToOne(inversedBy: 'transactionsIssued', cascade: ['persist'])]
#[ORM\JoinColumn(nullable: false)]
private ?Accounting $origin = null;
@@ -45,8 +43,6 @@ class AccountingTransaction
/**
* The Accounting receiving the money of this Transaction.
*/
#[Assert\NotBlank()]
#[API\ApiProperty(writable: false)]
#[ORM\ManyToOne(inversedBy: 'transactionsReceived', cascade: ['persist'])]
#[ORM\JoinColumn(nullable: false)]
private ?Accounting $target = null;
@@ -75,8 +71,6 @@ class AccountingTransaction
public function setOrigin(?Accounting $origin): static
{
$origin->addTransactionsIssued($this);
$this->origin = $origin;
return $this;
@@ -89,8 +83,6 @@ class AccountingTransaction
public function setTarget(?Accounting $target): static
{
$target->addTransactionsReceived($this);
$this->target = $target;
return $this;
+1
View File
@@ -10,6 +10,7 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* A GatewayCharge represents a monetary payment that can be done by an issuer at checkout with the Gateway.
*/
#[API\ApiResource()]
#[ORM\Entity(repositoryClass: GatewayChargeRepository::class)]
class GatewayCharge
{
+31 -37
View File
@@ -27,6 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[API\Get()]
#[API\ApiFilter(filterClass: SearchFilter::class, properties: ['origin' => 'exact', 'charges.target' => 'exact'])]
#[ORM\Entity(repositoryClass: GatewayCheckoutRepository::class)]
#[ORM\Index(fields: ['migratedId'])]
class GatewayCheckout
{
use TimestampableCreationEntity;
@@ -75,21 +76,21 @@ class GatewayCheckout
* A list of URLs provided by the Gateway for this checkout.\
* e.g: Fulfill payment, API resource address.
*
* @var Collection<int, GatewayLink>
* @var GatewayLink[]
*/
#[API\ApiProperty(writable: false)]
#[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayLink::class, cascade: ['persist'])]
private Collection $gatewayLinks;
#[ORM\Column]
private array $gatewayLinks = [];
/**
* A list of tracking codes provided by the Gateway for this checkout.\
* e.g: Order ID, Payment Capture ID, Checkout Session Token.
*
* @var Collection<int, GatewayTracking>
* @var GatewayTracking[]
*/
#[API\ApiProperty(writable: false)]
#[ORM\OneToMany(mappedBy: 'checkout', targetEntity: GatewayTracking::class, cascade: ['persist'])]
private Collection $gatewayTrackings;
#[ORM\Column]
private array $gatewayTrackings = [];
/**
* GatewayCheckout was migrated from an invest record in Goteo v3 platform.
@@ -103,7 +104,7 @@ class GatewayCheckout
*/
#[API\ApiProperty(writable: false)]
#[ORM\Column(length: 255, nullable: true)]
private ?string $migratedReference = null;
private ?string $migratedId = null;
/**
* A free-form collection of additional data associated with this checkout operation.
@@ -115,8 +116,6 @@ class GatewayCheckout
{
$this->charges = new ArrayCollection();
$this->status = GatewayCheckoutStatus::Pending;
$this->gatewayLinks = new ArrayCollection();
$this->gatewayTrackings = new ArrayCollection();
}
public function getId(): ?int
@@ -185,61 +184,56 @@ class GatewayCheckout
}
/**
* @return Collection<int, GatewayLink>
* @return GatewayLink[]
*/
public function getGatewayLinks(): Collection
public function getGatewayLinks(): array
{
return $this->gatewayLinks;
}
public function addGatewayLink(GatewayLink $link): static
{
if (!$this->gatewayLinks->contains($link)) {
$this->gatewayLinks->add($link);
$link->setCheckout($this);
}
$this->gatewayLinks = [...$this->gatewayLinks, $link];
return $this;
}
public function removeGatewayLink(GatewayLink $link): static
{
if ($this->gatewayLinks->removeElement($link)) {
// set the owning side to null (unless already changed)
if ($link->getCheckout() === $this) {
$link->setCheckout(null);
$this->gatewayLinks = \array_filter(
$this->gatewayLinks,
function (GatewayLink $existingLink) use ($link) {
return $existingLink->href !== $link->href;
}
}
);
return $this;
}
/**
* @return Collection<int, GatewayTracking>
* @return GatewayTracking[]
*/
public function getGatewayTrackings(): Collection
public function getGatewayTrackings(): array
{
return $this->gatewayTrackings;
}
public function addGatewayTracking(GatewayTracking $gatewayTracking): static
public function addGatewayTracking(GatewayTracking $tracking): static
{
if (!$this->gatewayTrackings->contains($gatewayTracking)) {
$this->gatewayTrackings->add($gatewayTracking);
$gatewayTracking->setCheckout($this);
}
$this->gatewayTrackings = [...$this->gatewayTrackings, $tracking];
return $this;
}
public function removeGatewayTracking(GatewayTracking $gatewayTracking): static
public function removeGatewayTracking(GatewayTracking $tracking): static
{
if ($this->gatewayTrackings->removeElement($gatewayTracking)) {
// set the owning side to null (unless already changed)
if ($gatewayTracking->getCheckout() === $this) {
$gatewayTracking->setCheckout(null);
$this->gatewayTrackings = \array_filter(
$this->gatewayTrackings,
function (GatewayTracking $existingTracking) use ($tracking) {
return $existingTracking->title !== $tracking->title
&& $existingTracking->value !== $tracking->value;
}
}
);
return $this;
}
@@ -256,14 +250,14 @@ class GatewayCheckout
return $this;
}
public function getMigratedReference(): ?string
public function getMigratedId(): ?string
{
return $this->migratedReference;
return $this->migratedId;
}
public function setMigratedReference(?string $migratedReference): static
public function setMigratedId(?string $migratedId): static
{
$this->migratedReference = $migratedReference;
$this->migratedId = $migratedId;
return $this;
}
+4 -89
View File
@@ -2,114 +2,29 @@
namespace App\Entity;
use ApiPlatform\Metadata as API;
use App\Repository\GatewayLinkRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: GatewayLinkRepository::class)]
class GatewayLink
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
/**
* The complete target URL.
*/
#[ORM\Column(type: Types::TEXT)]
private ?string $href = null;
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
*/
#[ORM\Column(length: 255)]
private ?string $rel = null;
public string $rel;
/**
* The HTTP method required to make the related call.
*/
#[ORM\Column(length: 255)]
private ?string $method = null;
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.
*/
#[ORM\Column(enumType: GatewayLinkType::class)]
private ?GatewayLinkType $type = null;
#[API\ApiProperty(readable: false)]
#[ORM\ManyToOne(inversedBy: 'links')]
#[ORM\JoinColumn(nullable: false)]
private ?GatewayCheckout $checkout = null;
public function getId(): ?int
{
return $this->id;
}
public function getHref(): ?string
{
return $this->href;
}
public function setHref(string $href): static
{
$this->href = $href;
return $this;
}
public function getRel(): ?string
{
return $this->rel;
}
public function setRel(string $rel): static
{
$this->rel = $rel;
return $this;
}
public function getMethod(): ?string
{
return $this->method;
}
public function setMethod(string $method): static
{
$this->method = $method;
return $this;
}
public function getType(): ?GatewayLinkType
{
return $this->type;
}
public function setType(GatewayLinkType $type): static
{
$this->type = $type;
return $this;
}
public function getCheckout(): ?GatewayCheckout
{
return $this->checkout;
}
public function setCheckout(?GatewayCheckout $checkout): static
{
$this->checkout = $checkout;
return $this;
}
public GatewayLinkType $type;
}
+8 -60
View File
@@ -2,71 +2,19 @@
namespace App\Entity;
use App\Repository\GatewayTrackingRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: GatewayTrackingRepository::class)]
class GatewayTracking
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
/**
* A descriptive title for the tracking number.
*/
#[Assert\NotBlank()]
public string $title;
/**
* The tracking number given by the Gateway.
*/
#[ORM\Column(length: 255)]
private ?string $value = null;
/**
* A descriptive title for the tracking number.
*/
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\ManyToOne(inversedBy: 'gatewayIds')]
#[ORM\JoinColumn(nullable: false)]
private ?GatewayCheckout $checkout = null;
public function getId(): ?int
{
return $this->id;
}
public function getValue(): ?string
{
return $this->value;
}
public function setValue(string $value): static
{
$this->value = $value;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getCheckout(): ?GatewayCheckout
{
return $this->checkout;
}
public function setCheckout(?GatewayCheckout $checkout): static
{
$this->checkout = $checkout;
return $this;
}
#[Assert\NotBlank()]
public string $value;
}
+5 -5
View File
@@ -76,7 +76,7 @@ class Project
*/
#[API\ApiProperty(writable: false)]
#[ORM\Column(length: 255, nullable: true)]
private ?string $migratedReference = null;
private ?string $migratedId = null;
public function __construct()
{
@@ -148,14 +148,14 @@ class Project
return $this;
}
public function getMigratedReference(): ?string
public function getMigratedId(): ?string
{
return $this->migratedReference;
return $this->migratedId;
}
public function setMigratedReference(?string $migratedReference): static
public function setMigratedId(?string $migratedId): static
{
$this->migratedReference = $migratedReference;
$this->migratedId = $migratedId;
return $this;
}
+6 -5
View File
@@ -37,6 +37,7 @@ 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)]
#[ORM\Index(fields: ['migratedId'])]
class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUserInterface
{
use TimestampableCreationEntity;
@@ -136,7 +137,7 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
*/
#[API\ApiProperty(writable: false)]
#[ORM\Column(length: 255, nullable: true)]
private ?string $migratedReference = null;
private ?string $migratedId = null;
/**
* The projects owned by this User.
@@ -373,14 +374,14 @@ class User implements UserInterface, UserOwnedInterface, PasswordAuthenticatedUs
return $this;
}
public function getMigratedReference(): ?string
public function getMigratedId(): ?string
{
return $this->migratedReference;
return $this->migratedId;
}
public function setMigratedReference(?string $migratedReference): static
public function setMigratedId(?string $migratedId): static
{
$this->migratedReference = $migratedReference;
$this->migratedId = $migratedId;
return $this;
}
+1 -1
View File
@@ -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();
+15 -2
View File
@@ -4,10 +4,23 @@ 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): array
{
if ($key !== null) {
return [
$key => \array_key_exists($key, $this->config)
? $this->config[$key]
: $default,
];
}
return $this->config;
}
}
+57 -61
View File
@@ -3,7 +3,6 @@
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;
@@ -12,7 +11,7 @@ use App\Entity\GatewayTracking;
use App\Entity\Money;
use App\Entity\Tipjar;
use App\Library\Benzina\Pump\Trait\ArrayPumpTrait;
use App\Library\Benzina\Pump\Trait\ProgressivePumpTrait;
use App\Library\Benzina\Pump\Trait\DoctrinePumpTrait;
use App\Library\Economy\Payment\CashGateway;
use App\Library\Economy\Payment\CecaGateway;
use App\Library\Economy\Payment\DropGateway;
@@ -22,12 +21,14 @@ use App\Library\Economy\Payment\WalletGateway;
use App\Repository\ProjectRepository;
use App\Repository\TipjarRepository;
use App\Repository\UserRepository;
use App\Service\GatewayCheckoutService;
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';
@@ -74,33 +75,30 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
private ProjectRepository $projectRepository,
private TipjarRepository $tipjarRepository,
private EntityManagerInterface $entityManager,
private GatewayCheckoutService $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', GatewayCheckout::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;
}
@@ -113,7 +111,6 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
}
$user = $users[$record['user']];
$project = $projects[$record['project']];
$checkout = new GatewayCheckout();
$checkout->setOrigin($user->getAccounting());
@@ -125,19 +122,26 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
}
$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->setType($this->getChargeType($record));
$charge->setMoney($this->getChargeMoney($record['amount'], $record['currency']));
$charge->setTarget($project->getAccounting());
if (empty($record['project'])) {
$charge->setTarget($user->getAccounting());
}
if (!empty($record['project'])) {
$project = $projects[$record['project']];
$charge->setTarget($project->getAccounting());
}
$checkout->addCharge($charge);
if ($record['donate_amount'] > 0) {
$tip = new GatewayCharge();
@@ -145,25 +149,11 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
$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);
}
$checkout = $this->checkoutService->chargeCheckout($checkout);
}
$this->entityManager->persist($checkout);
@@ -173,32 +163,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
@@ -277,31 +273,31 @@ class CheckoutsPump extends AbstractPump implements PumpInterface
private function getCheckoutTrackings(array $record): array
{
$v3Tracking = new GatewayTracking();
$v3Tracking->setValue($record['id']);
$v3Tracking->setTitle(self::TRACKING_TITLE_V3);
$v3Tracking->title = self::TRACKING_TITLE_V3;
$v3Tracking->value = $record['id'];
$trackings = [$v3Tracking];
if (!empty($record['payment'])) {
$payment = new GatewayTracking();
$payment->setValue($record['payment']);
$payment->setTitle(self::TRACKING_TITLE_PAYMENT);
$payment->title = self::TRACKING_TITLE_PAYMENT;
$payment->value = $record['payment'];
$trackings[] = $payment;
}
if (!empty($record['transaction'])) {
$transaction = new GatewayTracking();
$transaction->setValue($record['transaction']);
$transaction->setTitle(self::TRACKING_TITLE_TRANSACTION);
$transaction->title = self::TRACKING_TITLE_TRANSACTION;
$transaction->value = $record['transaction'];
$trackings[] = $transaction;
}
if (!empty($record['preapproval'])) {
$preapproval = new GatewayTracking();
$preapproval->setValue($record['preapproval']);
$preapproval->setTitle(self::TRACKING_TITLE_PREAPPROVAL);
$preapproval->title = self::TRACKING_TITLE_PREAPPROVAL;
$preapproval->value = $record['preapproval'];
$trackings[] = $preapproval;
}
+16 -20
View File
@@ -7,14 +7,14 @@ 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',
@@ -100,27 +100,22 @@ class ProjectsPump extends AbstractPump implements PumpInterface
) {
}
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,8 +129,9 @@ 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']));
$project->setDateUpdated(new \DateTime());
$accounting = $this->getAccounting($record);
$accounting->setProject($project);
@@ -151,15 +147,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;
+22 -12
View File
@@ -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 array at the specified key, or all keys if null
*/
public function process(mixed $data): void;
public function getConfig(?string $key = null, mixed $default = null): array;
/**
* 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', false)) {
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;
}
}
+12 -16
View File
@@ -5,13 +5,13 @@ 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',
@@ -55,24 +55,20 @@ class UsersPump extends AbstractPump implements PumpInterface
) {
}
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']);
foreach ($data as $key => $record) {
if ($this->isPumped($record, $pumped, ['migratedReference' => 'id'])) {
continue;
}
$batch = $this->skipPumped($batch, 'id', User::class, 'migratedId');
foreach ($batch as $key => $record) {
$user = new User();
$user->setAccounting(new Accounting());
$user->setUsername($this->getUsername($record));
@@ -82,7 +78,7 @@ class UsersPump extends AbstractPump implements PumpInterface
$user->setName($record['name']);
$user->setActive(false);
$user->setMigrated(true);
$user->setMigratedReference($record['id']);
$user->setMigratedId($record['id']);
$accounting = new Accounting();
$accounting->setUser($user);
@@ -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;
-43
View File
@@ -1,43 +0,0 @@
<?php
namespace App\Repository;
use App\Entity\GatewayLink;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<GatewayLink>
*/
class GatewayLinkRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, GatewayLink::class);
}
// /**
// * @return GatewayLink[] Returns an array of GatewayLink 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): ?GatewayLink
// {
// return $this->createQueryBuilder('g')
// ->andWhere('g.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
@@ -1,43 +0,0 @@
<?php
namespace App\Repository;
use App\Entity\GatewayTracking;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<GatewayTracking>
*/
class GatewayTrackingRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, GatewayTracking::class);
}
// /**
// * @return GatewayTracking[] Returns an array of GatewayTracking 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): ?GatewayTracking
// {
// return $this->createQueryBuilder('g')
// ->andWhere('g.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+47 -12
View File
@@ -7,7 +7,6 @@ use App\Entity\AccountingTransaction;
use App\Entity\GatewayCharge;
use App\Entity\GatewayCheckout;
use App\Entity\GatewayCheckoutStatus;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\RouterInterface;
class GatewayCheckoutService
@@ -17,10 +16,14 @@ class GatewayCheckoutService
public function __construct(
private RouterInterface $router,
private EntityManagerInterface $entityManager,
) {
}
/**
* Generates a suitable value for redirection params to external gateways.
*
* @return string Absolute URL to the gateways redirection handler
*/
public function generateRedirectUrl(
GatewayCheckout $checkout,
string $type = self::RESPONSE_TYPE_SUCCESS,
@@ -38,17 +41,55 @@ class GatewayCheckoutService
);
}
public function getGatewayReference(GatewayCheckout $checkout, GatewayCharge $charge): string
/**
* Generates a full tracking code for a given charge in a checkout.
*
* @return string A tracking code suitable for external gateway data matching
*/
public function generateTracking(GatewayCheckout $checkout, GatewayCharge $charge): string
{
return sprintf(
'AO%d-CO%d-CH%d-AT%d',
return \sprintf(
'%s-%s',
$this->generateCheckoutTracking($checkout),
$this->generateChargeTracking($charge)
);
}
/**
* Generates a partial tracking code for any given checkout.
*
* @return string A tracking code suitable for external gateway data matching
*/
public function generateCheckoutTracking(GatewayCheckout $checkout): string
{
return \sprintf(
'AO%d-CO%d',
$checkout->getOrigin()->getId(),
$checkout->getId(),
$checkout->getId()
);
}
/**
* Generates a partial tracking code for any given charge.
*
* @return string A tracking code suitable for external gateway data matching
*/
public function generateChargeTracking(GatewayCharge $charge): string
{
return \sprintf(
'CH%d-AT%d',
$charge->getId(),
$charge->getTarget()->getId()
);
}
/**
* Updates a successful GatewayCheckout and generates the transactions for each charge.
*
* @param GatewayCheckout $checkout The GatewayCheckout to be updated
*
* @return GatewayCheckout The updated GatewayCheckout with updated charges and generated transactions
*/
public function chargeCheckout(GatewayCheckout $checkout): GatewayCheckout
{
$checkout->setStatus(GatewayCheckoutStatus::Charged);
@@ -59,15 +100,9 @@ class GatewayCheckoutService
$transaction->setOrigin($checkout->getOrigin());
$transaction->setTarget($charge->getTarget());
$this->entityManager->persist($transaction);
$charge->setTransaction($transaction);
$this->entityManager->persist($charge);
}
$this->entityManager->flush();
return $checkout;
}
}
+22 -96
View File
@@ -3,8 +3,6 @@
namespace App\Tests\Entity;
use App\Entity\Accounting;
use App\Entity\AccountingTransaction;
use App\Entity\Money;
use App\Entity\Tipjar;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -23,114 +21,42 @@ class AccountingTest extends KernelTestCase
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
}
public function testAccountingGetsUpdatedByPersistingTransactions()
public function testAccountingGetsUpdatedByCascade()
{
$tipjarA = new Tipjar();
$tipjarA->setName('TEST_TIPJAR_A');
$accA = new Accounting();
$accA->setTipjar($tipjarA);
$accountingA = new Accounting();
$accountingA->setTipjar($tipjarA);
$this->entityManager->persist($tipjarA);
$this->entityManager->flush();
/** @var Accounting */
$accountingA = $this->entityManager->getRepository(Tipjar::class)
->findOneBy(['name' => 'TEST_TIPJAR_A'])
->getAccounting();
$this->assertNotNull($accountingA->getId());
$this->assertEquals(Tipjar::class, $accountingA->getOwnerClass());
$this->assertEquals('TEST_TIPJAR_A', $accountingA->getTipjar()->getName());
$tipjarB = new Tipjar();
$tipjarB->setName('TEST_TIPJAR_B');
$accB = new Accounting();
$accB->setTipjar($tipjarB);
$accountingB = new Accounting();
$accountingB->setTipjar($tipjarB);
$trx = new AccountingTransaction();
$trx->setMoney(new Money(120, 'EUR'));
$trx->setOrigin($accA);
$trx->setTarget($accB);
$this->entityManager->persist($trx);
$this->entityManager->persist($tipjarB);
$this->entityManager->flush();
/** @var Accounting */
$accA = $this->entityManager->getRepository(Tipjar::class)
->findOneBy(['name' => 'TEST_TIPJAR_A'])
->getAccounting();
/** @var Accounting */
$accB = $this->entityManager->getRepository(Tipjar::class)
$accountingB = $this->entityManager->getRepository(Tipjar::class)
->findOneBy(['name' => 'TEST_TIPJAR_B'])
->getAccounting();
$this->assertNotNull($accA->getId());
$this->assertNotNull($accB->getId());
$this->assertCount(1, $accA->getTransactionsIssued());
$this->assertCount(0, $accA->getTransactionsReceived());
$this->assertCount(0, $accB->getTransactionsIssued());
$this->assertCount(1, $accB->getTransactionsReceived());
$this->assertEquals(120, $accA->getTransactionsIssued()[0]->getMoney()->amount);
$this->assertEquals('EUR', $accA->getTransactionsIssued()[0]->getMoney()->currency);
$this->assertEquals(120, $accB->getTransactionsReceived()[0]->getMoney()->amount);
$this->assertEquals('EUR', $accB->getTransactionsReceived()[0]->getMoney()->currency);
}
public function testAccountingSortsMixedTransactions()
{
$tipjarA = new Tipjar();
$tipjarA->setName('TEST_TIPJAR_A');
$accA = new Accounting();
$accA->setTipjar($tipjarA);
$tipjarB = new Tipjar();
$tipjarB->setName('TEST_TIPJAR_B');
$accB = new Accounting();
$accB->setTipjar($tipjarB);
$trx1 = new AccountingTransaction();
$trx1->setMoney(new Money(1, 'EUR'));
$trx1->setOrigin($accA);
$trx1->setTarget($accB);
$trx2 = new AccountingTransaction();
$trx2->setMoney(new Money(2, 'USD'));
$trx2->setOrigin($accB);
$trx2->setTarget($accA);
$this->entityManager->persist($trx1);
$this->entityManager->persist($trx2);
$this->entityManager->flush();
/** @var Accounting */
$accA = $this->entityManager->getRepository(Tipjar::class)
->findOneBy(['name' => 'TEST_TIPJAR_A'])
->getAccounting();
/** @var Accounting */
$accB = $this->entityManager->getRepository(Tipjar::class)
->findOneBy(['name' => 'TEST_TIPJAR_B'])
->getAccounting();
$this->assertNotNull($accA->getId());
$this->assertNotNull($accB->getId());
$this->assertCount(2, $accA->getTransactions());
$this->assertCount(2, $accB->getTransactions());
$this->assertEquals(1, $accA->getTransactions()[0]->getMoney()->amount);
$this->assertEquals('EUR', $accA->getTransactions()[0]->getMoney()->currency);
$this->assertEquals(1, $accB->getTransactions()[0]->getMoney()->amount);
$this->assertEquals('EUR', $accB->getTransactions()[0]->getMoney()->currency);
$this->assertEquals(2, $accA->getTransactions()[1]->getMoney()->amount);
$this->assertEquals('USD', $accA->getTransactions()[1]->getMoney()->currency);
$this->assertEquals(2, $accB->getTransactions()[1]->getMoney()->amount);
$this->assertEquals('USD', $accB->getTransactions()[1]->getMoney()->currency);
$this->assertEquals($accA, $accA->getTransactions()[0]->getOrigin());
$this->assertEquals($accA, $accA->getTransactions()[1]->getTarget());
$this->assertEquals($accB, $accB->getTransactions()[1]->getOrigin());
$this->assertEquals($accB, $accB->getTransactions()[0]->getTarget());
$this->assertNotNull($accountingB->getId());
$this->assertEquals(Tipjar::class, $accountingB->getOwnerClass());
$this->assertEquals('TEST_TIPJAR_B', $accountingB->getTipjar()->getName());
}
}
+2 -2
View File
@@ -95,7 +95,7 @@ class UsersPumpTest extends KernelTestCase
$this->assertCount(0, $usersPrePumping);
$this->pump->process([$testUser]);
$this->pump->pump([$testUser]);
$usersPostPumping = $this->entityManager->getRepository(User::class)
->findAll();
@@ -108,6 +108,6 @@ class UsersPumpTest extends KernelTestCase
$this->assertNotEquals($testUser['confirmed'], $user->isEmailConfirmed());
$this->assertTrue($user->isMigrated());
$this->assertEquals($testUser['id'], $user->getMigratedReference());
$this->assertEquals($testUser['id'], $user->getMigratedId());
}
}