x-one / payu-bundle
Integrates PayU payment API with Symfony applications
Requires
- php: >=8.2
- openpayu/openpayu: 2.3.*
- symfony/framework-bundle: ^6.2
- symfony/orm-pack: ^2.3
- symfony/routing: ^6.2
- symfony/translation: ^6.2
- symfony/uid: ^6.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.22
- phpstan/phpstan: ^1.10
- symfony/phpunit-bridge: ^6.2
README
Paczka integrująca PayU REST API za pomocą biblioteki OpenPayU.
Instalacja
composer require x-one/payu-bundle
Symfony Flex powinien automatycznie dodać nowy wpis do pliku config/bundles.php
.
Konfiguracja
Paczkę konfigurujemy za pomocą pliku config/packages/x_one_payu.yaml
.
x_one_payu:
api:
environment: ... # Środowisko - "sandbox" lub "secure"
merchant_pos_id: ... # Id punktu płatności (pos_id)
signature_key: ... # Drugi klucz (MD5)
oauth_client_id: ... # Protokół OAuth - client_id
oauth_client_secret: ... # Protokół OAuth - client_secret
oauth_grant_type: ... # Grant type - "client_credentials" (domyślnie) lub "trusted_merchant"
oauth_email: ... # Wymagane, jeżeli grant type ustawiony na "trusted_merchant"
oauth_ext_customer_id: ... # Wymagane, jeżeli grant type ustawiony na "trusted_merchant"
continue_route: ... # Symfonowy route do przekierowania po płatności
notify_route: ... # Symfonowy route do odbioru powiadomienia o zmianie statusu
Powyższe wartości konfiguracyjne (poza routkami) pobieramy z konfiguracji punktu płatności sklepu w panelu PayU. Zakładka "Moje sklepy" > Wchodzimy w konkretny sklep > zakładka "Punkty płatności" > W szczegółach danego punktu mamy widoczną konfigurację.
Routing
Paczka udostępnia route przetwarzający powiadomienia z PayU. Aby je zaimportować, dodaj wpis do config/routes.yaml
:
x_one_payu:
resource: '@XOnePayuBundle/config/routes.php'
prefix: /payments/payu
Zaimportowany route można wykorzystać w konfiguracji paczki:
x_one_payu:
api:
notify_route: x_one_payu_notify
Encje
Paczka wymaga dodania encji rozszerzających bazowe z bundle — nazewnictwo dowolne:
PayuOrder
rozszerzającejXOne\Bundle\PayuBundle\Entity\Order
PayuRefund
rozszerzającejXOne\Bundle\PayuBundle\Entity\Refund
PayuSubscription
rozszerzającejXOne\Bundle\PayuBundle\Entity\Subscription
Przykład encji:
<?php
declare(strict_types=1);
namespace App\Entity\Payment;
use App\Repository\Payment\PayuOrderRepository;
use Doctrine\ORM\Mapping as ORM;
use XOne\Bundle\PayuBundle\Entity\Order;
#[ORM\Entity(repositoryClass: PayuOrderRepository::class)]
class PayuOrder extends Order
{
}
Po ich utworzeniu należy dodać je do konfiguracji:
# config/packages/x_one_payu.yaml
x_one_payu:
entities:
order: App\Entity\Payment\PayuOrder
refund: App\Entity\Payment\PayuRefund
subscription: App\Entity\Payment\PayuSubscription
Zamówienia
Zgodnie z nazewnictwem PayU, termin płatność używany jest zamiennie z terminem zamówienie.
Tworzenie zamówienia
Aby stworzyć zamówienie w PayU, należy utworzyć encję zamówienia i przekazać ją do klienta HTTP:
use XOne\Bundle\PayuBundle\Http\ClientInterface;
use XOne\Bundle\PayuBundle\Factory\OrderFactoryInterface;
use XOne\Bundle\PayuBundle\Model\OrderInterface;
/**
* @var OrderFactoryInterface $orderFactory
* @var OrderInterface $order
*/
$order = $orderFactory->create(
description: 'Zamówienie w sklepie',
currencyCode: 'PLN',
totalAmount: 149_99, // 149.99zł
);
/**
* @var ClientInterface $httpClient
*/
$httpClient->createOrder($payuOrder);
W przekazanym obiekcie zamówienia uzupełnione zostają:
- identyfikator zamówienia PayU;
- link do przekierowania klienta na stronę płatności;
Opis zamówienia
Zamówienie PayU składa się z 4 różnych opisów:
Pole | Opis |
---|---|
description | Opis zamówienia widoczny w panelu PayU (wymagany) |
additionalDescription | Dodatkowy opis zamówienia widoczny w panelu PayU (opcjonalny) |
visibleDescription | Opis zamówienia widoczny dla kupującego na stronie płatności PayU (opcjonalny) |
statementDescription | Opis widoczny na wyciągu bankowym, w tytule operacji (opcjonalny) |
Aktualizacja statusu zamówienia
PayU informuje system o aktualizacji zamówienia wysyłając zapytania POST na route x_one_payu_notify
:
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
/**
* @var RoutingConfigurator $routes
*/
$routes->add('x_one_payu_notify', '/notify')
->controller('x_one_payu.controller.notify')
->methods(['POST']);
Wbudowany kontroler XOne\Bundle\PayuBundle\Controller\NotifyController
uruchamia proces obsługi powiadomienia:
use Symfony\Component\HttpFoundation\Response;
use XOne\Bundle\PayuBundle\Http\ClientInterface;
class NotifyController
{
public function __construct(
private ClientInterface $client,
) {
}
public function __invoke(): Response
{
$this->client->consumeNotification();
return new Response();
}
}
Po otrzymaniu powiadomienia od Payu wywołuje event XOne\Bundle\PayuBundle\Event\OrderNotificationEvent
, w którym mamy dostęp do:
- obiektu zamówienia;
- odpowiedzi od PayU;
Bazowo bundle nasłuchuje tego eventu przez listener XOne\Bundle\PayuBundle\EventListener\UpdateOrderStatusFromNotification
, który aktualizuje status zamówienia. Dodatkowo listener ten wywołuje kolejny event XOne\Bundle\PayuBundle\Event\OrderStatusChangeEvent
, w którym mamy dostęp do:
- obiektu zamówienia;
- poprzedniego statusu zamówienia;
Event OrderStatusChangeEvent
jest wywoływany tylko w momencie zmiany statusu — jeżeli status z powiadomienia jest taki sam jak status w systemie, event nie zostanie wywołany. W większości przypadków to właśnie tego eventu chcemy nasłuchiwać w aplikacjach.
Zwroty
Każda płatność może zostać zwrócona częściowo lub w całości.
Notka: zgodnie z nazewnictwem PayU, termin płatność używany jest zamiennie z terminem zamówienie.
Tworzenie zwrotu
Aby stworzyć zwrot zamówienia w PayU, należy utworzyć encję zwrotu i przekazać ją do klienta HTTP:
use XOne\Bundle\PayuBundle\Http\ClientInterface;
use XOne\Bundle\PayuBundle\Factory\RefundFactoryInterface;
use XOne\Bundle\PayuBundle\Model\OrderInterface;
/**
* @var RefundFactoryInterface $refundFactory
* @var OrderInterface $order
*/
$refund = $refundFactory->create($order);
/**
* @var ClientInterface $httpClient
*/
$httpClient->createRefund($payuRefund);
W przekazanym obiekcie zamówienia uzupełnione zostają:
- identyfikator zwrotu PayU;
Aktualizacja statusu zwrotu
Aby pobrać zwroty konkretnego zamówienia, należy przekazać obiekt zamówienia klientowi HTTP:
use XOne\Bundle\PayuBundle\Model\RefundResponse;
use XOne\Bundle\PayuBundle\Http\ClientInterface;
use XOne\Bundle\PayuBundle\Model\OrderInterface;
/**
* @var ClientInterface $httpClient
* @var OrderInterface $order
* @var RefundResponse[] $refunds
*/
$refunds = $payuClient->getOrderRefunds($order);
W odpowiedzi otrzymamy tablicę RefundResponse
, które możemy przekonwertować/zsynchronizować z naszymi encjami zwrotów.
Przykład logiki aktualizacji statusów:
use XOne\Bundle\PayuBundle\Model\RefundResponse;
use XOne\Bundle\PayuBundle\Http\ClientInterface;
use XOne\Bundle\PayuBundle\Model\OrderInterface;
use XOne\Bundle\PayuBundle\Model\RefundStatus;
use XOne\Bundle\PayuBundle\Repository\OrderRepositoryInterface;
use XOne\Bundle\PayuBundle\Repository\RefundRepositoryInterface;
/**
* @var OrderRepositoryInterface $orderRepository
* @var OrderInterface[] $orders
*/
$orders = $orderRepository->findHavingPendingRefunds();
foreach ($orders as $order) {
/**
* @var ClientInterface $httpClient
* @var RefundResponse[] $refundResponses
*/
$refundResponses = $httpClient->getOrderRefunds($order);
foreach ($refundResponses as $refundResponse) {
if ($refundResponse->isPending()) {
continue;
}
$payuRefund = $order->getRefundByPayuId($refundResponse->getPayuId());
if (null === $payuRefund) {
continue;
}
$payuRefund->updateStatusFromRefundResponse($refundResponse);
/**
* @var RefundRepositoryInterface $refundRepository
*/
$refundRepository->save($payuRefund);
}
}
Płatności cykliczne
Płatności cykliczne są uznawane za subskrypcje.
Subskrypcja spina ze sobą wiele płatności i przechowuje następujące dane:
- pierwotny token karty TOK — patrz tokenizacja karty
- re-używalny token karty TOKC — patrz tokenizacja karty
- częstotliwość płatności — patrz definicja częstotliwości
Notka: zgodnie z nazewnictwem PayU, termin płatność używany jest zamiennie z terminem zamówienie.
Tokenizacja karty
Płatności cykliczne opierają się na tokenizacji danych karty. Szczegółowy opis ich tworzenia znajdziemy w dokumentacji PayU.
W skrócie, tokeny dzielimy na jednorazowe TOK oraz wielokrotnego użytku TOKC.
Pierwsza w cyklu płatność wymaga jednorazowego tokenu TOK. Dla płatności cyklicznych tworzymy je za pomocą formularza Secure Form z wartością MULTI przekazaną jako argument do metody tokenize. Wykorzystanie tokenu wygenerowanego przez podanie innej wartości uniemożliwi poprawne utworzenie cyklu.
Po pierwszym użyciu tokena jednorazowego TOK tworzony jest token wielorazowego użytku TOKC, który może być wykorzystywany do tworzenia kolejnych płatności bez konieczności dodatkowej autoryzacji przez właściciela karty płatniczej.
Tworzenie subskrypcji
Pierwsza w cyklu płatność wymaga tokenu TOK — patrz tokenizacja karty.
use XOne\Bundle\PayuBundle\Factory\OrderFactoryInterface;
use XOne\Bundle\PayuBundle\Factory\SubscriptionFactoryInterface;
use XOne\Bundle\PayuBundle\Model\OrderInterface;
use XOne\Bundle\PayuBundle\Model\SubscriptionInterface;
use XOne\Bundle\PayuBundle\Model\SubscriptionFrequencyType;
/**
* @var SubscriptionFactoryInterface $subscriptionFactory
* @var SubscriptionInterface $subscription
*/
$subscription = $subscriptionFactory->create(
firstCardToken: 'TOK_1LJRPX2LQMRU95G3Fyl9uKwUf75E',
frequencyType: SubscriptionFrequencyType::Monthly,
);
/**
* @var OrderFactoryInterface $orderFactory
* @var OrderInterface $order
*/
$order = $orderFactory->create(
description: 'Płatność cykliczna',
currencyCode: 'PLN',
totalAmount: 149_99, // 149.99zł
);
$order->setSubscription($subscription);
Tak utworzoną płatność należy wysłać do PayU za pomocą klienta HTTP:
use XOne\Bundle\PayuBundle\Http\ClientInterface;
use XOne\Bundle\PayuBundle\Model\OrderInterface;
/**
* @var ClientInterface $httpClient
* @var OrderInterface $order
*/
$httpClient->createOrder($order);
$order->getSubscription()->getReusableCardToken(); // TOKC_1J19GJs9192hJSJ4hf929PWMs62P
Jeżeli zamówienie zostanie utworzone poprawnie, do subskrypcji przypisane zostaną:
- re-używalny token TOKC do wykorzystania do kolejnej płatności w cyklu (zamiast TOK)
- dane karty — numer (gwiazdkowany) oraz data ważności
Tworzenie kolejnych płatności w cyklu
Za tworzenie kolejnych zamówień odpowiada sama aplikacja.
Najczęstszym sposobem jest uruchamianie raz dziennie procesu tworzenia kolejnych zamówień. Za pomocą odpowiedniego zapytania SQL należy pobrać subskrypcje, w których:
1) ma co najmniej jedno zamówienie (pierwsze uruchamiające cykl)
2) ma zamówienia gdzie w każdym data zakończenia transakcji (local_receipt_date_time
) przekracza ustawiony czas
3) nie ma zamówień w trakcie (status NEW
, PENDING
lub WAITING_FOR_CONFIRMATION
)
Jeżeli przykładowo aplikacja dopuszcza 7 dni na ponowienie płatności, należy dodatkowo sprawdzić
czy nie ma zamówień o statusie CANCELED
utworzonych w ciągu ostatnich 7 dni.
Definicja częstotliwości płatności w subskrypcji
Definicja częstotliwości płatności składa się z dwóch części:
- typu — dzień lub miesiąc
- wartości — ilość dni lub miesięcy
Przykładowo, jeżeli subskrypcja jest miesięczna, definicja wygląda następująco:
use XOne\Bundle\PayuBundle\Model\SubscriptionInterface;
use XOne\Bundle\PayuBundle\Model\SubscriptionFrequencyType;
/** @var SubscriptionInterface $subscription */
$subscription->getFrequencyType(); // SubscriptionFrequencyType::Monthly
$subscription->getFrequencyValue(); // 1
Analogicznie, jeżeli subskrypcja jest kwartalna:
use XOne\Bundle\PayuBundle\Model\SubscriptionInterface;
use XOne\Bundle\PayuBundle\Model\SubscriptionFrequencyType;
/** @var SubscriptionInterface $subscription */
$subscription->getFrequencyType(); // SubscriptionFrequencyType::Monthly
$subscription->getFrequencyValue(); // 3
Jeżeli natomiast subskrypcja wymaga płatności co 2 tygodnie:
use XOne\Bundle\PayuBundle\Model\SubscriptionInterface;
use XOne\Bundle\PayuBundle\Model\SubscriptionFrequencyType;
/** @var SubscriptionInterface $subscription */
$subscription->getFrequencyType(); // SubscriptionFrequencyType::Daily
$subscription->getFrequencyValue(); // 14
Rozwój bundle
Uwaga: wszystkie poniższe kroki wymagane przed wydaniem nowej wersji można uruchomić za pomocą pojedynczej komendy:
composer run-script pre-commit-checks
Testy
Upewnij się, że testy nie zwracają żadnego błędu:
vendor/bin/simple-phpunit
Quality control
Upewnij się, że kod jest sformatowany poprawnie (dzięki php-cs-fixer) oraz że PHPStan nie zwraca żadnych błędów:
vendor/bin/php-cs-fixer fix
vendor/bin/phpstan analyze
Wersjonowanie
Aby paczkę dało się zaktualizować przez composera, po zmergowaniu zmian do głównego brancha, należy utworzyć tag w formacie vX.Y.Z
, np.
git tag -a v1.1.0 -m "Version v1.1.0"
git push --tags