Symfony2: интеграция с платежными системами с помощью JMSPaymentCoreBundle

Вступление

JMSPaymentCoreBundle предоставляет базу для различных бэкендов платежных систем. Пакет представляет собой набор абстракций, унифицированный API для финансовых транзакций.
Краткий список возможностей:

  • Простой, унифицированный API
  • Возможность сохранения сущностей финансовых операций
  • Управление транзакциями
  • Шифрование данных

Лицензия

Код опубликован под Apache2 license.
Документация - Attribution-NonCommercial-NoDerivs 3.0 Unported
license
.

Установка

Для установки JMSPaymentCoreBundle можно воспользоваться Composer. Добавьте следующую строку в файл composer.json:

// composer.json
{
    // ...
    require: {
        // ...
        "jms/payment-core-bundle": "master-dev"
    }
}
Желательно заменить  "master-dev" на актуальную stable ветку. После этого нужно установить новые зависимости запустив:
$ php composer.phar update

Этой командой Composer загрузит и установит все необходимые файлы. Далее необходимо обновить
AppKernel.php добавив следующие строки:

<?php
// in AppKernel::registerBundles()
$bundles = array(
    // ...
    new JMSPaymentCoreBundleJMSPaymentCoreBundle(),
    // ...
);

Первоначальная настройка

Настройка пакета JMSPaymentCoreBundle заключается в выборе "секретной" строки с помобщью которой будут шифроваться финансовый данные.
Следующие строки нужно добавить в configuration.yml проекта:

jms_payment_core:
    secret: someS3cretP4ssw0rd 
Важно: если вы измените "секретную" строку - все данные зашифрованные предыдущей "секретной" строкой будут нечитаемы.

Настройка бэкендов

Некоторые бэкенды платежных систем предоставляют свои пакеты, которые также требуют некоторые настройки.

Модель

Перед тем как принимать/обрабатывать платежи, давайте кратко ознакомимся с моделью платежей.

PaymentInstruction

PaymentInstruction это  первый объект который вам нужно создать. Он содержит такую иниформацию как общее количество, способ оплат, валюта, и другие данные, которые необходимы для определенного типа платежа, например информацию кредитной карты. 

Заметка: Все данные платежа могут быть автоматически зашифрованы, если вам это необходимо.
Ниже описаны состояния в которых может быть объект PaymentInstruction:

PaymentInstruction State Flow

Платеж (Patment)

Каждый объект  PaymentInstruction может быть разделен на несколько платежей.
Payment всегда содержит общее количество и текущее состояние объекта, такие как initiated, approved, deposited, итд.
Это позволяет, например, запросить часть от общей суммы перед предоставлением "товара", и остальное после этого.

Ниже описаны различные состояния объекта Payment:

Payment State Flow

Финансовая операция (FinancialTransaction)

Каждый платеж (Payment) может иметь несколько транзакций. Каждая транзакция (FinancialTransaction)представляет специфическое взаимодействие с бэкендом конкретной платежной системы. Например, в случае оплаты кредитной картой, это может быть авторизация.


Ниже приведены состояния объекта FinancialTransaction:
Financial Transaction State Flow

Использование

Для того чтобы интегрировать пакет JMSPaymentCoreBundle в ваше приложение, предположим что у вас уже есть объект "заказ" или его эквивалент. Он может выглядеть следующим образом:

<?php

use DoctrineORMMapping as ORM;
use JMSPaymentCoreBundleEntityPaymentInstruction;

class Order
{
    /** @ORMOneToOne(targetEntity="JMSPaymentCore:PaymentInstruction") */
    private $paymentInstruction;

    /** @ORMColumn(type="string", unique = true) */
    private $orderNumber;

    /** @ORMColumn(type="decimal", precision = 2) */
    private $amount;

    // ...

    public function __construct($amount, $orderNumber)
    {
        $this->amount = $amount;
        $this->orderNumber = $orderNumber;
    }

    public function getOrderNumber()
    {
        return $this->orderNumber;
    }

    public function getAmount()
    {
        return $this->amount;
    }

    public function getPaymentInstruction()
    {
        return $this->paymentInstruction;
    }

    public function setPaymentInstruction(PaymentInstruction $instruction)
    {
        $this->paymentInstruction = $instruction;
    }

    // ...
} 
Объект "заказ" не обязателен, но т.к. он обычно присутсвует в приложениях, он приведен в качестве демонстрации возможностей.

Выбор способа оплаты

Обычно, вы желаете предоставить потенциальному покупателю выбор способа оплаты. Для этих целей JMSPaymentCoreBundle поставляеться с специальной формой jms_choose_payment_method.
Следующий пример использует JMSDiExtraBundle, и SensioFrameworkExtraBundle но они не обязательны.
Заметка: в примере не учтены вопросы безопасности приложения. Для полноценной реализации следует учитывать права доступа на действия (detailsAction и другие).

<?php

