This commit is contained in:
2026-02-02 15:18:51 +01:00
parent 7a26dd69a5
commit ae0ee002ec
170 changed files with 7446 additions and 1519 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "pay-now/paynow-php-sdk",
"description": "PHP client library for accessing Paynow API",
"version": "2.2.2",
"version": "2.4.4",
"keywords": [
"paynow",
"mbank",
@@ -20,7 +20,7 @@
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=7.1",
"php": ">=7.2",
"psr/http-message": "^1.0 || ^2.0",
"php-http/client-implementation": "^1.0 || ^2.0",
"php-http/message-factory": "^1.0 || ^2.0",
@@ -28,7 +28,7 @@
"php-http/httplug": "^2.2"
},
"require-dev": {
"phpunit/phpunit": "^7.0",
"phpunit/phpunit": "^8.5.36",
"php-http/mock-client": "^1.3",
"squizlabs/php_codesniffer": "^3.4",
"friendsofphp/php-cs-fixer": "^2.15",
@@ -52,5 +52,11 @@
"test": "vendor/bin/phpunit",
"cs-check": "php-cs-fixer fix --no-interaction --dry-run --diff",
"cs-fix": "php-cs-fixer fix -v --diff"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -6,6 +6,8 @@ class Configuration implements ConfigurationInterface
{
public const API_VERSION = 'v1';
public const API_VERSION_V2 = 'v2';
public const API_VERSION_V3 = 'v3';
public const API_PRODUCTION_URL = 'https://api.paynow.pl';
public const API_SANDBOX_URL = 'https://api.sandbox.paynow.pl';
public const USER_AGENT = 'paynow-php-sdk';
@@ -32,7 +34,7 @@ class Configuration implements ConfigurationInterface
*/
private function get($key)
{
return isset($this->data[$key]) ? $this->data[$key] : null;
return $this->data[$key] ?? null;
}
/**

View File

@@ -13,11 +13,11 @@ class PaynowException extends Exception
/**
* PaynowException constructor.
* @param string $message
* @param int $code
* @param int|null $code
* @param string|null $body
* @param Throwable|null $previous
*/
public function __construct(string $message, int $code = 0, ?string $body = null, Throwable $previous = null)
public function __construct(string $message, ?int $code = 0, ?string $body = null, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);

View File

@@ -3,13 +3,14 @@
namespace Paynow\HttpClient;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
class ApiResponse
{
/**
* Body content
*
* @var string
* @var StreamInterface
*/
public $body;

View File

@@ -43,7 +43,7 @@ class HttpClient implements HttpClientInterface
} catch (NotFoundException $exception) {
$this->client = HttpClientDiscovery::find();
}
$this->url = Psr17FactoryDiscovery::findUrlFactory()->createUri((string)$config->getUrl());
$this->url = Psr17FactoryDiscovery::findUriFactory()->createUri((string)$config->getUrl());
}
/**
@@ -60,8 +60,8 @@ class HttpClient implements HttpClientInterface
/**
* @param RequestInterface $request
* @throws HttpClientException
* @return ApiResponse
* @throws HttpClientException
*/
private function send(RequestInterface $request): ApiResponse
{
@@ -76,16 +76,17 @@ class HttpClient implements HttpClientInterface
* @param string $url
* @param array $data
* @param string|null $idempotencyKey
* @throws HttpClientException
* @return ApiResponse
* @throws HttpClientException
*/
public function post(string $url, array $data, ?string $idempotencyKey = null): ApiResponse
{
$headers = $this->prepareHeaders($data);
$isv3 = strpos($url, Configuration::API_VERSION_V3) !== false;
$headers = $this->prepareHeaders($data, [], $idempotencyKey, $isv3);
if ($idempotencyKey) {
$headers['Idempotency-Key'] = $idempotencyKey;
}
if ($idempotencyKey && !$isv3) {
$headers['Idempotency-Key'] = $idempotencyKey;
}
$request = $this->messageFactory->createRequest(
'POST',
@@ -104,12 +105,13 @@ class HttpClient implements HttpClientInterface
/**
* @param string $url
* @param array $data
* @throws HttpClientException
* @param string|null $idempotencyKey
* @return ApiResponse
* @throws HttpClientException
*/
public function patch(string $url, array $data): ApiResponse
public function patch(string $url, array $data, ?string $idempotencyKey = null): ApiResponse
{
$headers = $this->prepareHeaders($data);
$headers = $this->prepareHeaders($data, [], $idempotencyKey, strpos($url, Configuration::API_VERSION_V3) !== false);
$request = $this->messageFactory->createRequest(
'PATCH',
$this->url->withPath($url)
@@ -127,17 +129,49 @@ class HttpClient implements HttpClientInterface
/**
* @param string $url
* @param string|null $query
* @throws HttpClientException
* @param string|null $idempotencyKey
* @return ApiResponse
* @throws HttpClientException
*/
public function get(string $url, string $query = null): ApiResponse
public function get(string $url, ?string $query = null, ?string $idempotencyKey = null): ApiResponse
{
$request = $this->messageFactory->createRequest(
'GET',
$query ? $this->url->withPath($url)->withQuery($query) : $this->url->withPath($url)
);
foreach ($this->prepareHeaders() as $name => $value) {
$parameters = [];
if ($query) {
parse_str(urldecode($query), $parameters);
}
foreach ($this->prepareHeaders(null, $parameters, $idempotencyKey, strpos($url, Configuration::API_VERSION_V3) !== false) as $name => $value) {
$request = $request->withHeader($name, $value);
}
return $this->send($request);
}
/**
* @param string $url
* @param string $idempotencyKey
* @param string|null $query
* @return ApiResponse
* @throws HttpClientException
*/
public function delete(string $url, string $idempotencyKey, ?string $query = null): ApiResponse
{
$request = $this->messageFactory->createRequest(
'DELETE',
$query ? $this->url->withPath($url)->withQuery($query) : $this->url->withPath($url)
);
$parameters = [];
if ($query) {
parse_str(urldecode($query), $parameters);
}
foreach ($this->prepareHeaders(null, $parameters, $idempotencyKey, strpos($url, Configuration::API_VERSION_V3) !== false) as $name => $value) {
$request = $request->withHeader($name, $value);
}
@@ -150,24 +184,40 @@ class HttpClient implements HttpClientInterface
*/
private function arrayAsJson(array $data): string
{
return json_encode($data);
return json_encode($data, JSON_UNESCAPED_SLASHES);
}
/**
* @param null|array $data
* @param array|null $body
* @param array $query
* @param string|null $idempotencyKey
* @param bool $isv3
* @return array
*/
private function prepareHeaders(?array $data = null)
private function prepareHeaders(?array $body = null, array $query = [], ?string $idempotencyKey = '', bool $isv3 = true): array
{
$headers = [
'Api-Key' => $this->config->getApiKey(),
'User-Agent' => $this->getUserAgent(),
'Accept' => 'application/json'
'Accept' => 'application/json',
];
if ($data) {
if ($isv3) {
$headers['Idempotency-Key'] = $idempotencyKey;
$headers['Signature'] = SignatureCalculator::generateV3(
$this->config->getApiKey(),
$this->config->getSignatureKey(),
$idempotencyKey,
$body ? json_encode($body, JSON_UNESCAPED_SLASHES) : '',
$query
);
}
if (!is_null($body)) {
$headers['Content-Type'] = 'application/json';
$headers['Signature'] = (string)new SignatureCalculator($this->config->getSignatureKey(), json_encode($data));
if (!$isv3) {
$headers['Signature'] = SignatureCalculator::generate($this->config->getSignatureKey(), json_encode($body, JSON_UNESCAPED_SLASHES));
}
}
return $headers;

View File

@@ -6,7 +6,9 @@ interface HttpClientInterface
{
public function post(string $url, array $data, ?string $idempotencyKey = null): ApiResponse;
public function patch(string $url, array $data): ApiResponse;
public function patch(string $url, array $data, ?string $idempotencyKey = null): ApiResponse;
public function get(string $url, ?string $query = null): ApiResponse;
public function get(string $url, ?string $query = null, ?string $idempotencyKey = null): ApiResponse;
public function delete(string $url, string $idempotencyKey, ?string $query = null): ApiResponse;
}

View File

@@ -11,8 +11,12 @@ class PaymentMethod
private $image;
private $status;
private $authorizationType;
/**
* @var SavedInstrument[]
*/
private $savedInstruments = [];
public function __construct($id, $type, $name, $description, $image, $status, $authorizationType)
public function __construct($id, $type, $name, $description, $image, $status, $authorizationType, $savedInstruments = [])
{
$this->id = $id;
$this->type = $type;
@@ -21,6 +25,18 @@ class PaymentMethod
$this->image = $image;
$this->status = $status;
$this->authorizationType = $authorizationType;
if (!empty($savedInstruments)) {
foreach ($savedInstruments as $savedInstrument) {
$this->savedInstruments[] = new SavedInstrument(
$savedInstrument->name,
$savedInstrument->expirationDate,
$savedInstrument->brand,
$savedInstrument->image,
$savedInstrument->token,
$savedInstrument->status
);
}
}
}
public function getId()
@@ -65,4 +81,12 @@ class PaymentMethod
{
return $this->authorizationType;
}
/**
* @return SavedInstrument[]
*/
public function getSavedInstruments()
{
return $this->savedInstruments;
}
}

View File

@@ -9,4 +9,6 @@ class Type
public const GOOGLE_PAY = 'GOOGLE_PAY';
public const APPLE_PAY = 'APPLE_PAY';
public const PBL = 'PBL';
public const PAYPO = 'PAYPO';
public const CLICK_TO_PAY = 'CLICK_TO_PAY';
}

View File

@@ -10,7 +10,7 @@ class PaymentMethods
/**
* @var PaymentMethod[]
*/
private $list;
private $list = [];
public function __construct($body)
{
@@ -25,7 +25,8 @@ class PaymentMethods
$item->description,
$item->image,
$item->status,
$item->authorizationType ?? null
$item->authorizationType ?? null,
$item->savedInstruments ?? []
);
}
}

View File

@@ -13,25 +13,30 @@ class DataProcessing extends Service
* Retrieve data processing notice
*
* @param string|null $locale
*
* @throws PaynowException
* @param string|null $idempotencyKey
* @return Notices
* @throws PaynowException
*/
public function getNotices(?string $locale): Notices
public function getNotices(?string $locale, ?string $idempotencyKey = null): Notices
{
$parameters = [];
if (! empty($locale)) {
if (!empty($locale)) {
$parameters['locale'] = $locale;
}
try {
if (empty($idempotencyKey)) {
$idempotencyKey = md5(($locale ?? '') . '_' . $this->getClient()->getConfiguration()->getApiKey());
}
$decodedApiResponse = $this->getClient()
->getHttpClient()
->get(
Configuration::API_VERSION . "/payments/dataprocessing/notices",
http_build_query($parameters, '', '&')
)
->decode();
->getHttpClient()
->get(
Configuration::API_VERSION_V3 . "/payments/dataprocessing/notices",
http_build_query($parameters, '', '&'),
$idempotencyKey
)
->decode();
return new Notices($decodedApiResponse);
} catch (HttpClientException $exception) {

View File

@@ -16,25 +16,29 @@ class Payment extends Service
*
* @param array $data
* @param string|null $idempotencyKey
* @throws PaynowException
* @return Authorize
* @throws PaynowException
*/
public function authorize(array $data, ?string $idempotencyKey = null): Authorize
{
try {
if (empty($idempotencyKey)) {
$idempotencyKey = ($data['externalId'] ?? null) ? md5($data['externalId']) : md5('_' . $this->getClient()->getConfiguration()->getApiKey());
}
$decodedApiResponse = $this->getClient()
->getHttpClient()
->post(
'/' . Configuration::API_VERSION . '/payments',
'/' . Configuration::API_VERSION_V3 . '/payments',
$data,
$idempotencyKey ?? $data['externalId']
$idempotencyKey
)
->decode();
return new Authorize(
$decodedApiResponse->paymentId,
$decodedApiResponse->status,
! empty($decodedApiResponse->redirectUrl) ? $decodedApiResponse->redirectUrl : null
!empty($decodedApiResponse->redirectUrl) ? $decodedApiResponse->redirectUrl : null
);
} catch (HttpClientException $exception) {
throw new PaynowException(
@@ -52,30 +56,42 @@ class Payment extends Service
* @param string|null $currency
* @param int|null $amount
* @param bool $applePayEnabled
* @param string|null $idempotencyKey
* @param string|null $buyerExternalId
* @return PaymentMethods
* @throws PaynowException
*/
public function getPaymentMethods(?string $currency = null, ?int $amount = 0, bool $applePayEnabled = true): PaymentMethods
public function getPaymentMethods(?string $currency = null, ?int $amount = 0, bool $applePayEnabled = true, ?string $idempotencyKey = null, ?string $buyerExternalId = null): PaymentMethods
{
$parameters = [
'applePayEnabled' => $applePayEnabled,
];
if (! empty($currency)) {
$parameters['currency'] = $currency;
}
$parameters = [];
if ($amount > 0) {
$parameters['amount'] = $amount;
}
$parameters['applePayEnabled'] = $applePayEnabled;
if (!empty($currency)) {
$parameters['currency'] = $currency;
}
if (!empty($buyerExternalId)) {
$parameters['externalBuyerId'] = $buyerExternalId;
}
try {
if (empty($idempotencyKey)) {
$idempotencyKey = md5($currency . '_' . $amount . '_' . $this->getClient()->getConfiguration()->getApiKey());
}
$decodedApiResponse = $this->getClient()
->getHttpClient()
->get(
Configuration::API_VERSION_V2 . '/payments/paymentmethods',
http_build_query($parameters, '', '&')
)
->decode();
->getHttpClient()
->get(
Configuration::API_VERSION_V3 . '/payments/paymentmethods',
http_build_query($parameters, '', '&'),
$idempotencyKey
)
->decode();
return new PaymentMethods($decodedApiResponse);
} catch (HttpClientException $exception) {
throw new PaynowException(
@@ -87,19 +103,59 @@ class Payment extends Service
}
}
/**
* @param string $externalBuyerId
* @param string $token
* @param string $idempotencyKey
* @throws PaynowException
*/
public function removeSavedInstrument(string $externalBuyerId, string $token, string $idempotencyKey): void
{
$parameters = [
'externalBuyerId' => $externalBuyerId,
'token' => $token,
];
try {
$this->getClient()
->getHttpClient()
->delete(
Configuration::API_VERSION_V3 . '/payments/paymentmethods/saved',
$idempotencyKey,
http_build_query($parameters, '', '&')
);
} catch (HttpClientException $exception) {
throw new PaynowException(
$exception->getMessage(),
$exception->getStatus(),
$exception->getBody(),
$exception
);
}
}
/**
* Retrieve payment status
*
* @param string $paymentId
* @throws PaynowException
* @param string|null $idempotencyKey
* @return Status
* @throws PaynowException
*/
public function status(string $paymentId): Status
public function status(string $paymentId, ?string $idempotencyKey = null): Status
{
try {
if (empty($idempotencyKey)) {
$idempotencyKey = md5($paymentId);
}
$decodedApiResponse = $this->getClient()
->getHttpClient()
->get(Configuration::API_VERSION . "/payments/$paymentId/status")
->get(
Configuration::API_VERSION_V3 . "/payments/$paymentId/status",
null,
$idempotencyKey
)
->decode();
return new Status($decodedApiResponse->paymentId, $decodedApiResponse->status);

View File

@@ -16,8 +16,8 @@ class Refund extends Service
* @param string $idempotencyKey
* @param int $amount
* @param null $reason
* @throws PaynowException
* @return Status
* @throws PaynowException
*/
public function create(string $paymentId, string $idempotencyKey, int $amount, $reason = null): Status
{
@@ -25,7 +25,7 @@ class Refund extends Service
$decodedApiResponse = $this->getClient()
->getHttpClient()
->post(
'/' . Configuration::API_VERSION . '/payments/' . $paymentId . '/refunds',
'/' . Configuration::API_VERSION_V3 . '/payments/' . $paymentId . '/refunds',
[
'amount' => $amount,
'reason' => $reason
@@ -45,17 +45,25 @@ class Refund extends Service
}
/**
* Retrieve refund status
* @param $refundId
* @throws PaynowException
* @param string|null $idempotencyKey
* @return Status
* @throws PaynowException
*/
public function status($refundId): Status
public function status($refundId, ?string $idempotencyKey = null): Status
{
try {
if (empty($idempotencyKey)) {
$idempotencyKey = md5($refundId);
}
$decodedApiResponse = $this->getClient()
->getHttpClient()
->get(Configuration::API_VERSION . "/refunds/$refundId/status")
->get(
Configuration::API_VERSION_V3 . "/refunds/$refundId/status",
null,
$idempotencyKey
)
->decode();
return new Status($decodedApiResponse->refundId, $decodedApiResponse->status);

View File

@@ -9,24 +9,35 @@ use Paynow\HttpClient\HttpClientException;
class ShopConfiguration extends Service
{
/**
* @param string $continueUrl
* @param string $notificationUrl
* @throws PaynowException
* @return ApiResponse
*/
public function changeUrls(string $continueUrl, string $notificationUrl)
public const STATUS_ENABLED = 'ENABLED';
public const STATUS_DISABLED = 'DISABLED';
public const STATUS_UNINSTALLED = 'UNINSTALLED';
public const STATUS_UPDATED = 'UPDATED';
/**
* @param string $continueUrl
* @param string $notificationUrl
* @param string|null $idempotencyKey
* @return ApiResponse
* @throws PaynowException
*/
public function changeUrls(string $continueUrl, string $notificationUrl, ?string $idempotencyKey = null): ApiResponse
{
$data = [
'continueUrl' => $continueUrl,
'notificationUrl' => $notificationUrl,
];
try {
if (empty($idempotencyKey)) {
$idempotencyKey = md5($this->getClient()->getConfiguration()->getApiKey());
}
return $this->getClient()
->getHttpClient()
->patch(
'/' . Configuration::API_VERSION.'/configuration/shop/urls',
$data
'/' . Configuration::API_VERSION_V3 . '/configuration/shop/urls',
$data,
$idempotencyKey
);
} catch (HttpClientException $exception) {
throw new PaynowException(
@@ -37,4 +48,31 @@ class ShopConfiguration extends Service
);
}
}
/**
* @param array $statuses
* @return ApiResponse
* @throws PaynowException
*/
public function status(array $statuses): ApiResponse
{
try {
$idempotencyKey = md5($this->getClient()->getConfiguration()->getApiKey());
return $this->getClient()
->getHttpClient()
->post(
'/' . Configuration::API_VERSION_V3 . '/configuration/shop/plugin/status',
$statuses,
$idempotencyKey
);
} catch (HttpClientException $exception) {
throw new PaynowException(
$exception->getMessage(),
$exception->getStatus(),
$exception->getBody(),
$exception
);
}
}
}

View File

@@ -15,6 +15,56 @@ class SignatureCalculator
* @throws InvalidArgumentException
*/
public function __construct(string $signatureKey, string $data)
{
$this->hash = self::generate($signatureKey, $data);
}
/**
* @param string $apiKey
* @param string $signatureKey
* @param string $idempotencyKey
* @param string $data
* @param array $parameters
* @return string
*/
public static function generateV3(string $apiKey, string $signatureKey, string $idempotencyKey, string $data = '', array $parameters = []): string
{
if (empty($apiKey)) {
throw new InvalidArgumentException('You did not provide a api key');
}
if (empty($signatureKey)) {
throw new InvalidArgumentException('You did not provide a Signature key');
}
if (empty($idempotencyKey)) {
throw new InvalidArgumentException('You did not provide a idempotency key');
}
$parsedParameters = [];
foreach ($parameters as $key => $value) {
$parsedParameters[$key] = is_array($value) ? $value : [$value];
}
$signatureBody = [
'headers' => [
'Api-Key' => $apiKey,
'Idempotency-Key' => $idempotencyKey,
],
'parameters' => $parsedParameters ?: new \stdClass(),
'body' => $data,
];
return base64_encode(hash_hmac('sha256', json_encode($signatureBody, JSON_UNESCAPED_SLASHES), $signatureKey, true));
}
/**
* @param string $signatureKey
* @param string $data
* @return string
*/
public static function generate(string $signatureKey, string $data): string
{
if (empty($signatureKey)) {
throw new InvalidArgumentException('You did not provide a Signature key');
@@ -23,7 +73,8 @@ class SignatureCalculator
if (empty($data)) {
throw new InvalidArgumentException('You did not provide any data');
}
$this->hash = base64_encode(hash_hmac('sha256', $data, $signatureKey, true));
return base64_encode(hash_hmac('sha256', $data, $signatureKey, true));
}
/**

View File

@@ -13,6 +13,7 @@ class NotificationTest extends TestCase
* @param $payload
* @param $headers
* @throws SignatureVerificationException
* @suppress PhanNoopNew
*/
public function testVerifyPayloadSuccessfully($payload, $headers)
{
@@ -40,6 +41,11 @@ class NotificationTest extends TestCase
];
}
/**
* @return void
* @throws SignatureVerificationException
* @suppress PhanNoopNew
*/
public function testShouldThrowExceptionOnIncorrectSignature()
{
// given
@@ -53,12 +59,14 @@ class NotificationTest extends TestCase
// then
}
/**
* @suppress PhanNoopNew
* @return void
*/
public function testShouldThrowExceptionOnMissingPayload()
{
// given
$this->expectException(InvalidArgumentException::class);
$payload = null;
$headers = [];
// when
new Notification('s3ecret-k3y', null, null);
@@ -66,12 +74,16 @@ class NotificationTest extends TestCase
// then
}
/**
* @return void
* @throws SignatureVerificationException
* @suppress PhanNoopNew
*/
public function testShouldThrowExceptionOnMissingPayloadHeaders()
{
// given
$this->expectException(InvalidArgumentException::class);
$payload = $this->loadData('notification.json', true);
$headers = null;
// when
new Notification('s3ecret-k3y', $payload, null);

View File

@@ -12,6 +12,9 @@ class TestCase extends BaseTestCase
protected $client;
/**
* @suppress PhanAccessMethodInternal
*/
public function __construct($name = null, array $data = [], $dataName = '')
{
$this->client = new Client(