x-one/przelewy24-bundle

There is no license information available for the latest version (v0.1.2) of this package.

Integrates Przelewy24 payment API with Symfony applications

Installs: 185

Dependents: 0

Suggesters: 0

Security: 0

Type:symfony-bundle

v0.1.2 2024-02-09 14:33 UTC

This package is not auto-updated.

Last update: 2024-12-11 18:34:08 UTC


README

Przelewy24Bundle integrates Symfony applications with Przelewy24 payment API, widely used in Poland.

Installation

See the English tutorial, or read the Polish translation.

Example usage

The Transaction entity from Installation docs is too simple for real world use. In a typical use-case, we will want to create a relation between a Transaction and what it's associated with.

Let's assume that we want to pay for Order entities. We must start with adding a relation to the Transaction entity:

#[ORM\Entity]
class Transaction extends Przelewy24Transaction
{
    #[ORM\OneToOne(inversedBy: 'transaction')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Order $order = null;

    // ... Getters and setters
}

Create a database migration and run it. Now it's possible to handle the payment process for an Order. We need to create a Transaction entity:

class OrderTransactionFactory
{
    public function __construct(
        // You can either set the redirect URL in services.yaml,
        //  when your frontend is decoupled (SPA) from the backend

        // Or you can instead inject UrlGenerator
        //  and point to an internal route
        private string $afterPaymentRedirectUrl,
    ) {}

    /** Creates a transaction for an order. */
    public function createTransaction(Order $order): Transaction
    {
        // Let's assume Order stores prices in PLN in a decimal format: 12.34 PLN
        // We need to convert PLN to 1/100 of PLN (grosz),
        //  as it's the lowest common denominator
        $transactionAmount = (int) bcmul($order->getTotalGrossPrice(), '100', 2);

        $transaction = new Transaction();

        $transaction
            ->setOrder($order)
            ->setEmail($order->getEmail())
            ->setAmount($transactionAmount)
            ->setUrlReturn($this->afterPaymentRedirectUrl)
            ->setDescription('Order ' . $order->getId())
        ;

        return $transaction;
    }
}

We can now use OrderTransactionFactory.createTransaction to create a Transaction for a given Order. We still need to submit it to the Przelewy24 API:

use XOne\Bundle\Przelewy24Bundle\Service\Przelewy24Client;
use Doctrine\ORM\EntityManagerInterface;

class OrderPaymentService
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private OrderTransactionFactory $transactionFactory,
        private Przelewy24Client $przelewy24Client,
    ) {
    }

    public function beginPayment(Order $order): Transaction
    {
        // <Business logic - validate the entity, ...>

        // Create a Transaction
        $transaction = $this->transactionFactory->createTransaction($divorce);

        // Save Transaction in the database prior to calling Przelewy24 API
        // This ensures that no race conditions can occur
        $this->entityManager->persist($transaction);
        $this->entityManager->flush();

        // Call Przelewy24 API - generates a payment link for the user
        $this->przelewy24Client->submitTransaction($transaction);
        $this->entityManager->flush();

        return $transaction;
    }

Side-note: It's beneficial to use locks. It protects business logic related to the payment process from race conditions.

$store = new FlockStore();
$factory = new LockFactory($store);
$lock = $factory->createLock('order-payment-'.$order->getId(), ttl: 5);

if (!$lock->acquire()) {
    throw new \Error('The Order payment process is locked!');
}

try {
    return $this->orderPaymentService->beginPayment($order);
} finally {
    $lock->release();
}

We can now use OrderPaymentService.beginPayment to create an Transaction for an Order and submit it to Przelewy24 API. The results are stored in the Transaction entity, which is persisted in the database.

We can now use the resulting Transaction in a controller:

public function __invoke(Order $order): JsonResponse
{
    $transaction = $this->orderPaymentService->beginPayment($order);

    // Process $transaction->status to ensure it's successful

    return new JsonResponse([
        'gatewayUrl' => $transaction->getUrlGateway(),
    ]);
}

The user now can successfully start the payment process. On success, they receive a payment gateway URL, and they can pay for their Order.

Once a customer successfully pays, the Przelewy24 API calls a webhook in our application. This is handled by the bundle and the transaction is marked as paid.

Let's process the Order entity on successful payment. We must create a Message Handler:

use App\Entity\Transaction;
use XOne\Bundle\Przelewy24Bundle\Messenger\Przelewy24TransactionPaid;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class TransactionPaidHandler
{
    public function __construct(
        private EntityManagerInterface $entityManager,
    ) {
    }

    public function __invoke(Przelewy24TransactionPaid $message): void
    {
        // Read the Transaction entity
        $repository = $this->entityManager->getRepository($message->transactionFQCN);
        /** @var Transaction $transaction */
        $transaction = $repository->find($message->transactionId);

        $order = $transaction->getOrder();
        if (!is_null($order)) {
            // Do something with the Order
            // Send emails, mark it for realization, etc...
        } else {
            throw new \Error('Payment for a Transaction was processed, but there were no relations!');
        }
    }
}

Now the payment process is fully handled. An user can pay for their Order, and a successful payment will be processed.

If you introduce additional relations to the Transaction entity, it is be possible to handle them in any way you want in the TransactionPaidHandler. For example, it's possible to introduce multiple 1-1 relations with different Entities, and handle the payment process with separate business logic.

Roadmap

  • Internal refactor to improve code readability and testability
  • Extend the Transaction entity to support more fields from Przelewy24 API, such as currency, language, etc.
  • Write unit and integration tests