x-one/payu-bundle

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

Integrates PayU payment API with Symfony applications

Installs: 444

Dependents: 0

Suggesters: 0

Security: 0

Type:symfony-bundle

v2.0.0 2024-10-02 14:08 UTC

This package is auto-updated.

Last update: 2024-11-25 10:45:46 UTC


README

Packagist Version

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ącej XOne\Bundle\PayuBundle\Entity\Order
  • PayuRefund rozszerzającej XOne\Bundle\PayuBundle\Entity\Refund
  • PayuSubscription rozszerzającej XOne\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:

PoleOpis
descriptionOpis zamówienia widoczny w panelu PayU (wymagany)
additionalDescriptionDodatkowy opis zamówienia widoczny w panelu PayU (opcjonalny)
visibleDescriptionOpis zamówienia widoczny dla kupującego na stronie płatności PayU (opcjonalny)
statementDescriptionOpis 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:

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