Links

Payment Plugins

A payment plugin is a add-ons to extend payment options

Overview

Payment is an essential part of the checkout process. A payment plugin is a add-ons that is used to process payments for eCommerce websites. It allows the customer to make a purchase without leaving the website and it also provides an interface for the store owner to manage their orders and customers.

Create a payment plugin

First , you have to visit to plugin basics to get detailed information :
This content explains only how to develop payment plugin gateway.
Payment plugins have 3 type :

Internal Payment Implementation

If you want to develop card based payment plugin , then you have to implement in your plugin gateway
CardPaymentGatewayInterface
Example Stripe gateway:
<?php
namespace Uvodo\Stripe;
use Brick\Money\Exception\UnknownCurrencyException;
use Framework\Http\StatusCodes;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriFactoryInterface;
use RuntimeException;
use Support\PaymentGateway\CardPaymentGatewayInterface;
use Support\PaymentGateway\Exceptions\InvalidRequestException;
use Support\PaymentGateway\Exceptions\CardDeclinedException;
use Support\PaymentGateway\PaymentRequest;
use Support\PaymentGateway\PaymentResponseInterface;
use Support\PaymentGateway\SuccessResponse;
use Support\PaymentGateway\ValueObjects\Card;
/** @package Plugins\Stripe */
class StripePaymentGateway implements CardPaymentGatewayInterface
{
public const SHORT_NAME = "stripe";
private ClientInterface $httpClient;
private RequestFactoryInterface $httpRequestFactory;
private UriFactoryInterface $uriFactory;
private StreamFactoryInterface $streamFactory;
private string $host = 'api.stripe.com';
private string $scheme = "https";
private string $version = 'v1';
private SecretKey $privateKey;
/**
* @param ClientInterface $httpClient
* @param RequestFactoryInterface $httpRequestFactory
* @param UriFactoryInterface $uriFactory
* @param StreamFactoryInterface $streamFactory
* @param SecretKey $privateKey
* @return void
*/
public function __construct(
ClientInterface $httpClient,
RequestFactoryInterface $httpRequestFactory,
UriFactoryInterface $uriFactory,
StreamFactoryInterface $streamFactory,
SecretKey $privateKey
) {
$this->httpClient = $httpClient;
$this->httpRequestFactory = $httpRequestFactory;
$this->uriFactory = $uriFactory;
$this->streamFactory = $streamFactory;
$this->privateKey = $privateKey;
}
/**
* @inheritDoc
*/
public function getShortName(): string
{
return self::SHORT_NAME;
}
/**
* @inheritDoc
* @throws ClientExceptionInterface
*/
public function pay(PaymentRequest $request): PaymentResponseInterface
{
/**
* @todo body details here
*/
$body = [
'amount' => $request->amount->getValue(),
'currency' => $request->currencyCode->getValue(),
];
if ($request->card) {
$token = $this->createCardToken($request->card);
$body['source'] = $token;
}
$resp = $this->sendRequest('POST', '/charges', $body);
$json = json_decode($resp->getBody()->getContents());
$authorization = $json->id;
$payResp = new SuccessResponse();
$payResp->setAuthorization($authorization);
return $payResp;
}
/**
* @param string $method
* @param string $path
* @param null|array $body
* @return ResponseInterface
* @throws InvalidArgumentException
* @throws ClientExceptionInterface
* @throws RuntimeException
* @throws CardDeclinedException
* @throws InvalidRequestException
*/
private function sendRequest(
string $method,
string $path,
?array $body = null
): ResponseInterface {
$req = $this->createHttpRequest($method, $path);
if ($body) {
$stream = $this->createStreamBody($body);
$req = $req->withBody($stream);
}
$resp = $this->httpClient->sendRequest($req);
$this->validateResponse($resp);
return $resp;
}
/**
* @param string $method
* @param string $path
* @return RequestInterface
* @throws InvalidArgumentException
*/
private function createHttpRequest(
string $method,
string $path
): RequestInterface {
$uri = $this->uriFactory->createUri('/' . $this->version . $path)
->withHost($this->host)
->withScheme($this->scheme);
$req = $this->httpRequestFactory->createRequest($method, $uri)
->withHeader(
'Authorization',
'Bearer ' . $this->privateKey->getValue()
);
return $req;
}
/**
* @param array $body
* @return StreamInterface
*/
private function createStreamBody(array $body): StreamInterface
{
return $this->streamFactory->createStream(http_build_query($body));
}
/**
* @param Card $card
* @return string
* @throws InvalidArgumentException
* @throws ClientExceptionInterface
* @throws RuntimeException
* @throws CardDeclinedException
* @throws InvalidRequestException
*/
private function createCardToken(Card $card): string
{
$body = [
'card' => [
'number' => $card->number->getValue(),
'exp_month' => $card->expMonth->getValue(),
'exp_year' => $card->expYear->getValue(),
'cvc' => $card->cvc->getValue()
],
];
$resp = $this->sendRequest('POST', '/tokens', $body);
$json = json_decode($resp->getBody()->getContents());
return $json->id;
}
/**
* @param ResponseInterface $resp
* @return void
* @throws RuntimeException
* @throws CardDeclinedException
* @throws InvalidRequestException
*/
private function validateResponse(ResponseInterface $resp): void
{
if (
$resp->getStatusCode() < StatusCodes::HTTP_OK
|| $resp->getStatusCode() >= StatusCodes::HTTP_MULTIPLE_CHOICES
) {
$json = json_decode($resp->getBody()->getContents());
$error = $json->error;
$code = $error->code;
if ($code == 'card_declined') {
throw new CardDeclinedException(
$error->decline_code,
$error->message
);
}
throw new InvalidRequestException($error->message, $code, $error->param ?? null);
}
}
}