use JMSDiExtraBundleAnnotation as DI;
use JMSPaymentCoreBundleEntityPayment;
use JMSPaymentCoreBundlePluginControllerResult;
use JMSPaymentCoreBundlePluginExceptionActionRequiredException;
use JMSPaymentCoreBundlePluginExceptionActionVisitUrl;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SensioBundleFrameworkExtraBundleConfigurationTemplate;
use SymfonyComponentHttpFoundationRedirectResponse;

/**
 * @Route("/payments")
 */
class PaymentController
{
    /** @DIInject */
    private $request;

    /** @DIInject */
    private $router;

    /** @DIInject("doctrine.orm.entity_manager") */
    private $em;

    /** @DIInject("payment.plugin_controller") */
    private $ppc;

    /**
     * @Route("/{orderNumber}/details", name = "payment_details")
     * @Template
     */
    public function detailsAction(Order $order)
    {
        $form = $this->getFormFactory()->create('jms_choose_payment_method', null, array(
            'amount'   => $order->getAmount(),
            'currency' => 'EUR',
            'default_method' => 'payment_paypal', // Optional
            'predefined_data' => array(
                'paypal_express_checkout' => array(
                    'return_url' => $this->router->generate('payment_complete', array(
                        'orderNumber' => $order->getOrderNumber(),
                    ), true),
                    'cancel_url' => $this->router->generate('payment_cancel', array(
                        'orderNumber' => $order->getOrderNumber(),
                    ), true)
                ),
            ),
        ));

        if ('POST' === $this->request->getMethod()) {
            $form->bindRequest($this->request);

            if ($form->isValid()) {
                $this->ppc->createPaymentInstruction($instruction = $form->getData());

                $order->setPaymentInstruction($instruction);
                $this->em->persist($order);
                $this->em->flush($order);

                return new RedirectResponse($this->router->generate('payment_complete', array(
                    'orderNumber' => $order->getOrderNumber(),
                )));
            }
        }

        return array(
            'form' => $form->createView()
        );
    }

    // ...

    /** @DILookupMethod("form.factory") */
    protected function getFormFactory() { }
} 

Тип форм jms_choose_payment_method
автоматически отбражает форму с доступными способами оплаты. После связывания, форма проверит данные для выбраной платежной системы, и в случае успеха возвратит объект PaymentInstruction.

Перевод денег

В предыдущей главе мы получили объект  PaymentInstruction. Давайте посмотрим как мы можем вносить деньги на счет. Если посмотреть на detailsAction, указанный в предыдущем примере, мы перенаправляем пользователя на маршрут payment_complete для которого мы сейчас создадим соответствующее действие в контроллере:

<?php

use JMSDiExtraBundleAnnotation as DI;
use JMSPaymentCoreBundleEntityPayment;
use JMSPaymentCoreBundlePluginControllerResult;
use JMSPaymentCoreBundlePluginExceptionActionRequiredException;
use JMSPaymentCoreBundlePluginExceptionActionVisitUrl;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SensioBundleFrameworkExtraBundleConfigurationTemplate;
use SymfonyComponentHttpFoundationRedirectResponse;

/**
 * @Route("/payments")
 */
class PaymentController
{
    /** @DIInject */
    private $request;

    /** @DIInject */
    private $router;

    /** @DIInject("doctrine.orm.entity_manager") */
    private $em;

    /** @DIInject("payment.plugin_controller") */
    private $ppc;

    // ... see previous section

    /**
     * @Route("/{orderNumber}/complete", name = "payment_complete")
     */
    public function completeAction(Order $order)
    {
        $instruction = $order->getPaymentInstruction();
        if (null === $pendingTransaction = $instruction->getPendingTransaction()) {
            $payment = $this->ppc->createPayment($instruction->getId(), $instruction->getAmount() - $instruction->getDepositedAmount());
        } else {
            $payment = $pendingTransaction->getPayment();
        }

        $result = $this->ppc->approveAndDeposit($payment->getId(), $payment->getTargetAmount());
        if (Result::STATUS_PENDING === $result->getStatus()) {
            $ex = $result->getPluginException();

            if ($ex instanceof ActionRequiredException) {
                $action = $ex->getAction();

                if ($action instanceof VisitUrl) {
                    return new RedirectResponse($action->getUrl());
                }

                throw $ex;
            }
        } else if (Result::STATUS_SUCCESS !== $result->getStatus()) {
            throw new RuntimeException('Transaction was not successful: '.$result->getReasonCode());
        }

        // payment was successful, do something interesting with the order
    }
} 

События

PluginController обрабатывает события для некоторых изменений в платежах. Это может быть использовано в вашем приложении для выполнения определенных действий, например, когда платеж выполнен.
Заметка: Список возможных событий можно найти в классе JMSPaymentCoreBundlePluginControllerEventEvents.

События при изменении статуса платежа

Имя: payment.state_change
Класс события: JMSPaymentCoreBundlePluginControllerEventPaymentStateChangeEvent
Это событие возникает сразу после изменения статуса платежа. Все связанные сущности будут обновлены. Вы можете получить доступ к объектам Payment, PaymentInstruction, получить старое и новое состояние платежа.

Источник:

Свободный перевод документации: http://jmsyst.com/bundles/JMSPaymentCoreBundle

Комментарии