承接 x-one/payu-bundle 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

x-one/payu-bundle

Composer 安装命令:

composer require x-one/payu-bundle

包简介

Integrates PayU payment API with Symfony applications

README 文档

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

x-one/payu-bundle 适用场景与选型建议

x-one/payu-bundle 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 731 次下载、GitHub Stars 达 0, 最近一次更新时间为 2024 年 02 月 13 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 x-one/payu-bundle 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 x-one/payu-bundle 我们能提供哪些服务?
定制开发 / 二次开发

基于 x-one/payu-bundle 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 731
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 22
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: Unknown
  • 更新时间: 2024-02-13