External Payment Implementation

If you want to develop external payment plugin , then you have to implement in your plugin gateway
OtherPaymentGatewayInterface
Example Mercado Pago gateway:
<?php
namespace Uvodo\MercadoPago;
use Brick\Money\Exception\UnknownCurrencyException;
use Brick\Money\Money;
use Framework\Http\StatusCodes;
use Modules\Order\Domain\Entities\OrderItemEntity;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Support\PaymentGateway\Exceptions\InvalidRequestException;
use Support\PaymentGateway\OtherPaymentGatewayInterface;
use Support\PaymentGateway\PaymentConfirmationRequest;
use Support\PaymentGateway\PaymentRequest;
use Support\PaymentGateway\PaymentResponseInterface;
use Support\PaymentGateway\RedirectResponse;
use Support\PaymentGateway\SuccessResponse;
use Uvodo\MercadoPago\Exceptions\PaymentException;
/** @package Plugins\MercadoPago */
class MercadoPagoPaymentGateway implements OtherPaymentGatewayInterface
{
public const SHORT_NAME = "mercado-pago";
private string $host;
private string $scheme = "https";
private string $version = 'v1';
private const STATUS_APPROVED = 'approved';
private const STATUS_DECLINED = 'declined';
/**
* @param ClientInterface $httpClient
* @param RequestFactoryInterface $httpRequestFactory
* @param UriFactoryInterface $uriFactory
* @param StreamFactoryInterface $streamFactory
* @param ApiTokenKey $apiTokenKey
*/
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $httpRequestFactory,
private UriFactoryInterface $uriFactory,
private StreamFactoryInterface $streamFactory,
private ApiTokenKey $apiTokenKey
) {
$this->host = 'api.mercadopago.com';
}
public function getShortName(): string
{
return self::SHORT_NAME;
}
/**
* @param PaymentRequest $request
* @return PaymentResponseInterface
* @throws ClientExceptionInterface
* @throws InvalidRequestException|UnknownCurrencyException
*/
public function pay(PaymentRequest $request): PaymentResponseInterface
{
$data = $this->createPreference($request);
$payResp = new RedirectResponse();
$payResp->setAuthorization($data->id);
$payResp->setRedirectUrl($data->init_point);
return $payResp;
}
/**
*/
public function confirm(PaymentConfirmationRequest $request): PaymentResponseInterface
{
parse_str($request->queryString, $params);
$payment = $this->getPayment($params['payment_id']);
if (is_null($payment)) {
throw new PaymentException("Payment is invalid");
}
if ($payment->status === self::STATUS_APPROVED) {
$res = new SuccessResponse();
$res->setAuthorization($params['payment_id']);
return $res;
} elseif ($payment->status === self::STATUS_DECLINED) {
throw new PaymentException("Payment was declined");
}
throw new PaymentException("Payment is invalid");
}
/**
* @param PaymentRequest $request
* @return mixed
* @throws ClientExceptionInterface
* @throws InvalidRequestException|UnknownCurrencyException
*/
private function createPreference(
PaymentRequest $request
): mixed {
$uri = $this->uriFactory->createUri('/checkout/preferences')
->withHost($this->host)
->withScheme($this->scheme);
$redirectUrl = env('STOREFRONT_URL') . '/checkout/' . $request->cardToken->getValue() . '?';
$returnQuery = http_build_query(
[
'gateway' => self::SHORT_NAME,
'redirect_type' => 'return'
]
);
$cancelQuery = http_build_query(
[
'gateway' => self::SHORT_NAME,
'redirect_type' => 'cancel'
]
);
$backUrls = [
'success' => $redirectUrl . $returnQuery,
'pending' => $redirectUrl . $cancelQuery,
'failure' => $redirectUrl . $cancelQuery,
];
$currency = $request->order->getShop()->getCurrencyCode()->getValue();
$items = [];
$payer = [
'name' => $request->order->getCustomer()->getFirstName()->getValue(),
'surname' => $request->order->getCustomer()->getLastName()->getValue(),
'email' => $request->order->getCustomer()->getEmail()->getValue(),
'phone' => $request->order->getCustomer()->getPhoneNumber()->getValue(),
];
/** @var OrderItemEntity $item */
foreach ($request->order->getItems() as $item) {
$price = $item->getSalePrice() && $item->getSalePrice()->getValue() ?: $item->getPrice();
if (!is_null($price) && $price->getValue() > 0) {
$price = Money::ofMinor($price->getValue(), $currency);
$major = $price->getAmount()->getIntegralPart();
$minor = $price->getAmount()->getFractionalPart();
$items[] = [
'id' => $item->getId()->getValue(),
'title' => $item->getProduct()->getTitle()->getValue(),
'description' => $item->getProduct()->getMetaDescription()->getValue(),
'quantity' => $item->getQuantity()->getValue(),
'currency_id' => $currency,
'unit_price' => (float)($major . '.' . $minor)
];
}
}
$shippingCost = $request->order->getShippingCost()->getValue();
if (!is_null($shippingCost)) {
$shippingCost = Money::ofMinor($shippingCost, $currency);
$major = $shippingCost->getAmount()->getIntegralPart();
$minor = $shippingCost->getAmount()->getFractionalPart();
$items[] = [
'id' => rand(1000, 9000),
'title' => 'Shipping Cost',
'description' => 'The amount is shipping cost for order',
'quantity' => 1,
'currency_id' => $currency,
'unit_price' => (float)($major . '.' . $minor)
];
}
$taxCost = $request->order->getTotalTax()->getValue();
if (!is_null($taxCost)) {
$taxCost = Money::ofMinor($taxCost, $currency);
$major = $taxCost->getAmount()->getIntegralPart();
$minor = $taxCost->getAmount()->getFractionalPart();
$items[] = [
'id' => rand(1000, 9000),
'title' => 'Tax Cost',
'description' => 'The amount is tax cost for order',
'quantity' => 1,
'currency_id' => $currency,
'unit_price' => (float)($major . '.' . $minor)
];
}
$body = $this->streamFactory->createStream(json_encode([
"items" => $items,
"back_urls" => $backUrls,
'auto_return' => 'all',
'payer' => $payer
]));
$req = $this->httpRequestFactory->createRequest('POST', $uri)
->withHeader('Content-Type', 'application/json')
->withHeader(
'Authorization',
'Bearer ' . $this->apiTokenKey->getValue()
)->withBody($body);
$resp = $this->httpClient->sendRequest($req);
$this->validateResponse($resp);
return json_decode($resp->getBody()->getContents());
}
private function getPayment(string $paymentId)
{
$uri = $this->uriFactory->createUri($this->version . '/payments/' . $paymentId)
->withHost($this->host)
->withScheme($this->scheme);
$req = $this->httpRequestFactory->createRequest('GET', $uri)
->withHeader('Content-Type', 'application/json')
->withHeader(
'Authorization',
'Bearer ' . $this->apiTokenKey->getValue()
);
$resp = $this->httpClient->sendRequest($req);
$this->validateResponse($resp);
return json_decode($resp->getBody()->getContents());
}
/**
* @param ResponseInterface $res
* @return void
* @throws InvalidRequestException
*/
private function validateResponse(ResponseInterface $res): void
{
if ($res->getStatusCode() > StatusCodes::HTTP_CREATED) {
$json = json_decode($res->getBody()->getContents());
$description = $json->Message ?? '';
throw new InvalidRequestException($description, $res->getStatusCode());
}
}
}

