10 changed files with 13 additions and 384 deletions
@ -1,24 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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