10 changed files with 13 additions and 384 deletions
@ -1,24 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
declare(strict_types=1); |
|
||||||
|
|
||||||
namespace App\Controller\Administration; |
|
||||||
|
|
||||||
use App\Credits\Entity\CreditTransaction; |
|
||||||
use Doctrine\ORM\EntityManagerInterface; |
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
|
||||||
use Symfony\Component\HttpFoundation\Response; |
|
||||||
use Symfony\Component\Routing\Attribute\Route; |
|
||||||
|
|
||||||
class CreditTransactionController extends AbstractController |
|
||||||
{ |
|
||||||
#[Route('/admin/transactions', name: 'admin_credit_transactions')] |
|
||||||
public function index(EntityManagerInterface $em): Response |
|
||||||
{ |
|
||||||
$transactions = $em->getRepository(CreditTransaction::class)->findBy([], ['createdAt' => 'DESC']); |
|
||||||
|
|
||||||
return $this->render('admin/transactions.html.twig', [ |
|
||||||
'transactions' => $transactions, |
|
||||||
]); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,100 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
namespace App\Credits\Entity; |
|
||||||
|
|
||||||
use Doctrine\ORM\Mapping as ORM; |
|
||||||
|
|
||||||
#[ORM\Entity] |
|
||||||
class CreditTransaction |
|
||||||
{ |
|
||||||
#[ORM\Id] |
|
||||||
#[ORM\GeneratedValue] |
|
||||||
#[ORM\Column(type: 'integer')] |
|
||||||
private int $id; |
|
||||||
|
|
||||||
#[ORM\Column(length: 64)] |
|
||||||
private string $npub; |
|
||||||
|
|
||||||
#[ORM\Column(type: 'integer')] |
|
||||||
private int $amount; |
|
||||||
|
|
||||||
#[ORM\Column(length: 16)] |
|
||||||
private string $type; // 'credit' or 'debit' |
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime')] |
|
||||||
private \DateTime $createdAt; |
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)] |
|
||||||
private ?string $reason = null; |
|
||||||
|
|
||||||
public function __construct(string $npub, int $amount, string $type, ?string $reason = null) |
|
||||||
{ |
|
||||||
$this->npub = $npub; |
|
||||||
$this->amount = $amount; |
|
||||||
$this->type = $type; |
|
||||||
$this->createdAt = new \DateTime(); |
|
||||||
$this->reason = $reason; |
|
||||||
} |
|
||||||
|
|
||||||
public function getId(): int |
|
||||||
{ |
|
||||||
return $this->id; |
|
||||||
} |
|
||||||
|
|
||||||
public function setId(int $id): void |
|
||||||
{ |
|
||||||
$this->id = $id; |
|
||||||
} |
|
||||||
|
|
||||||
public function getNpub(): string |
|
||||||
{ |
|
||||||
return $this->npub; |
|
||||||
} |
|
||||||
|
|
||||||
public function setNpub(string $npub): void |
|
||||||
{ |
|
||||||
$this->npub = $npub; |
|
||||||
} |
|
||||||
|
|
||||||
public function getAmount(): int |
|
||||||
{ |
|
||||||
return $this->amount; |
|
||||||
} |
|
||||||
|
|
||||||
public function setAmount(int $amount): void |
|
||||||
{ |
|
||||||
$this->amount = $amount; |
|
||||||
} |
|
||||||
|
|
||||||
public function getType(): string |
|
||||||
{ |
|
||||||
return $this->type; |
|
||||||
} |
|
||||||
|
|
||||||
public function setType(string $type): void |
|
||||||
{ |
|
||||||
$this->type = $type; |
|
||||||
} |
|
||||||
|
|
||||||
public function getCreatedAt(): \DateTime |
|
||||||
{ |
|
||||||
return $this->createdAt; |
|
||||||
} |
|
||||||
|
|
||||||
public function setCreatedAt(\DateTime $createdAt): void |
|
||||||
{ |
|
||||||
$this->createdAt = $createdAt; |
|
||||||
} |
|
||||||
|
|
||||||
public function getReason(): ?string |
|
||||||
{ |
|
||||||
return $this->reason; |
|
||||||
} |
|
||||||
|
|
||||||
public function setReason(?string $reason): void |
|
||||||
{ |
|
||||||
$this->reason = $reason; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@ -1,68 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
namespace App\Credits\Service; |
|
||||||
|
|
||||||
use App\Credits\Entity\CreditTransaction; |
|
||||||
use App\Credits\Util\RedisCreditStore; |
|
||||||
use Doctrine\ORM\EntityManagerInterface; |
|
||||||
use Psr\Cache\InvalidArgumentException; |
|
||||||
|
|
||||||
readonly class CreditsManager |
|
||||||
{ |
|
||||||
public function __construct( |
|
||||||
private RedisCreditStore $redisStore, |
|
||||||
private EntityManagerInterface $em |
|
||||||
) {} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function getBalance(string $npub): int |
|
||||||
{ |
|
||||||
return $this->redisStore->getBalance($npub); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function resetBalance(string $npub): int |
|
||||||
{ |
|
||||||
return $this->redisStore->resetBalance($npub); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function addCredits(string $npub, int $amount, ?string $reason = null): void |
|
||||||
{ |
|
||||||
$this->redisStore->addCredits($npub, $amount); |
|
||||||
|
|
||||||
$tx = new CreditTransaction($npub, $amount, 'credit', $reason); |
|
||||||
$this->em->persist($tx); |
|
||||||
$this->em->flush(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function canAfford(string $npub, int $cost): bool |
|
||||||
{ |
|
||||||
return $this->getBalance($npub) >= $cost; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function spendCredits(string $npub, int $cost, ?string $reason = null): void |
|
||||||
{ |
|
||||||
if (!$this->canAfford($npub, $cost)) { |
|
||||||
throw new \RuntimeException("Insufficient credits for $npub"); |
|
||||||
} |
|
||||||
|
|
||||||
$this->redisStore->spendCredits($npub, $cost); |
|
||||||
|
|
||||||
$tx = new CreditTransaction($npub, $cost, 'debit', $reason); |
|
||||||
$this->em->persist($tx); |
|
||||||
$this->em->flush(); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,90 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
namespace App\Credits\Util; |
|
||||||
|
|
||||||
use App\Credits\Entity\CreditTransaction; |
|
||||||
use Doctrine\ORM\EntityManagerInterface; |
|
||||||
use Psr\Cache\InvalidArgumentException; |
|
||||||
use Symfony\Contracts\Cache\CacheInterface; |
|
||||||
|
|
||||||
readonly class RedisCreditStore |
|
||||||
{ |
|
||||||
public function __construct( |
|
||||||
private CacheInterface $creditsCache, |
|
||||||
private EntityManagerInterface $em |
|
||||||
) {} |
|
||||||
|
|
||||||
private function key(string $npub): string |
|
||||||
{ |
|
||||||
return 'credits_' . $npub; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function resetBalance(string $npub): int |
|
||||||
{ |
|
||||||
$this->creditsCache->delete($this->key($npub)); |
|
||||||
|
|
||||||
// Fetch all transactions for the given npub |
|
||||||
$transactions = $this->em->getRepository(CreditTransaction::class) |
|
||||||
->findBy(['npub' => $npub]); |
|
||||||
|
|
||||||
// Initialize the balance |
|
||||||
$balance = 0; |
|
||||||
|
|
||||||
// Calculate the final balance based on the transactions |
|
||||||
foreach ($transactions as $tx) { |
|
||||||
if ($tx->getType() === 'credit') { |
|
||||||
$balance += $tx->getAmount(); |
|
||||||
} elseif ($tx->getType() === 'debit') { |
|
||||||
$balance -= $tx->getAmount(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Write the calculated balance into the Redis cache |
|
||||||
$item = $this->creditsCache->getItem($this->key($npub)); |
|
||||||
$item->set($balance); |
|
||||||
$this->creditsCache->save($item); |
|
||||||
|
|
||||||
return $balance; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function getBalance(string $npub): int |
|
||||||
{ |
|
||||||
// Use cache pool to fetch the credit balance |
|
||||||
return $this->creditsCache->get($this->key($npub), function () use ($npub) { |
|
||||||
return $this->resetBalance($npub); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function addCredits(string $npub, int $amount): void |
|
||||||
{ |
|
||||||
$currentBalance = $this->getBalance($npub); |
|
||||||
$item = $this->creditsCache->getItem($this->key($npub)); |
|
||||||
$balance = $currentBalance + $amount; |
|
||||||
$item->set($balance); |
|
||||||
$this->creditsCache->save($item); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
public function spendCredits(string $npub, int $amount): void |
|
||||||
{ |
|
||||||
$currentBalance = $this->getBalance($npub); |
|
||||||
$item = $this->creditsCache->getItem($this->key($npub)); |
|
||||||
if ($currentBalance < $amount) { |
|
||||||
throw new \RuntimeException('Insufficient credits'); |
|
||||||
} |
|
||||||
$balance = $currentBalance - $amount; |
|
||||||
$item->set($balance); |
|
||||||
$this->creditsCache->save($item); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,40 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
namespace App\Twig\Components; |
|
||||||
|
|
||||||
use App\Credits\Service\CreditsManager; |
|
||||||
use Psr\Cache\InvalidArgumentException; |
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; |
|
||||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; |
|
||||||
use Symfony\UX\LiveComponent\Attribute\LiveAction; |
|
||||||
use Symfony\UX\LiveComponent\ComponentToolsTrait; |
|
||||||
use Symfony\UX\LiveComponent\DefaultActionTrait; |
|
||||||
|
|
||||||
#[AsLiveComponent] |
|
||||||
final class GetCreditsComponent |
|
||||||
{ |
|
||||||
use DefaultActionTrait; |
|
||||||
use ComponentToolsTrait; |
|
||||||
|
|
||||||
public function __construct( |
|
||||||
private readonly CreditsManager $creditsManager, |
|
||||||
private readonly TokenStorageInterface $tokenStorage) |
|
||||||
{ |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @throws InvalidArgumentException |
|
||||||
*/ |
|
||||||
#[LiveAction] |
|
||||||
public function grantVoucher(): void |
|
||||||
{ |
|
||||||
$npub = $this->tokenStorage->getToken()?->getUserIdentifier(); |
|
||||||
if ($npub) { |
|
||||||
$this->creditsManager->addCredits($npub, 5, 'voucher'); |
|
||||||
} |
|
||||||
|
|
||||||
// Dispatch event to notify parent |
|
||||||
$this->emit('creditsAdded'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Loading…
Reference in new issue