From 0c4c3696cb0d1159ac9a85d217b5200eef99502d Mon Sep 17 00:00:00 2001
From: Payzum
Date: Wed, 2 Sep 2026 22:26:29 +0000
Subject: [PATCH 1/2] feat(shop): add Payzum as a crypto/stablecoin payment
provider
Adds Payzum through Omnipay, with the asynchronous settlement design agreed
in #4712: a signed inbound notification endpoint completes the order, and the
buyer's return no longer fails a payment that is still confirming on-chain.
- POST /api/v2/Shop/Checkout/Notify/Payzum/{order_id}: the driver verifies
the HMAC-SHA-512 signature over the raw request bytes before any field is
readable; order, amount and currency are checked and completion is
idempotent across redeliveries.
- handlePaymentReturn refreshes the invoice once for Payzum and leaves the
order in PROCESSING while it confirms; the checkout page renders a new
'processing' state.
- Config, enum, .env.example and composer entries for payzum/omnipay-payzum.
---
.env.example | 4 +
app/Actions/Shop/CheckoutService.php | 186 ++++++++++++++++
app/Enum/OmnipayProviderType.php | 2 +
app/Factories/OmnipayFactory.php | 20 ++
.../Controllers/Shop/CheckoutController.php | 39 +++-
.../Requests/Checkout/FinalizeRequest.php | 8 +-
app/Http/Requests/Checkout/NotifyRequest.php | 89 ++++++++
composer.json | 1 +
composer.lock | 117 +++++++++-
config/omnipay.php | 11 +
lang/ar/webshop.php | 2 +
lang/bg/webshop.php | 2 +
lang/cz/webshop.php | 2 +
lang/de/webshop.php | 2 +
lang/el/webshop.php | 2 +
lang/en/webshop.php | 2 +
lang/es/webshop.php | 2 +
lang/fa/webshop.php | 2 +
lang/fr/webshop.php | 2 +
lang/hu/webshop.php | 2 +
lang/it/webshop.php | 2 +
lang/ja/webshop.php | 2 +
lang/nl/webshop.php | 2 +
lang/no/webshop.php | 2 +
lang/pl/webshop.php | 2 +
lang/pt/webshop.php | 2 +
lang/ru/webshop.php | 2 +
lang/sk/webshop.php | 2 +
lang/sv/webshop.php | 2 +
lang/tr/webshop.php | 2 +
lang/vi/webshop.php | 2 +
lang/zh_CN/webshop.php | 2 +
lang/zh_TW/webshop.php | 2 +
.../v7/components/webshop/CancelledFailed.vue | 11 +
.../v8/components/webshop/CancelledFailed.vue | 11 +
routes/api_v2_shop.php | 4 +
.../Checkout/CheckoutNotifyControllerTest.php | 206 ++++++++++++++++++
37 files changed, 752 insertions(+), 3 deletions(-)
create mode 100644 app/Http/Requests/Checkout/NotifyRequest.php
create mode 100644 tests/Webshop/Checkout/CheckoutNotifyControllerTest.php
diff --git a/.env.example b/.env.example
index 1660350de98..ab4372c238c 100644
--- a/.env.example
+++ b/.env.example
@@ -371,6 +371,10 @@ VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# PAYPAL_CLIENT_ID=
# PAYPAL_SECRET=
+# Configuration values for Payzum integration (crypto/stablecoin payments)
+# PAYZUM_API_KEY=
+# PAYZUM_WEBHOOK_SECRET=
+
###################################################################
# AI Vision (facial recognition & NSFW classification) #
###################################################################
diff --git a/app/Actions/Shop/CheckoutService.php b/app/Actions/Shop/CheckoutService.php
index 670000f6fe1..030ebabf9a1 100644
--- a/app/Actions/Shop/CheckoutService.php
+++ b/app/Actions/Shop/CheckoutService.php
@@ -20,11 +20,16 @@
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Omnipay\Common\Exception\InvalidCreditCardException;
+use Omnipay\Common\Exception\InvalidRequestException;
use Omnipay\Common\GatewayInterface;
+use Omnipay\Common\Message\NotificationInterface;
use Omnipay\Common\Message\RedirectResponseInterface;
use Omnipay\Common\Message\ResponseInterface;
use Omnipay\Dummy\Message\Response as DummyResponse;
use Omnipay\Mollie\Message\Response\FetchTransactionResponse;
+use Omnipay\Payzum\Gateway as PayzumGateway;
+use Omnipay\Payzum\Message\Response\FetchTransactionResponse as PayzumFetchTransactionResponse;
+use Omnipay\Payzum\Message\Response\PurchaseResponse as PayzumPurchaseResponse;
/**
* Service for handling checkout operations using Omnipay.
@@ -95,6 +100,12 @@ public function processPayment(Order $order, string $return_url, string $cancel_
Session::put('metadata.' . $order->id, $metadata);
}
+ if ($response instanceof PayzumPurchaseResponse) {
+ // Keep the gateway payment id so the return handler can
+ // refresh the payment status while it confirms on-chain.
+ Session::put('metadata.' . $order->id, ['transactionReference' => $response->getTransactionReference()]);
+ }
+
if (!$response instanceof RedirectResponseInterface) {
throw new LycheeLogicException('Expected RedirectResponseInterface for redirect response.');
}
@@ -183,6 +194,10 @@ public function handlePaymentReturn(Order $order, OmnipayProviderType $provider)
$gateway = $this->omnipay_factory->create_gateway($provider);
+ if ($provider === OmnipayProviderType::PAYZUM && $gateway instanceof PayzumGateway) {
+ return $this->handleAsyncPaymentReturn($order, $gateway, $metadata);
+ }
+
try {
if ($order->status !== PaymentStatusType::PROCESSING) {
throw new LycheeLogicException('Order with invalid status.');
@@ -204,6 +219,171 @@ public function handlePaymentReturn(Order $order, OmnipayProviderType $provider)
return $order;
}
+ /**
+ * Handle the buyer's return for asynchronous providers (crypto settles
+ * on-chain, usually after the redirect back).
+ *
+ * The order is only ever completed from a verified source: either the
+ * signed payment notification (handlePaymentNotification) or the
+ * status refresh below. A payment that is still confirming stays in
+ * PROCESSING — it must never be marked FAILED just because the buyer
+ * returned before the chain did.
+ *
+ * @param Order $order The order being processed
+ * @param PayzumGateway $gateway The initialized gateway
+ * @param array $metadata Session metadata stored at purchase time
+ *
+ * @return Order The refreshed order
+ */
+ private function handleAsyncPaymentReturn(Order $order, PayzumGateway $gateway, array $metadata): Order
+ {
+ if ($order->status !== PaymentStatusType::PROCESSING) {
+ // Already settled, e.g. the notification landed before the redirect.
+ return $order;
+ }
+
+ $transaction_reference = $metadata['transactionReference'] ?? null;
+ if (!is_string($transaction_reference) || $transaction_reference === '') {
+ // Nothing to poll (e.g. session lost): the signed notification
+ // will complete the order server-side.
+ return $order;
+ }
+
+ try {
+ $response = $gateway->fetchTransaction(['transactionReference' => $transaction_reference])->send();
+
+ if ($response->isSuccessful()) {
+ return $this->completePayment($order, $response);
+ }
+
+ if ($response instanceof PayzumFetchTransactionResponse && ($response->isExpired() || $response->isCancelled())) {
+ $order->status = PaymentStatusType::FAILED;
+ $order->save();
+ }
+ // Still pending or confirming: leave the order in PROCESSING.
+ } catch (\Exception $e) {
+ Log::error('Error refreshing async payment status: ' . $e->getMessage(), [
+ 'order_id' => $order->id,
+ 'exception' => $e,
+ ]);
+ // Leave the order in PROCESSING; the notification stays authoritative.
+ }
+
+ return $order;
+ }
+
+ /**
+ * Complete an order from a signed Payzum payment notification.
+ *
+ * The Omnipay driver verifies the HMAC-SHA-512 signature over the raw
+ * request bytes (with a replay window) before any payload field is
+ * readable — a forged, stale, or malformed delivery aborts with 400
+ * without touching the order. Deliveries are retried by the gateway,
+ * so a redelivered notification for an already-completed order is a
+ * no-op.
+ *
+ * @param Order $order The order the notification URL points at
+ *
+ * @return Order The updated order
+ */
+ public function handlePaymentNotification(Order $order): Order
+ {
+ // The current request is passed explicitly: the driver verifies the
+ // notification signature against its raw body and headers.
+ $gateway = $this->omnipay_factory->create_notification_gateway(OmnipayProviderType::PAYZUM, request());
+ if (!$gateway instanceof PayzumGateway) {
+ throw new LycheeLogicException('Expected Payzum gateway.');
+ }
+
+ $notification = $gateway->acceptNotification();
+
+ try {
+ // Accessors verify the signature on first use.
+ $status = $notification->getTransactionStatus();
+ } catch (InvalidRequestException $e) {
+ Log::warning('Rejected Payzum notification: ' . $e->getMessage(), ['order_id' => $order->id]);
+ abort(400, 'Invalid notification');
+ }
+
+ if ($order->status === PaymentStatusType::COMPLETED || $order->status === PaymentStatusType::CLOSED) {
+ // Redelivered notification: acknowledge without a second fulfilment.
+ // Checked before the reference comparison below, because completing
+ // the order replaced its transaction id with the provider reference.
+ return $order;
+ }
+
+ if ($notification->getTransactionId() !== $order->transaction_id) {
+ Log::warning('Payzum notification order mismatch.', ['order_id' => $order->id]);
+ abort(400, 'Order mismatch');
+ }
+
+ if ($status === NotificationInterface::STATUS_FAILED) {
+ // The invoice expired or failed before full payment arrived.
+ $order->status = PaymentStatusType::FAILED;
+ $order->save();
+
+ return $order;
+ }
+
+ if ($status !== NotificationInterface::STATUS_COMPLETED) {
+ // Pending or confirming: acknowledge without touching the order.
+ return $order;
+ }
+
+ $payload = $notification->getData();
+
+ if (!$this->isNotifiedAmountExpected($payload, $order)) {
+ Log::warning('Payzum notification amount/currency mismatch.', ['order_id' => $order->id]);
+ abort(400, 'Amount mismatch');
+ }
+
+ $transaction_reference = $notification->getTransactionReference();
+ if ($transaction_reference === null) {
+ abort(400, 'Missing payment reference');
+ }
+
+ $order->markAsPaid($transaction_reference);
+
+ return $order;
+ }
+
+ /**
+ * Whether a notification reports the exact amount and currency of the order.
+ *
+ * Compared as Money objects rather than floats, so the check is exact.
+ *
+ * @param array $payload The verified notification payload
+ * @param Order $order The order the notification is about
+ *
+ * @return bool
+ */
+ private function isNotifiedAmountExpected(array $payload, Order $order): bool
+ {
+ $notified_amount = $payload['price_amount'] ?? null;
+ $notified_currency = $payload['price_currency'] ?? null;
+
+ if (!is_string($notified_amount) && !is_numeric($notified_amount)) {
+ return false;
+ }
+
+ if (!is_string($notified_currency)) {
+ return false;
+ }
+
+ $expected_currency = $order->amount_cents->getCurrency()->getCode();
+ if (strtoupper($notified_currency) !== strtoupper($expected_currency)) {
+ return false;
+ }
+
+ try {
+ $notified = $this->money_service->createFromDecimal((string) $notified_amount, $expected_currency);
+ } catch (\Exception) {
+ return false;
+ }
+
+ return $notified->equals($order->amount_cents);
+ }
+
/**
* Prepare parameters for the purchase request.
*
@@ -233,6 +413,12 @@ private function preparePurchaseParameters(Order $order, string $return_url, str
'description' => 'Order #' . $order->id,
];
+ if ($this->gateway instanceof PayzumGateway) {
+ // Crypto settles asynchronously: the signed notification posted to
+ // this URL is what completes the order, not the browser return.
+ $params['notifyUrl'] = route('shop.checkout.notify', ['order_id' => $order->id]);
+ }
+
// Add customer details if available
if ($order->email !== null) {
$params['email'] = $order->email;
diff --git a/app/Enum/OmnipayProviderType.php b/app/Enum/OmnipayProviderType.php
index d5d1eb51c2f..4a8ca488d1b 100644
--- a/app/Enum/OmnipayProviderType.php
+++ b/app/Enum/OmnipayProviderType.php
@@ -22,6 +22,7 @@ enum OmnipayProviderType: string
case DUMMY = 'Dummy';
case MOLLIE = 'Mollie';
case PAYPAL = 'PayPal';
+ case PAYZUM = 'Payzum';
case STRIPE = 'Stripe';
/**
@@ -48,6 +49,7 @@ public function requiredKeys(): array
return match ($this) {
OmnipayProviderType::DUMMY => ['apiKey'],
OmnipayProviderType::MOLLIE => ['apiKey', 'profileId'],
+ OmnipayProviderType::PAYZUM => ['apiKey', 'webhookSecret'],
OmnipayProviderType::STRIPE => ['apiKey', 'publishableKey', 'disabled'], // we set disabled to prevent it from showing up.
OmnipayProviderType::PAYPAL => ['clientId', 'secret'],
};
diff --git a/app/Factories/OmnipayFactory.php b/app/Factories/OmnipayFactory.php
index d1cf038e3ba..b495eb45da7 100644
--- a/app/Factories/OmnipayFactory.php
+++ b/app/Factories/OmnipayFactory.php
@@ -14,6 +14,7 @@
use App\Exceptions\Shop\ProviderConfigurationNotFoundException;
use Omnipay\Common\GatewayInterface;
use Omnipay\Omnipay;
+use Symfony\Component\HttpFoundation\Request as HttpRequest;
class OmnipayFactory
{
@@ -38,6 +39,25 @@ public function create_gateway(OmnipayProviderType $provider): GatewayInterface
return $gateway;
}
+ /**
+ * Create a payment gateway instance bound to a specific HTTP request.
+ *
+ * Used to verify incoming payment notifications: the driver reads the raw
+ * body and headers of that request to check its signature, so it must be
+ * the request being handled and not Omnipay's default built from globals.
+ *
+ * @param OmnipayProviderType $provider
+ * @param HttpRequest $http_request
+ *
+ * @return GatewayInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function create_notification_gateway(OmnipayProviderType $provider, HttpRequest $http_request): GatewayInterface
+ {
+ return $this->initialize_gateway(Omnipay::create($provider->value, null, $http_request), $provider);
+ }
+
/**
* @param GatewayInterface $gateway
* @param OmnipayProviderType $provider
diff --git a/app/Http/Controllers/Shop/CheckoutController.php b/app/Http/Controllers/Shop/CheckoutController.php
index 20f48af78e3..202a9eebd88 100644
--- a/app/Http/Controllers/Shop/CheckoutController.php
+++ b/app/Http/Controllers/Shop/CheckoutController.php
@@ -15,12 +15,14 @@
use App\Http\Requests\Checkout\CancelRequest;
use App\Http\Requests\Checkout\CreateSessionRequest;
use App\Http\Requests\Checkout\FinalizeRequest;
+use App\Http\Requests\Checkout\NotifyRequest;
use App\Http\Requests\Checkout\OfflineRequest;
use App\Http\Requests\Checkout\ProcessRequest;
use App\Http\Resources\Shop\CheckoutOptionResource;
use App\Http\Resources\Shop\CheckoutResource;
use App\Http\Resources\Shop\OrderResource;
use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\URL;
@@ -157,7 +159,10 @@ public function finalize(FinalizeRequest $request, string $provider, string $tra
$message = 'Payment failed or was not completed.';
if ($success) {
- OrderCompleted::dispatchIf($request->configs()->getValueAsBool('webshop_auto_fulfill_enabled'), $order->id);
+ // wasChanged: only fulfil when THIS request completed the order —
+ // asynchronous providers may have completed it from the payment
+ // notification already, which also dispatched the event.
+ OrderCompleted::dispatchIf($request->configs()->getValueAsBool('webshop_auto_fulfill_enabled') && $order->wasChanged('status'), $order->id);
$complete_url = URL::route('shop.checkout.complete');
$redirect_url = null;
$message = 'Payment completed successfully.';
@@ -173,6 +178,13 @@ public function finalize(FinalizeRequest $request, string $provider, string $tra
);
}
+ if (!$success && $order->status === PaymentStatusType::PROCESSING) {
+ // Asynchronous providers (crypto): the payment is still confirming
+ // on-chain and the signed notification will complete the order.
+ // The checkout page renders the real order status.
+ return redirect()->route('shop.checkout.complete');
+ }
+
if (!$success) {
return redirect()->route('shop.checkout.failed');
}
@@ -180,6 +192,31 @@ public function finalize(FinalizeRequest $request, string $provider, string $tra
return redirect()->route('shop.checkout.complete');
}
+ /**
+ * Handle a server-to-server payment notification (Payzum).
+ *
+ * The notification signature is verified by the gateway driver before
+ * any field is read; see CheckoutService::handlePaymentNotification().
+ *
+ * @param NotifyRequest $request The request carrying the signed notification
+ * @param string $transaction_id The order transaction id from the url
+ *
+ * @return Response Empty acknowledgement; the gateway stops retrying on 2xx
+ */
+ public function notify(NotifyRequest $request, string $transaction_id): Response
+ {
+ /** @disregard P1013 */
+ $order = $this->checkout_service->handlePaymentNotification($request->basket());
+
+ // wasChanged: fulfil once. A redelivered notification for an order that
+ // is already completed must not dispatch the event a second time.
+ if ($order->status === PaymentStatusType::COMPLETED && $order->wasChanged('status')) {
+ OrderCompleted::dispatchIf($request->configs()->getValueAsBool('webshop_auto_fulfill_enabled'), $order->id);
+ }
+
+ return response()->noContent();
+ }
+
/**
* Handle cancellation of the payment process.
*
diff --git a/app/Http/Requests/Checkout/FinalizeRequest.php b/app/Http/Requests/Checkout/FinalizeRequest.php
index d69d3f7cb84..8881d271ad5 100644
--- a/app/Http/Requests/Checkout/FinalizeRequest.php
+++ b/app/Http/Requests/Checkout/FinalizeRequest.php
@@ -38,7 +38,13 @@ class FinalizeRequest extends BaseApiRequest implements HasBasket
*/
public function authorize(): bool
{
- return $this->order?->status === PaymentStatusType::PROCESSING && $this->order?->provider === $this->provider_type && $this->provider_type !== null;
+ // Asynchronous providers can complete the order from the payment
+ // notification before the buyer's browser returns; that return is
+ // still legitimate and must be able to show the completed order.
+ $is_valid_status = $this->order?->status === PaymentStatusType::PROCESSING ||
+ ($this->order?->status === PaymentStatusType::COMPLETED && $this->provider_type === OmnipayProviderType::PAYZUM);
+
+ return $is_valid_status && $this->order?->provider === $this->provider_type && $this->provider_type !== null;
}
/**
diff --git a/app/Http/Requests/Checkout/NotifyRequest.php b/app/Http/Requests/Checkout/NotifyRequest.php
new file mode 100644
index 00000000000..2c17e5576b2
--- /dev/null
+++ b/app/Http/Requests/Checkout/NotifyRequest.php
@@ -0,0 +1,89 @@
+order?->provider === OmnipayProviderType::PAYZUM &&
+ in_array($this->order?->status, [
+ PaymentStatusType::PROCESSING,
+ PaymentStatusType::CANCELLED,
+ PaymentStatusType::COMPLETED,
+ PaymentStatusType::CLOSED,
+ ], true);
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ */
+ public function rules(): array
+ {
+ return [
+ self::ORDER_ID_ATTRIBUTE => ['required', 'string'],
+ ];
+ }
+
+ protected function prepareForValidation(): void
+ {
+ /** @disregard */
+ $this->merge([
+ self::ORDER_ID_ATTRIBUTE => $this->route(self::ORDER_ID_ATTRIBUTE),
+ ]);
+ }
+
+ protected function processValidatedValues(array $values, array $files): void
+ {
+ $order = Order::find($values[self::ORDER_ID_ATTRIBUTE]);
+ if ($order === null) {
+ throw new ModelNotFoundException('Order not found.');
+ }
+ $this->order = $order;
+ }
+
+ public function basket(): ?Order
+ {
+ return $this->order;
+ }
+}
diff --git a/composer.json b/composer.json
index a0dccb2811f..df9fba92099 100644
--- a/composer.json
+++ b/composer.json
@@ -76,6 +76,7 @@
"omnipay/dummy": "^3.0",
"omnipay/mollie": "^5.5",
"omnipay/stripe": "^3.2",
+ "payzum/omnipay-payzum": "^0.1.1",
"opcodesio/log-viewer": "dev-lycheeOrg",
"paypal/paypal-server-sdk": "2.4.0",
"php-ffmpeg/php-ffmpeg": "^1.0",
diff --git a/composer.lock b/composer.lock
index 2d20936f909..04590a50a28 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e38b8c7b427ffdb9d729d6b75959a9cb",
+ "content-hash": "0008a62321fec85c47374ad9e0b87883",
"packages": [
{
"name": "apimatic/core",
@@ -6224,6 +6224,121 @@
},
"time": "2026-08-21T15:05:45+00:00"
},
+ {
+ "name": "payzum/omnipay-payzum",
+ "version": "v0.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/payzum-dev/omnipay-payzum.git",
+ "reference": "19a259aa1a46513731f6d590202554c29e61da0a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/payzum-dev/omnipay-payzum/zipball/19a259aa1a46513731f6d590202554c29e61da0a",
+ "reference": "19a259aa1a46513731f6d590202554c29e61da0a",
+ "shasum": ""
+ },
+ "require": {
+ "omnipay/common": "^3.2",
+ "payzum/payzum-php": "^0.1",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "omnipay/tests": "^4.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Omnipay\\Payzum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Payzum",
+ "homepage": "https://payzum.com"
+ }
+ ],
+ "description": "Payzum driver for the Omnipay payment processing library — accept crypto and stablecoin payments (USDC/USDT, multi-chain, non-custodial).",
+ "homepage": "https://github.com/payzum-dev/omnipay-payzum",
+ "keywords": [
+ "USDC",
+ "USDT",
+ "bitcoin",
+ "crypto",
+ "ethereum",
+ "gateway",
+ "merchant",
+ "omnipay",
+ "pay",
+ "payment",
+ "payzum",
+ "stablecoin"
+ ],
+ "support": {
+ "docs": "https://merchant.payzum.com/docs",
+ "issues": "https://github.com/payzum-dev/omnipay-payzum/issues",
+ "source": "https://github.com/payzum-dev/omnipay-payzum"
+ },
+ "time": "2026-09-02T20:39:40+00:00"
+ },
+ {
+ "name": "payzum/payzum-php",
+ "version": "v0.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/payzum-dev/payzum-php.git",
+ "reference": "c646c948883da1aad21d43d4692791604bc09bbc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/payzum-dev/payzum-php/zipball/c646c948883da1aad21d43d4692791604bc09bbc",
+ "reference": "c646c948883da1aad21d43d4692791604bc09bbc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Payzum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Payzum",
+ "homepage": "https://payzum.com"
+ }
+ ],
+ "description": "Official PHP SDK for the Payzum crypto payment API — accept stablecoin and crypto payments, verify IPN webhooks.",
+ "homepage": "https://merchant.payzum.com",
+ "keywords": [
+ "USDC",
+ "USDT",
+ "bitcoin",
+ "crypto",
+ "ethereum",
+ "payment-gateway",
+ "payments",
+ "payzum",
+ "stablecoin"
+ ],
+ "support": {
+ "docs": "https://merchant.payzum.com/docs",
+ "issues": "https://github.com/payzum-dev/payzum-php/issues",
+ "source": "https://github.com/payzum-dev/payzum-php"
+ },
+ "time": "2026-08-30T21:24:12+00:00"
+ },
{
"name": "php-ffmpeg/php-ffmpeg",
"version": "v1.4.0",
diff --git a/config/omnipay.php b/config/omnipay.php
index 08c5e4320df..aa48b332768 100644
--- a/config/omnipay.php
+++ b/config/omnipay.php
@@ -43,4 +43,15 @@
'clientId' => env('PAYPAL_CLIENT_ID', ''),
'secret' => env('PAYPAL_SECRET', ''),
],
+
+ /**
+ * Payzum gateway configuration (crypto/stablecoin payments).
+ * The webhookSecret verifies the signed payment notifications that
+ * complete orders asynchronously.
+ */
+ 'Payzum' => [
+ 'apiKey' => env('PAYZUM_API_KEY', ''),
+ 'webhookSecret' => env('PAYZUM_WEBHOOK_SECRET', ''),
+ 'testMode' => (bool) env('OMNIPAY_TEST_MODE', false),
+ ],
];
diff --git a/lang/ar/webshop.php b/lang/ar/webshop.php
index 9229f74fecd..a51da2e1634 100644
--- a/lang/ar/webshop.php
+++ b/lang/ar/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'تم إلغاء الدفع.',
'paymentFailed' => 'فشل الدفع',
'paymentFailedMessage' => 'لم نتمكن من تأكيد دفعتك. يرجى المحاولة مرة أخرى أو التواصل مع الدعم إذا استمرت المشكلة.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/bg/webshop.php b/lang/bg/webshop.php
index 88770024d94..0721928615f 100644
--- a/lang/bg/webshop.php
+++ b/lang/bg/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Плащането е отменено.',
'paymentFailed' => 'Плащането е неуспешно',
'paymentFailedMessage' => 'Не успяхме да потвърдим вашето плащане. Моля, опитайте отново или се свържете с поддръжката, ако проблемът продължава.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/cz/webshop.php b/lang/cz/webshop.php
index 3dc11977d01..3e518e0da40 100644
--- a/lang/cz/webshop.php
+++ b/lang/cz/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/de/webshop.php b/lang/de/webshop.php
index 239ef6515be..847a91e8af0 100644
--- a/lang/de/webshop.php
+++ b/lang/de/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Die Zahlung wurde abgebrochen.',
'paymentFailed' => 'Zahlung fehlgeschlagen',
'paymentFailedMessage' => 'Wir konnten Ihre Zahlung nicht bestätigen. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support, falls das Problem weiterhin besteht.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/el/webshop.php b/lang/el/webshop.php
index 40e71390cee..9a33f6e661b 100644
--- a/lang/el/webshop.php
+++ b/lang/el/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/en/webshop.php b/lang/en/webshop.php
index 0fc06225e2f..36f196bbfc4 100644
--- a/lang/en/webshop.php
+++ b/lang/en/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/es/webshop.php b/lang/es/webshop.php
index 8f951044214..a9dc9a5ea30 100644
--- a/lang/es/webshop.php
+++ b/lang/es/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/fa/webshop.php b/lang/fa/webshop.php
index d1c10ac136d..0485544b4db 100644
--- a/lang/fa/webshop.php
+++ b/lang/fa/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'پرداخت لغو شده است.',
'paymentFailed' => 'پرداخت ناموفق بود',
'paymentFailedMessage' => 'ما نتوانستیم پرداخت شما را تأیید کنیم. لطفاً دوباره تلاش کنید یا در صورت تداوم مشکل با پشتیبانی تماس بگیرید.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/fr/webshop.php b/lang/fr/webshop.php
index 8f116123079..38b4d6ebd15 100644
--- a/lang/fr/webshop.php
+++ b/lang/fr/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Le paiement a été annulé.',
'paymentFailed' => 'Échec du paiement',
'paymentFailedMessage' => 'Nous n’avons pas pu confirmer votre paiement. Veuillez réessayer ou contacter le support si le problème persiste.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/hu/webshop.php b/lang/hu/webshop.php
index 9052f4de919..37e5f0aed15 100644
--- a/lang/hu/webshop.php
+++ b/lang/hu/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/it/webshop.php b/lang/it/webshop.php
index d94231ea15f..aa3605a1b20 100644
--- a/lang/it/webshop.php
+++ b/lang/it/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/ja/webshop.php b/lang/ja/webshop.php
index 8401ad93424..31d03f9f365 100644
--- a/lang/ja/webshop.php
+++ b/lang/ja/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/nl/webshop.php b/lang/nl/webshop.php
index 27731bfe673..e7d9add7573 100644
--- a/lang/nl/webshop.php
+++ b/lang/nl/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'De betaling is geannuleerd.',
'paymentFailed' => 'Betaling mislukt',
'paymentFailedMessage' => 'We konden uw betaling niet bevestigen. Probeer het opnieuw of neem contact op met de ondersteuning als het probleem aanhoudt.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/no/webshop.php b/lang/no/webshop.php
index 60f7933e43b..c5e6d99e5ba 100644
--- a/lang/no/webshop.php
+++ b/lang/no/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Betalingen er kansellert.',
'paymentFailed' => 'Betaling mislyktes',
'paymentFailedMessage' => 'Vi kunne ikke bekrefte betalingen din. Vennligst prøv igjen eller kontakt support hvis problemet vedvarer.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/pl/webshop.php b/lang/pl/webshop.php
index 4e7c147fee2..221cb379723 100644
--- a/lang/pl/webshop.php
+++ b/lang/pl/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/pt/webshop.php b/lang/pt/webshop.php
index e12136d3434..c7bf71b1c07 100644
--- a/lang/pt/webshop.php
+++ b/lang/pt/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/ru/webshop.php b/lang/ru/webshop.php
index 322132a83d3..a781ce7cd8c 100644
--- a/lang/ru/webshop.php
+++ b/lang/ru/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/sk/webshop.php b/lang/sk/webshop.php
index eb12b53c508..d141e650621 100644
--- a/lang/sk/webshop.php
+++ b/lang/sk/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/sv/webshop.php b/lang/sv/webshop.php
index f451e24ee1b..9afee481648 100644
--- a/lang/sv/webshop.php
+++ b/lang/sv/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/tr/webshop.php b/lang/tr/webshop.php
index a085dd2c67d..3e1842c29f2 100644
--- a/lang/tr/webshop.php
+++ b/lang/tr/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/vi/webshop.php b/lang/vi/webshop.php
index c0dd0805895..bc8359918ef 100644
--- a/lang/vi/webshop.php
+++ b/lang/vi/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/zh_CN/webshop.php b/lang/zh_CN/webshop.php
index 16a77da54e7..aa9da115162 100644
--- a/lang/zh_CN/webshop.php
+++ b/lang/zh_CN/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/lang/zh_TW/webshop.php b/lang/zh_TW/webshop.php
index 172e1680cb6..d3f115510fa 100644
--- a/lang/zh_TW/webshop.php
+++ b/lang/zh_TW/webshop.php
@@ -285,5 +285,7 @@
'paymentCancelledMessage' => 'Payment has been cancelled.',
'paymentFailed' => 'Payment failed',
'paymentFailedMessage' => 'We were not able to confirm your payment. Please try again or contact support if the problem persists.',
+ 'paymentProcessing' => 'Payment being confirmed',
+ 'paymentProcessingMessage' => 'Your payment is being confirmed by the payment provider. The order will complete automatically once the payment is confirmed — you can safely close this page.',
],
];
diff --git a/resources/js/v7/components/webshop/CancelledFailed.vue b/resources/js/v7/components/webshop/CancelledFailed.vue
index 74a62fbdc39..9b8bdef5d6d 100644
--- a/resources/js/v7/components/webshop/CancelledFailed.vue
+++ b/resources/js/v7/components/webshop/CancelledFailed.vue
@@ -21,6 +21,17 @@
+
+
{{ $t("webshop.cancelledFailed.paymentProcessing") }}
+
+
+ {{ $t("webshop.cancelledFailed.paymentProcessingMessage") }}
+
+
+