Cash Payment Implementation

If you want to develop cash payment plugin , then you have to implement in your plugin gateway
ManualPaymentGatewayInterface
Example COD gateway:
<?php
namespace Uvodo\CashOnDelivery;
use Support\PaymentGateway\ManualPaymentGatewayInterface;
use Support\PaymentGateway\PaymentRequest;
use Support\PaymentGateway\PaymentResponseInterface;
use Support\PaymentGateway\SuccessResponse;
/** @package Plugins\CashOnDelivery */
class CashOnDeliveryPaymentGateway implements ManualPaymentGatewayInterface
{
public const SHORT_NAME = "cod";
/** @inheritDoc */
public function getShortName(): string
{
return self::SHORT_NAME;
}
/** @inheritDoc */
public function pay(PaymentRequest $request): PaymentResponseInterface
{
$payResp = new SuccessResponse();
$payResp->setAuthorization(uniqid('cod_'));
return $payResp;
}
}

Register Payment Gateway

After completing your payment plugin , you have to register your gateway in payment gateway service. To use payment factory just add PaymentGatewayFactory to constructor in your plugin's entry class.
Example here:
public function __construct(
ContainerInterface $container,
PaymentGatewayFactory $gatewayFactory,
RoutingBootstrapper $rb,
OptionHelper $optionHelper
) {
}
In your plugin's entry class , register the gateway inside boot function:
$this->paymentFactory
->registerPaymentGateway(
CashOnDeliveryPaymentGateway::SHORT_NAME,
CashOnDeliveryPaymentGateway::class,
$context
);
First parameter is short name for plugin , second parameter is your gateway class which you have already developed. Short name has to be unique to determine from where request coming.

Build a plugin zip

That's it. Your payment plugin is ready to build. Just run below command to get builded zip:
php bin/console plugin:build
You will see below screen:
Choose a plugin number which one you want to build:
After completing the command , build zip will be generated inside build folder in builded plugin folder.

Upload the builded zip

You can upload the zip with 2 ways.
  1. 1.
    via Admin Plugin Upload
  2. 2.
    via Command Line
Or you can use install command:
php bin/console plugin:install
Type there your plugin's builded zip path
And that's it , plugin already ready to use
🎉
🎉