Merge pull request #140 from goteo/fix/tpv-gateway

[fix] TPV Gateway
This commit is contained in:
2025-08-04 16:07:27 +02:00
committed by GitHub
7 changed files with 187 additions and 43 deletions
@@ -10,14 +10,11 @@ use App\ApiResource\TipjarApiResource;
use App\ApiResource\User\UserApiResource;
use App\Entity\Accounting\Accounting;
use App\Entity\Matchfunding\MatchCall;
use App\Entity\Money;
use App\Entity\Project\Project;
use App\Entity\Tipjar;
use App\Entity\User\User;
use App\Mapping\Transformer\AccountingBalanceMapTransformer;
use App\State\Accounting\AccountingStateProcessor;
use App\State\Accounting\AccountingStateProvider;
use AutoMapper\Attribute\MapFrom;
/**
* v4 features an advanced economy model under the hood.
@@ -34,7 +31,7 @@ use AutoMapper\Attribute\MapFrom;
processor: AccountingStateProcessor::class,
)]
#[API\GetCollection()]
#[API\Get()]
#[API\Get(output: BalancedAccountingApiResource::class)]
#[API\Patch(security: 'is_granted("ACCOUNTING_EDIT", object)')]
class AccountingApiResource
{
@@ -46,16 +43,21 @@ class AccountingApiResource
*/
public string $currency;
/**
* The money currently held by the Accounting.
*/
#[MapFrom(Accounting::class, transformer: AccountingBalanceMapTransformer::class)]
#[API\ApiProperty(writable: false, security: 'is_granted("ACCOUNTING_VIEW", object)')]
public Money $balance;
#[API\ApiProperty(readable: false, writable: false)]
public string $ownerClass;
#[API\ApiProperty(readable: false, writable: false)]
public ?UserApiResource $user = null;
#[API\ApiProperty(readable: false, writable: false)]
public ?ProjectApiResource $project = null;
#[API\ApiProperty(readable: false, writable: false)]
public ?TipjarApiResource $tipjar = null;
#[API\ApiProperty(readable: false, writable: false)]
public ?MatchCallApiResource $matchCall;
/**
* The resource owning this Accounting.
*
@@ -76,16 +78,4 @@ class AccountingApiResource
return null;
}
#[API\ApiProperty(readable: false, writable: false)]
public ?UserApiResource $user = null;
#[API\ApiProperty(readable: false, writable: false)]
public ?ProjectApiResource $project = null;
#[API\ApiProperty(readable: false, writable: false)]
public ?TipjarApiResource $tipjar = null;
#[API\ApiProperty(readable: false, writable: false)]
public ?MatchCallApiResource $matchCall;
}
@@ -0,0 +1,77 @@
<?php
namespace App\ApiResource\Accounting;
use ApiPlatform\Metadata as API;
use App\ApiResource\Matchfunding\MatchCallApiResource;
use App\ApiResource\Project\ProjectApiResource;
use App\ApiResource\TipjarApiResource;
use App\ApiResource\User\UserApiResource;
use App\Entity\Accounting\Accounting;
use App\Entity\Matchfunding\MatchCall;
use App\Entity\Money;
use App\Entity\Project\Project;
use App\Entity\Tipjar;
use App\Entity\User\User;
use App\Mapping\Transformer\AccountingBalanceMapTransformer;
use AutoMapper\Attribute\MapFrom;
use Symfony\Component\Serializer\Attribute\Ignore;
class BalancedAccountingApiResource
{
public int $id;
/**
* The preferred currency for monetary operations.\
* 3-letter ISO 4217 currency code.
*/
public string $currency;
/**
* The money currently held by the Accounting.
*/
#[MapFrom(Accounting::class, transformer: AccountingBalanceMapTransformer::class)]
#[API\ApiProperty(writable: false, security: 'is_granted("ACCOUNTING_VIEW", object)')]
public Money $balance;
#[Ignore]
#[API\ApiProperty(readable: false, writable: false)]
public string $ownerClass;
#[Ignore]
#[API\ApiProperty(readable: false, writable: false)]
public ?UserApiResource $user = null;
#[Ignore]
#[API\ApiProperty(readable: false, writable: false)]
public ?ProjectApiResource $project = null;
#[Ignore]
#[API\ApiProperty(readable: false, writable: false)]
public ?TipjarApiResource $tipjar = null;
#[Ignore]
#[API\ApiProperty(readable: false, writable: false)]
public ?MatchCallApiResource $matchCall;
/**
* The resource owning this Accounting.
*
* @return UserApiResource|ProjectApiResource|TipjarApiResource
*/
public function getOwner(): ?object
{
switch ($this->ownerClass) {
case User::class:
return $this->user;
case Project::class:
return $this->project;
case Tipjar::class:
return $this->tipjar;
case MatchCall::class:
return $this->matchCall;
}
return null;
}
}
+2
View File
@@ -21,6 +21,8 @@ use Symfony\Component\Validator\Constraints as Assert;
#[MapProvider(EntityMapProvider::class)]
#[ORM\Table(name: 'accounting_transaction')]
#[ORM\Entity(repositoryClass: TransactionRepository::class)]
#[ORM\Index(fields: ['origin'])]
#[ORM\Index(fields: ['target'])]
class Transaction
{
use TimestampedCreationEntity;
+36 -1
View File
@@ -2,12 +2,47 @@
namespace App\Gateway\Gateway;
use App\Entity\Gateway\Charge;
use App\Entity\Gateway\Checkout;
use App\Gateway\ChargeType;
use App\Gateway\GatewayInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
abstract class CecaGateway implements GatewayInterface
class CecaGateway implements GatewayInterface
{
public const LEGACY_MESSAGE = 'This Gateway implementation is only meant to give support to legacy-records. DO NOT USE. DO NOT IMPLEMENT.';
public static function getName(): string
{
return 'tpv';
}
public static function getSupportedChargeTypes(): array
{
return [
ChargeType::Single,
];
}
public function process(Checkout $checkout): Checkout
{
throw new \Exception(self::LEGACY_MESSAGE);
}
public function handleRedirect(Request $request): RedirectResponse
{
throw new \Exception(self::LEGACY_MESSAGE);
}
public function handleWebhook(Request $request): Response
{
throw new \Exception(self::LEGACY_MESSAGE);
}
public function processRefund(Charge $charge): void
{
throw new \Exception(self::LEGACY_MESSAGE);
}
}
@@ -34,19 +34,15 @@ class TransactionRepository extends ServiceEntityRepository
?\DateTimeInterface $dateEnd = null,
): array {
$queryBuilder = $this->createQueryBuilder('t');
$accountingExpr = $queryBuilder->expr();
if ($dateEnd === null) {
$dateEnd = new \DateTime();
}
$query = $queryBuilder
->andWhere($accountingExpr->orX(
$accountingExpr->eq('t.origin', $accounting->getId()),
$accountingExpr->eq('t.target', $accounting->getId()),
->andWhere($queryBuilder->expr()->orX(
't.origin = :accountingId',
't.target = :accountingId'
))
->setParameter('accountingId', $accounting->getId())
->andWhere('t.dateCreated <= :dateEnd')
->setParameter('dateEnd', $dateEnd);
->setParameter('dateEnd', $dateEnd ?? new \DateTime());
if ($dateStart !== null) {
$query->andWhere('t.dateCreated >= :dateStart')
@@ -58,6 +54,45 @@ class TransactionRepository extends ServiceEntityRepository
return $query->getQuery()->getResult();
}
/**
* @param \DateTimeInterface|null $dateStart a date to act as lower bound of the selection
* @param \DateTimeInterface|null $dateEnd A date to act as upper bound of the selection. Current date will be used if null.
*
* @return array Simplified array data of Transactions originated from or targeting the Accounting
*/
public function findForBalanceCalc(
Accounting $accounting,
?\DateTimeInterface $dateStart = null,
?\DateTimeInterface $dateEnd = null,
): array {
$queryBuilder = $this->createQueryBuilder('t');
$query = $queryBuilder
->select('
t.id,
t.money.amount,
t.money.currency,
IDENTITY(t.origin) AS origin_id,
IDENTITY(t.target) AS target_id
')
->andWhere($queryBuilder->expr()->orX(
't.origin = :accountingId',
't.target = :accountingId'
))
->setParameter('accountingId', $accounting->getId())
->andWhere('t.dateCreated <= :dateEnd')
->setParameter('dateEnd', $dateEnd ?? new \DateTime());
if ($dateStart !== null) {
$query->andWhere('t.dateCreated >= :dateStart')
->setParameter('dateStart', $dateStart);
}
$query->orderBy('t.id', 'ASC');
return $query->getQuery()->getArrayResult();
}
/**
* @return Transaction[] Transactions originated from the Accounting
*/
+13 -9
View File
@@ -27,16 +27,19 @@ class AccountingService
return $this->wallet->getBalance($accounting);
}
$balance = new Money(0, $accounting->getCurrency());
$trxs = $this->transactionRepository->findByAccounting($accounting);
$trxs = $this->transactionRepository->findForBalanceCalc($accounting);
foreach ($trxs as $transaction) {
if ($transaction->getTarget() === $accounting) {
$balance = $this->money->add($transaction->getMoney(), $balance);
$accountingId = $accounting->getId();
$balance = new Money(0, $accounting->getCurrency());
foreach ($trxs as $trx) {
$transaction = new Money($trx['money.amount'], $trx['money.currency']);
if ($trx['target_id'] === $accountingId) {
$balance = $this->money->add($transaction, $balance);
}
if ($transaction->getOrigin() === $accounting) {
$balance = $this->money->substract($transaction->getMoney(), $balance);
if ($trx['origin_id'] === $accountingId) {
$balance = $this->money->substract($transaction, $balance);
}
}
@@ -95,6 +98,7 @@ class AccountingService
$points = [];
$totalBalance = new Money(0, $accounting->getCurrency());
$accountingId = $accounting->getId();
foreach ($period as $start) {
$end = \DateTime::createFromInterface($start)->add($period->getDateInterval());
$periodTrxs = $this->getTransactionsInPeriod($trxs, $start, $end);
@@ -103,11 +107,11 @@ class AccountingService
foreach ($periodTrxs as $trx) {
$trxMoney = $trx->getMoney();
if ($trx->getTarget() === $accounting) {
if ($trx->getTarget()->getId() === $accountingId) {
$balance = $this->money->add($trxMoney, $balance);
}
if ($trx->getOrigin() === $accounting) {
if ($trx->getOrigin()->getId() === $accountingId) {
$balance = $this->money->substract($trxMoney, $balance);
}
}
@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\Accounting\AccountingApiResource;
use App\ApiResource\Accounting\BalancedAccountingApiResource;
use App\Mapping\AutoMapper;
use App\Service\AccountingService;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
@@ -48,6 +49,6 @@ class AccountingStateProvider implements ProviderInterface
return null;
}
return $this->autoMapper->map($accounting, AccountingApiResource::class);
return $this->autoMapper->map($accounting, BalancedAccountingApiResource::class);
}
}