Na wstępie powiem, że moja klasa Przelewy24Service działa poprawnie w Laravelu 11, ale używa curla i starej składni gdzie pola zawierają przedrostek p24_. Chciałem przerobić ja zgodnie z najnowszą dokumentacją, ale po modyfikacji dostaję response z curla, że CRC jest niepoprawne. Rzecz w tym, że nie zmieniam żadnych kluczy, używam tych samych.
Oto moja klasa:
<?php
namespace App\Services\Api\Przelewy24;
use App\DTOs\CounterpartyPaymentKeys;
use App\Interfaces\PaymentInterface;
use App\Models\Counterparty;
use App\Models\Order;
use App\Models\Payment;
use App\Services\Api\Przelewy24\Exceptions\Przelewy24ConnectionException;
use App\Services\Api\Przelewy24\Exceptions\Przelewy24TokenException;
use App\Services\Api\Przelewy24\Exceptions\Przelewy24VerifyPaymentException;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class Przelewy24Service
{
private const PRZELEWY24_API_VERSION = '3.2';
private const PRZELEWY24_COUNTRY = PaymentInterface::PAYMENT_COUNTRY;
private const PRZELEWY24_CURRENCY = PaymentInterface::PAYMENT_CURRENCY;
private const PRZELEWY24_TIME_LIMIT = PaymentInterface::PAYMENT_TIME_LIMIT_IN_MINUTES;
private const PRZELEWY24_WAIT_FOR_RESULT = false;
/**
* @throws Przelewy24ConnectionException
* @throws Przelewy24TokenException
*/
public function pay(
Order $order,
Payment $payment,
): string {
/** @var Counterparty $orderCounterparty */
$orderCounterparty = $order->counterparty;
$orderCounterpartyCanConnect = $this->counterpartyCanConnect($orderCounterparty);
if (!$orderCounterpartyCanConnect) {
throw new Przelewy24ConnectionException(
sprintf(
'Counterparty %s cannot establish connection.',
$orderCounterparty->getKey(),
)
);
}
$paymentKeys = $orderCounterparty->getPaymentKeys();
$amount = $order->convertOrderTotalToCents();
$description = sprintf('%s: %s', config('app.name'), $order->order_number);
$language = $this->getPaymentLanguageByOrderLanguage($order);
$payerEmailAddress = $order->user->email;
$sessionId = $payment->getPaymentSessionId();
$signature = $this->generateSignature($sessionId, $amount, self::PRZELEWY24_CURRENCY, $paymentKeys);
$urlReturn = $this->generateReturnUrl($order);
$urlStatus = $this->generateStatusUrl();
$token = $this->createToken(
paymentKeys: $paymentKeys,
sessionId: $sessionId,
amount: $amount,
description: $description,
payerEmailAddress: $payerEmailAddress,
urlReturn: $urlReturn,
urlStatus: $urlStatus,
language: $language,
signature: $signature,
);
$order->payment_uuid = $payment->getKey();
$order->save();
$paymentUrl = sprintf('https://%s.przelewy24.pl/trnRequest/%s', $this->getEnvironment(), $token);
$payment->savePaymentUrl($paymentUrl);
return $paymentUrl;
}
/**
* @throws Przelewy24TokenException
*/
private function createToken(
CounterpartyPaymentKeys $paymentKeys,
string $sessionId,
int $amount,
string $description,
string $payerEmailAddress,
string $urlReturn,
string $urlStatus,
string $language,
string $signature,
): string {
$urlTransactionRegister = sprintf('https://%s.przelewy24.pl/trnRegister', $this->getEnvironment());
$payerEmailAddress = Str::replace('+', '%2b', $payerEmailAddress);
$headers[] = 'p24_amount=' . $amount;
$headers[] = 'p24_api_version=' . self::PRZELEWY24_API_VERSION;
$headers[] = 'p24_country=' . self::PRZELEWY24_COUNTRY;
$headers[] = 'p24_crc=' . $paymentKeys->crc;
$headers[] = 'p24_currency=' . self::PRZELEWY24_CURRENCY;
$headers[] = 'p24_description=' . $description;
$headers[] = 'p24_email=' . $payerEmailAddress;
$headers[] = 'p24_language=' . $language;
$headers[] = 'p24_merchant_id=' . $paymentKeys->merchantId;
$headers[] = 'p24_pos_id=' . $paymentKeys->merchantId;
$headers[] = 'p24_session_id=' . $sessionId;
$headers[] = 'p24_sign=' . $signature;
$headers[] = 'p24_time_limit=' . self::PRZELEWY24_TIME_LIMIT;
$headers[] = 'p24_url_return=' . urlencode($urlReturn);
$headers[] = 'p24_url_status=' . urlencode($urlStatus);
$headers[] = 'p24_wait_for_result=' . self::PRZELEWY24_WAIT_FOR_RESULT;
$response = $this->makeCurlRequest($headers, $urlTransactionRegister);
parse_str($response, $output);
return $output['token'] ?? throw new Przelewy24TokenException($output['errorMessage'] ?? '');
}
/**
* @throws Przelewy24VerifyPaymentException
*/
public function verify(
Request $request,
Payment $payment,
): true {
$counterparty = $payment->order->counterparty;
/** @var CounterpartyPaymentKeys $counterpartyPaymentKeys */
$counterpartyPaymentKeys = $counterparty->getPaymentKeys();
$signature = sprintf(
'%s|%s|%s|%s|%s',
$request->post('p24_session_id'),
$request->post('p24_order_id'),
$request->post('p24_amount'),
self::PRZELEWY24_CURRENCY,
$counterpartyPaymentKeys->crc,
);
$signature = md5($signature);
$headers[] = 'p24_amount=' . $request->post('p24_amount');
$headers[] = 'p24_currency=' . self::PRZELEWY24_CURRENCY;
$headers[] = 'p24_merchant_id=' . $request->post('p24_merchant_id');
$headers[] = 'p24_order_id=' . $request->post('p24_order_id');
$headers[] = 'p24_pos_id=' . $request->post('p24_pos_id');
$headers[] = 'p24_session_id=' . $request->post('p24_session_id');
$headers[] = 'p24_sign=' . $signature;
$urlTransactionVerify = sprintf('https://%s.przelewy24.pl/trnVerify', $this->getEnvironment());
$response = $this->makeCurlRequest($headers, $urlTransactionVerify);
parse_str($response, $output);
if (isset($output['error']) && $output['error'] !== '0') {
throw new Przelewy24VerifyPaymentException($output['errorMessage'] ?? '');
}
return true;
}
public function counterpartyCanConnect(
Counterparty $counterparty,
): bool {
$urlTestConnection = sprintf('https://%s.przelewy24.pl/api/v1/testAccess', $this->getEnvironment());
$paymentKeys = $counterparty->getPaymentKeys();
return Http::withBasicAuth($paymentKeys->merchantId, $paymentKeys->apiKey)->get($urlTestConnection)->ok();
}
public function counterpartiesCanConnect(
Collection $counterparties
): Collection {
$urlTestConnection = sprintf('https://%s.przelewy24.pl/api/v1/testAccess', $this->getEnvironment());
$counterpartiesData = $counterparties->mapWithKeys(fn($counterparty) => [
$counterparty->getKey() => $counterparty->getPaymentKeys(),
]);
$responses = Http::pool(
fn($pool) => $counterpartiesData
->map(
fn($paymentKeys) => $pool
->withBasicAuth($paymentKeys->merchantId, $paymentKeys->apiKey)
->get($urlTestConnection)
)
->all()
);
return $counterpartiesData
->keys()
->combine(collect($responses)->map(fn($response) => $response->ok()));
}
private function generateSignature(
string $sessionId,
int $amount,
string $currency,
CounterpartyPaymentKeys $paymentKeys,
): string {
$signature = sprintf(
'%s|%s|%d|%s|%s',
$sessionId,
$paymentKeys->merchantId,
$amount,
$currency,
$paymentKeys->crc,
);
return md5($signature);
}
private function generateReturnUrl(Order $order): string
{
return route('frontend.orders.show', $order);
}
private function generateStatusUrl(): string
{
return route('frontend.payments.verify');
}
/**
* todo
*
* @param Order $order
*
* @return string
*/
private function getPaymentLanguageByOrderLanguage(Order $order): string
{
return 'PL';
}
/**
* Should be either:
*
* - sandbox - for testing
* - secure - for production
*
* @return string
*/
private function getEnvironment(): string
{
return config('przelewy24.environment');
}
private function makeCurlRequest(array $headers, string $curlUrl): bool|string
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, implode('&', $headers));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'TLSv1');
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_URL, $curlUrl);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}
Metoda pay() ma za zadanie zwrócić mi url'a do P24 gdzie użytkownik będzie mógł zapłacić, po płatności wraca na $urlReturn i po chwili P24 weryfikuje płatność poprzez wysłanie POST requesta do mojego routa z weryfikacją. Testowałem z ngrokiem i wszystko działa.
HTTP Requests
-------------
18:40:25.210 CEST GET /favicon-16x16.png 200 OK
18:40:13.240 CEST GET /orders/9ef936a1-0104-4f0e-aa33-1a9bf55cac93 200 OK
18:40:12.960 CEST POST /payments/verify 200 OK <----- tu dzwoni do mnie P24 i jest wszystko poprawnie
18:40:04.888 CEST POST /cart/checkout/summary 302 Found
18:40:01.828 CEST GET /images/p24-logo.svg 200 OK
18:40:00.636 CEST GET /cart/checkout/summary 200 OK
18:39:59.642 CEST POST /cart/checkout/start 302 Found
18:39:56.380 CEST GET /cart/checkout/start 200 OK
18:39:53.874 CEST GET /cart 200 OK
18:39:51.838 CEST GET /
Moje pytanie czy w ogóle ruszać tą klasę czy nie? W teorii wszystko działa, ale jak zaczynam to przepisywać z użyciem guzzle'a (wstrzykuję http clienta do kontrolera) albo fasady Http w Laravelu to jak dochodzi do generowania tokenu to mam info, że błędne CRC. W nowej dokumentacji widzę, że sygnatura nie jest już generowana z md5 tylko z sha384 itp.
Czy ktoś może mi zasugerować czy użycie tej klasy w takiej formie jest OK w roku 2025? W teorii nie chcę sobie robić niepotrzebnej dodatkowej pracy (bo nie wiem dlaczego dla tych samych merchantId, apiKey i crc wszystko działa na klasie którą tu wkleiłem, ale jak przerabiam to już nie. Co ciekawe w metodzie counterpartyCanConnect() którą napisałem z odwołaniem do api/v1 i Http::withBasicAuth połączenie jest poprawne.
Proszę o opinie.