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..76c432f786c 100644 --- a/app/Actions/Shop/CheckoutService.php +++ b/app/Actions/Shop/CheckoutService.php @@ -17,14 +17,20 @@ use App\Factories\OmnipayFactory; use App\Models\Order; use App\Services\MoneyService; +use Illuminate\Support\Facades\DB; 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 +101,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.'); } @@ -163,11 +175,51 @@ public function processPayment(Order $order, string $return_url, string $cancel_ public function completePayment(Order $order, ResponseInterface $response): Order { $transaction_id = $response->getTransactionReference(); - $order->markAsPaid($transaction_id); + $this->settle($order, $transaction_id); return $order; } + /** + * Mark an order as paid, at most once. + * + * The browser return and an inbound payment notification can arrive at the + * same moment, and both would otherwise observe PROCESSING and settle the + * order — dispatching OrderCompleted (and thus fulfilling) twice. The row + * is locked and re-read inside a transaction, so exactly one caller + * performs the transition; the loser sees the fresh state and reports that + * it changed nothing. + * + * @param Order $order The order to settle + * @param string $transaction_id The reference to store on the order + * + * @return bool Whether THIS call completed the order + */ + private function settle(Order $order, string $transaction_id): bool + { + return DB::transaction(function () use ($order, $transaction_id): bool { + $fresh = Order::query()->whereKey($order->getKey())->lockForUpdate()->first(); + + if ($fresh === null) { + return false; + } + + if (in_array($fresh->status, [PaymentStatusType::COMPLETED, PaymentStatusType::CLOSED], true)) { + // Someone else settled it first; adopt their state without + // claiming the transition. + $order->refresh(); + + return false; + } + + // Saved through the caller's instance, while this transaction holds + // the row lock, so wasChanged('status') is true for the winner only. + $order->markAsPaid($transaction_id); + + return true; + }); + } + /** * Handle the return from the payment gateway. * @@ -183,6 +235,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 +260,181 @@ 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()) { + // Same reasoning as in handlePaymentNotification(): the order's + // own transaction id has to stay the stable lookup key. + $this->settle($order, $order->transaction_id); + + return $order; + } + + 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'); + } + + if ($notification->getTransactionReference() === null) { + abort(400, 'Missing payment reference'); + } + + // Settled with the order's own transaction id rather than the gateway + // reference: that id is the lookup key of both the return and the + // notification URLs, and replacing it would make every later request + // for this order — including the buyer's browser return after an + // early notification — fail to resolve. The Payzum invoice stays + // reachable by it, since their API reads an invoice by payment id or + // by order id. + $this->settle($order, $order->transaction_id); + + 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 +464,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..42e68c50bb3 100644 --- a/lang/ar/webshop.php +++ b/lang/ar/webshop.php @@ -285,5 +285,7 @@ 'paymentCancelledMessage' => 'تم إلغاء الدفع.', 'paymentFailed' => 'فشل الدفع', 'paymentFailedMessage' => 'لم نتمكن من تأكيد دفعتك. يرجى المحاولة مرة أخرى أو التواصل مع الدعم إذا استمرت المشكلة.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..48bbbae399c 100644 --- a/lang/bg/webshop.php +++ b/lang/bg/webshop.php @@ -285,5 +285,7 @@ 'paymentCancelledMessage' => 'Плащането е отменено.', 'paymentFailed' => 'Плащането е неуспешно', 'paymentFailedMessage' => 'Не успяхме да потвърдим вашето плащане. Моля, опитайте отново или се свържете с поддръжката, ако проблемът продължава.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..66f1b40bc32 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..89a369bd22e 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..065fb1543c7 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..41c2c81f38c 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..98e9703d1c6 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..6c403821946 100644 --- a/lang/fa/webshop.php +++ b/lang/fa/webshop.php @@ -285,5 +285,7 @@ 'paymentCancelledMessage' => 'پرداخت لغو شده است.', 'paymentFailed' => 'پرداخت ناموفق بود', 'paymentFailedMessage' => 'ما نتوانستیم پرداخت شما را تأیید کنیم. لطفاً دوباره تلاش کنید یا در صورت تداوم مشکل با پشتیبانی تماس بگیرید.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..c58cf6edd34 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..b1f2d3b8631 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..86962229b78 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..45d4d065c56 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..b48551e1a89 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..3897229eea9 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..4af2898aa34 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..37f2f7dc27b 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..a0069c32213 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..b77cca4b9f2 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..fa2e3b73ace 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..caec3d6de61 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..48b2b7f1afc 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..231860e1e91 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..f8c56c82cd8 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.', + 'payment_processing' => 'Payment being confirmed', + 'payment_processing_message' => '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..1a105a4af62 100644 --- a/resources/js/v7/components/webshop/CancelledFailed.vue +++ b/resources/js/v7/components/webshop/CancelledFailed.vue @@ -21,6 +21,17 @@

+
+

{{ $t("webshop.cancelledFailed.payment_processing") }}

+
+

+ {{ $t("webshop.cancelledFailed.payment_processing_message") }} +

+
+