diff --git a/REVOLUT_IMPLEMENTATION.md b/REVOLUT_IMPLEMENTATION.md
new file mode 100644
index 0000000..d25547a
--- /dev/null
+++ b/REVOLUT_IMPLEMENTATION.md
@@ -0,0 +1,338 @@
+# Revolut Payment Integration Guide
+
+This guide explains how to implement Revolut payments using the unusualify/payable package.
+
+## Prerequisites
+
+1. Revolut Merchant Account
+2. API credentials from Revolut dashboard
+3. `unusualify/payable` package installed
+4. `@revolut/checkout` npm package for frontend
+
+## Installation
+
+1. Install the npm package:
+```bash
+npm install @revolut/checkout
+```
+
+## Backend Implementation
+
+### 1. Controller Method
+
+Create a controller method to handle the payment initialization:
+
+```php
+public function testRevolut()
+{
+ $payable = new Payable('revolut');
+
+ $params = [
+ 'amount' => 10.00, // Amount in major units (e.g., 10.00 EUR)
+ 'currency' => 'EUR', // Currency code (e.g., EUR, USD, GBP)
+ 'order_id' => 'ORDER-'.uniqid(), // Unique order ID
+ 'user_email' => 'customer@example.com',
+ 'user_id' => 1,
+ 'installment' => 1, // Number of installments (1 for one-time payment)
+ 'user_ip' => request()->ip(),
+ 'description' => 'Order description',
+ ];
+
+ $result = $payable->pay($params);
+
+ if (($result['type'] ?? null) === 'widget') {
+ return view('checkout.revolut', [
+ 'token' => $result['token'] ?? '',
+ 'env' => $result['env'] ?? 'sandbox',
+ ]);
+ }
+
+ abort(400, $result['message'] ?? 'Unable to initialize Revolut order');
+}
+```
+
+## Frontend Implementation
+
+### 1. Vue Component Implementation
+
+Create a new Vue component at `resources/js/Components/RevolutCheckout.vue`:
+
+```vue
+
+
+
Revolut Checkout
+
+
+
+
Cardholder name
+
+
+
+ Pay
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Usage in your Vue application:
+
+1. First, import and register the component:
+
+```javascript
+// In your app.js or main.js
+import RevolutCheckout from './Components/RevolutCheckout.vue';
+
+// If using Vue 3
+app.component('RevolutCheckout', RevolutCheckout);
+
+// If using Vue 2
+// Vue.component('RevolutCheckout', RevolutCheckout);
+```
+
+2. Use the component in your view:
+
+```vue
+
+
+
Complete Your Payment
+
+
+
+
+
+```
+
+### 2. Blade Template (Alternative to Vue) (`resources/views/checkout/revolut.blade.php`)
+
+```html
+
+
+
+
+
+ Revolut Checkout
+
+
+
+
+
Revolut Checkout
+
+
+
+
Cardholder name
+
+
+
+ Pay
+
+
+
+
+
+
+ @vite(['resources/js/revolut.js'])
+
+
+```
+
+### 2. JavaScript (`resources/js/revolut.js`)
+
+```javascript
+import RevolutCheckout from '@revolut/checkout';
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const token = document.querySelector('input[name="token"]').value;
+ const env = document.querySelector('input[name="env"]').value;
+
+ if (!token) {
+ console.error('Revolut token missing.');
+ return;
+ }
+
+ const loader = document.getElementById('revolut-loader');
+ const showLoader = () => { loader.style.display = 'flex'; };
+ const hideLoader = () => { loader.style.display = 'none'; };
+
+ const setButtonBusy = (btn, busy) => {
+ if (!btn) return;
+ if (busy) {
+ btn.disabled = true;
+ if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent;
+ btn.textContent = 'Processing...';
+ btn.setAttribute('aria-busy', 'true');
+ } else {
+ btn.disabled = false;
+ btn.textContent = btn.dataset.originalText || 'Pay';
+ btn.removeAttribute('aria-busy');
+ }
+ };
+
+ try {
+ const instance = await RevolutCheckout(token, env);
+ const cardTarget = document.getElementById('card-field');
+
+ if (cardTarget) {
+ const submitBtn = document.getElementById('button-submit');
+
+ const cardField = instance.createCardField({
+ target: cardTarget,
+ onSuccess() {
+ hideLoader();
+ setButtonBusy(submitBtn, false);
+ // Handle successful payment (e.g., redirect to success page)
+ window.location.href = '/payment/success';
+ },
+ onError(error) {
+ hideLoader();
+ setButtonBusy(submitBtn, false);
+ alert(`Payment failed: ${error.message || 'Unknown error'}`);
+ },
+ });
+
+ if (submitBtn) {
+ submitBtn.addEventListener('click', () => {
+ const nameInput = document.querySelector('input[name="cardholder-name"]');
+ const cardholderName = (nameInput && nameInput.value) ? nameInput.value : '';
+ const meta = { name: cardholderName, cardholderName };
+
+ showLoader();
+ setButtonBusy(submitBtn, true);
+ cardField.submit(meta);
+ });
+ }
+ }
+ } catch (e) {
+ console.error('Failed to initialize Revolut', e);
+ alert('Failed to initialize payment: ' + (e?.message || e));
+ }
+});
+```
+
+## Configuration
+
+Make sure to set up your Revolut API credentials in your `.env` file:
+
+```env
+REVOLUT_ENV=sandbox # or 'production' for live
+REVOLUT_PUBLIC_KEY=your_public_key
+REVOLUT_SECRET_KEY=your_secret_key
+```
diff --git a/config/payable.php b/config/payable.php
index d6c67d0..4efeb34 100644
--- a/config/payable.php
+++ b/config/payable.php
@@ -132,5 +132,19 @@
'secret_key' => env('BUCKAROO_LIVE_SECRET_KEY', ''),
],
],
+ 'revolut' => [
+ 'mode' => env('REVOLUT_MODE', 'sandbox'),
+ 'api_version' => env('REVOLUT_API_VERSION', '2024-05-01'),
+ 'sandbox' => [
+ 'api_url' => env('REVOLUT_SANDBOX_API_URL', ''),
+ 'public_key' => env('REVOLUT_SANDBOX_PUBLIC_KEY', ''),
+ 'secret_key' => env('REVOLUT_SANDBOX_SECRET_KEY', ''),
+ ],
+ 'live' => [
+ 'api_url' => env('REVOLUT_LIVE_API_URL', ''),
+ 'public_key' => env('REVOLUT_LIVE_PUBLIC_KEY', ''),
+ 'secret_key' => env('REVOLUT_LIVE_SECRET_KEY', ''),
+ ],
+ ],
],
];
diff --git a/src/Contracts/ShouldEmbedForm.php b/src/Contracts/ShouldEmbedForm.php
new file mode 100644
index 0000000..a535050
--- /dev/null
+++ b/src/Contracts/ShouldEmbedForm.php
@@ -0,0 +1,30 @@
+ 1000,
+ 'amount' => 1,
'installment' => 1,
'currency' => 'TRY',
'locale' => 'tr',
- 'order_id' => 'TEST-'.uniqid(),
- 'card_name' => 'John Doe',
+ 'order_id' => 'TEST-' . uniqid(),
+ 'card_name' => 'Ramazan Ayyildiz',
+ 'card_no' => '4912055018403926',
'company_name' => 'B2Press',
- 'card_no' => '9792290735587016',
- 'card_month' => '02',
- 'card_year' => '2026',
- 'card_cvv' => '790',
+ 'card_month' => '03',
+ 'card_year' => '2029',
+ 'card_cvv' => '659',
'user_email' => 'test@example.com',
'user_ip' => request()->ip(),
];
-
+
return $payable->pay($params);
}
public function testTebCommon()
{
$payable = new Payable('teb-common-pos');
-
+
$params = [
'paid_price' => 1,
'installment' => 1,
'currency' => 'TRY',
'locale' => 'tr',
- 'order_id' => 'TEST-'.uniqid(),
+ 'order_id' => 'TEST-' . uniqid(),
'card_name' => 'Ramazan Ayyildiz',
'card_no' => '4912055018403926',
'company_name' => 'B2Press',
@@ -49,39 +48,39 @@ public function testTebCommon()
'user_email' => 'test@example.com',
'user_ip' => request()->ip(),
];
-
+
return $payable->pay($params);
}
public function testPaypal()
{
$payable = new Payable('paypal');
-
+
$params = [
'paid_price' => 100.10,
'installment' => 1,
'currency' => 'USD',
- 'order_id' => 'TEST-'.uniqid(),
+ 'order_id' => 'TEST-' . uniqid(),
'user_email' => 'test@example.com',
'user_name' => 'John',
'user_surname' => 'Doe',
'user_ip' => request()->ip(),
'company_name' => 'B2Press',
- 'locale' => 'tr-TR',
+ 'locale' => 'tr-TR'
];
-
+
return $payable->pay($params);
}
public function testPaypalRefund()
{
$payable = new Payable('paypal');
-
+
$params = [
/* 'capture_id' => '5JC53758C94870609', */
- 'payment_id' => 17,
+ 'payment_id' =>17,
];
-
+
$response = $payable->refund($params);
dd($response);
}
@@ -89,12 +88,12 @@ public function testPaypalRefund()
public function testPaypalCancel()
{
$payable = new Payable('paypal');
-
+
$params = [
'authorization_id' => ' ',
'payment_id' => 7,
];
-
+
$response = $payable->cancel($params);
dd($response);
}
@@ -102,57 +101,85 @@ public function testPaypalCancel()
public function testIdeal()
{
$payable = new Payable('ideal');
-
+
$params = [
'paid_price' => 100.10,
'installment' => 1,
- 'currency' => 'USD',
+ 'currency' => 'EUR',
'issuer' => 'ABNANL2A',
- 'order_id' => 'TEST-'.uniqid(),
+ 'order_id' => 'TEST-' . uniqid(),
'user_email' => 'test@example.com',
'user_ip' => request()->ip(),
];
-
+
return $payable->pay($params);
}
public function testIdealQr()
{
$payable = new Payable('ideal-qr');
-
+
$params = [
'paid_price' => 100.10,
'installment' => 1,
'currency' => 'USD',
'issuer' => 'ABNANL2A',
- 'order_id' => 'TEST'.uniqid(),
+ 'order_id' => 'TEST' . uniqid(),
'user_email' => 'test@example.com',
- 'description' => 'Test purchase',
+ 'description' => 'Test Payment',
'user_ip' => request()->ip(),
];
-
- $qrimageurl = $payable->pay($params);
-
+
+ $qrimageurl= $payable->pay($params);
return ' ';
}
public function testIdealRefund()
{
$payable = new Payable('ideal');
-
+
$params = [
/* 'order_id' => 'TEST-6814ef7229c4b',
'transaction_key' => '89819DD62261406A942D1FA029835852',
'paid_price' => 100.10, */
'payment_id' => 16,
];
-
+
$response = $payable->refund($params);
dd($response);
}
+
public function handleResponse(Request $request)
{
dd($request->all());
}
-}
+
+ public function testRevolut()
+ {
+ $payable = new Payable('revolut');
+
+ $params = [
+ // Use either 'paid_price' (major units) or 'amount' (minor units)
+ 'amount' => 10.00,
+ 'currency' => 'EUR',
+ 'order_id' => 'ORDER-'.uniqid(),
+ 'user_email' => 'john@example.com',
+ 'user_id' => 1,
+ 'installment' => 1,
+ 'user_ip' => request()->ip(),
+ 'description' => 'Test Revolut payment',
+ ];
+
+ $result = $payable->pay($params);
+
+ if (($result['type'] ?? null) === 'widget') {
+ return view('checkout.revolut', [
+ 'token' => $result['token'] ?? '',
+ 'env' => $result['env'] ?? 'sandbox',
+ ]);
+ }
+
+ abort(400, $result['message'] ?? 'Unable to initialize Revolut order');
+ }
+}
\ No newline at end of file
diff --git a/src/Payable.php b/src/Payable.php
index 7fb9af3..e7357f7 100644
--- a/src/Payable.php
+++ b/src/Payable.php
@@ -96,6 +96,21 @@ public function pay($payload, $paymentPayload = [])
->pay($payload, $paymentPayload);
}
+ public function checkout($payload, $paymentPayload = [])
+ {
+ $validated = $this->validatePayload($payload);
+
+ $payload = array_merge_recursive_preserve($this->getPayloadSchema(), $payload);
+
+ $payload = $this->removeExceptional($payload);
+
+ $payment = $this->createPaymentRecord($payload, $paymentPayload);
+
+ return $this->service
+ ->setPayment($payment)
+ ->checkout($payload);
+ }
+
/**
* Cancel Payment
*
@@ -236,7 +251,6 @@ public function createPaymentRecord($payload, $paymentAttributes = [])
'order_id' => $payload['order_id'],
'payment_gateway' => $this->slug,
'status' => $this->statusEnum::PENDING,
-
'parameters' => Arr::except($payload, [
'user_email',
'installment',
diff --git a/src/Services/PaymentService.php b/src/Services/PaymentService.php
index 67d9f0d..55d783d 100644
--- a/src/Services/PaymentService.php
+++ b/src/Services/PaymentService.php
@@ -3,6 +3,7 @@
namespace Unusualify\Payable\Services;
use Illuminate\Support\Str;
+use Unusualify\Payable\Contracts\ShouldEmbedForm;
abstract class PaymentService extends URequest
{
@@ -104,6 +105,13 @@ abstract class PaymentService extends URequest
*/
public static $hasCancel = false;
+ /**
+ * Service
+ *
+ * @var string
+ */
+ public static $hasBuiltInForm = false;
+
public const STATUS_PENDING = 'PENDING';
public const STATUS_COMPLETED = 'COMPLETED';
@@ -218,6 +226,19 @@ public function getStatusEnum()
return $this->statusEnum;
}
+ /**
+ * Check if the service has a built-in form and implements the ShouldEmbedForm contract
+ *
+ * @return bool
+ */
+ public static function hasBuiltInForm()
+ {
+ $implements = class_implements(static::class);
+
+ return (static::$hasBuiltInForm ?? false)
+ && in_array(ShouldEmbedForm::class, $implements);
+ }
+
public function addQueryParameters($url, $payload = [])
{
$url_parts = parse_url($url);
@@ -359,6 +380,26 @@ public static function hasCancel($payload = null)
return static::$hasCancel && (method_exists(static::class, 'isCancellable') ? (new static)->isCancellable($payload) : true);
}
+ public function checkout(array $payload)
+ {
+ if($this->hasBuiltInForm()) {
+ $validated = $this->validateCheckoutPayload($payload);
+
+ if($validated !== true) {
+ return response()->json([
+ 'status' => 'error',
+ 'message' => $validated,
+ ], 403);
+ }
+
+ $payload = $this->hydrateCheckoutPayload($payload);
+
+ return $this->getBuiltInFormAttributes($payload);
+ }
+
+ return [];
+ }
+
/**
* Validate Refund
*
diff --git a/src/Services/RevolutService.php b/src/Services/RevolutService.php
new file mode 100644
index 0000000..4dad4cf
--- /dev/null
+++ b/src/Services/RevolutService.php
@@ -0,0 +1,409 @@
+setConfig();
+ }
+
+ /**
+ * Load config
+ *
+ */
+ public function setConfig()
+ {
+ $this->config = config($this->getConfigName()); // e.g. 'payable.revolut'
+ $this->setMode($this->config['mode'] ?? 'sandbox');
+
+ $modeCfg = $this->config[$this->mode] ?? [];
+ $this->url = rtrim($modeCfg['api_url'] ?? '', '/');
+ $this->secretKey = $modeCfg['secret_key'] ?? null;
+ $this->publicKey = $modeCfg['public_key'] ?? null; // not required for this flow
+ $this->apiVersion = $this->config['api_version'] ?? null;
+
+ $this->headers = [
+ 'Authorization' => 'Bearer '.$this->secretKey,
+ 'Revolut-Api-Version' => $this->apiVersion,
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ];
+ $this->setHeaders($this->headers);
+
+ return $this;
+ }
+
+ /** Minimal validation for widget flow */
+ public function validateCheckoutPayload(array $payload)
+ {
+ $required = ['order_id', 'currency'];
+ $missing = array_diff($required, array_keys($payload));
+
+ if (!empty($missing)) {
+ return 'These keys are missing for Revolut: '.implode(', ', $missing);
+ }
+
+ return true;
+ }
+
+ public function hydrateCheckoutPayload(array $payload): array
+ {
+ $amountMinor = $this->normalizeAmountMinor($payload);
+
+ return [
+ 'amount' => $amountMinor,
+ 'currency' => $payload['currency'] ?? 'EUR',
+ 'merchant_order_ext_ref' => $payload['order_id'],
+ 'merchant_customer_ext_ref' => $payload['user_id'] ?? null,
+ 'description' => $payload['description'] ?? ('Payment for order '.$payload['order_id']),
+ 'customer' => [
+ 'email' => $payload['user_email'] ?? null,
+ ],
+ 'metadata' => [
+ 'order_id' => $payload['order_id'],
+ 'user_email' => $payload['user_email'] ?? null,
+ 'ip' => $payload['user_ip'] ?? null,
+ ],
+ // Optional: 'capture_mode' => 'automatic' | 'manual'
+ ];
+ }
+
+ /** Convert to minor units */
+ protected function normalizeAmountMinor(array $params): int
+ {
+ if (isset($params['amount'])) return (int) $params['amount']; // minor
+ if (isset($params['price'])) return (int) $params['price']; // minor (your package)
+ if (isset($params['paid_price'])) return (int) round(((float) $params['paid_price']) * 100); // major→minor
+ return 0;
+ }
+
+ /** Build Create Order payload */
+ public function hydrateParams(array $params): array
+ {
+ return [];
+ }
+
+ /** POST /orders */
+ protected function createRevolutOrder(array $payload): array
+ {
+ $resp = $this->postReq(
+ $this->url,
+ '/orders',
+ $payload,
+ $this->headers,
+ 'json',
+ $this->mode
+ );
+
+ return is_string($resp) ? (array) json_decode($resp, true) : (array) $resp;
+ }
+
+ /** GET /orders/{id} (optional UX or webhook confirm) */
+ protected function retrieveRevolutOrder(string $orderId): array
+ {
+ $resp = $this->getReq(
+ $this->url,
+ '/orders/'.urlencode($orderId),
+ [],
+ $this->headers
+ );
+
+ return is_string($resp) ? (array) json_decode($resp, true) : (array) $resp;
+ }
+
+ /**
+ * Main entry for Card Field / Card Pop-up.
+ * Creates the order and returns data your JS embed will use.
+ */
+ public function createWidgetOrder_(array $params): array
+ {
+ $validated = $this->validateParams($params);
+
+ if ($validated !== true) return ['error' => $validated];
+
+ $payload = $this->hydrateParams($params);
+ $revolutOrder = $this->createRevolutOrder($payload);
+
+ // Persist PENDING with raw provider response
+ $this->payment?->update([
+ 'status' => $this->getStatusEnum()::PENDING,
+ 'response' => $revolutOrder,
+ ]);
+
+ dd($this->payment, $revolutOrder);
+
+ return [
+ 'token' => $revolutOrder['token'] ?? null, // REQUIRED by RevolutCheckout()
+ 'order_id' => $revolutOrder['id'] ?? null,
+ 'env' => $this->mode === 'live' ? 'prod' : 'sandbox',
+ 'amount' => $payload['amount'] ?? null,
+ 'currency' => $payload['currency'] ?? null,
+ 'email' => $payload['customer']['email'] ?? null,
+ ];
+ }
+
+ public function getBuiltInFormAttributes(array $payload): array
+ {
+ $revolutOrder = $this->createRevolutOrder($payload);
+
+ $this->payment?->update([
+ 'status' => $this->getStatusEnum()::PENDING,
+ 'response' => $revolutOrder,
+ ]);
+
+ return [
+ 'token' => $revolutOrder['token'] ?? null, // REQUIRED by RevolutCheckout()
+ 'paymentId' => $this->payment->id ?? null,
+ 'orderId' => $this->payment->order_id ?? null,
+ 'revolutOrderId' => $revolutOrder['id'] ?? null,
+ 'env' => $this->mode === 'live' ? 'prod' : 'sandbox',
+ 'amount' => $payload['amount'] ?? null,
+ 'currency' => $payload['currency'] ?? null,
+ 'email' => $payload['customer']['email'] ?? null,
+ ];
+ }
+ /**
+ * Optional: keep pay() compatible — just return directive for widget flow.
+ * Your controller/UI should call createWidgetOrder() and then run the JS.
+ */
+ public function pay(array $params)
+ {
+ $data = $this->createWidgetOrder($params);
+
+ if (!empty($data['token'])) {
+ return response()->json([
+ 'type' => 'widget',
+ 'order_id' => $data['order_id'],
+ 'token' => $data['token'],
+ 'env' => $data['env'],
+ 'message' => 'Use Card Field or Pop-up with RevolutCheckout(token, env).',
+ ]);
+ // return [
+ // 'type' => 'widget',
+ // 'order_id' => $data['order_id'],
+ // 'token' => $data['token'],
+ // 'env' => $data['env'],
+ // 'message' => 'Use Card Field or Pop-up with RevolutCheckout(token, env).',
+ // ];
+ }
+
+ return ['type' => 'error', 'message' => $data['error'] ?? 'Unable to initialize Revolut order'];
+ }
+
+ /** Return URL — UX only; don’t finalize here */
+ public function handleResponse(HttpRequest $request)
+ {
+ $q = $request->query();
+
+ $this->payment?->update([
+ 'status' => $this->getStatusEnum()::PENDING,
+ 'response' => $q,
+ ]);
+
+ $orderId = $q['order_id'] ?? null;
+ $order = $orderId ? $this->retrieveOrder($orderId) : null;
+
+ $payload = [
+ 'id' => $q['payment_id'] ?? ($this->payment->id ?? null),
+ 'status' => self::RESPONSE_STATUS_SUCCESS, // means "return handled"
+ 'payment_service' => $q['payment_service'] ?? $this->service,
+ 'token' => $q['token'] ?? '',
+ 'order_id' => $order['id'] ?? $orderId,
+ 'order_data' => $order ?: $q,
+ ];
+
+ return $this->generatePostForm($payload, $this->payableReturnUrl);
+ }
+
+ /** Webhook — source of truth (COMPLETED/FAILED/CANCELLED/AUTHORISED) */
+ public function handleWebhook(HttpRequest $request)
+ {
+ $payload = $request->all();
+
+ $orderId = $payload['order']['id']
+ ?? $payload['order_id']
+ ?? $payload['merchant_order_ext_ref']
+ ?? null;
+
+ $rawState = $payload['order']['state']
+ ?? $payload['state']
+ ?? $payload['status']
+ ?? null;
+
+ if (!$orderId) {
+ return response()->json(['status' => 'ignored', 'reason' => 'missing order id'], 202);
+ }
+
+ // Optional: re-fetch to confirm latest state
+ $order = $this->retrieveOrder($orderId);
+ $finalState = strtoupper($order['state'] ?? ($rawState ? strtoupper($rawState) : 'FAILED'));
+
+ $payment = $this->payment ?: $this->findPaymentByOrderId($orderId);
+ if ($payment) {
+ $payment->update([
+ 'status' => $this->mapRevolutStatus($finalState),
+ 'response' => $payload,
+ ]);
+ }
+
+ return response()->json(['ok' => true]);
+ }
+
+ /** POST /orders/{id}/cancel (void before capture) */
+ public function cancel(array|object $params)
+ {
+
+ $params = (array) $params;
+
+ $orderId = $params['id'] ?? $this->payment->response->id ?? null;
+
+ if (!$orderId) return ['status' => 'error', 'message' => 'Order ID is required'];
+
+ $resp = $this->postReq($this->url, '/orders/'.urlencode($orderId).'/cancel', (object)[], $this->headers, 'json', $this->mode);
+
+ $statusCode = 200;
+ $message = 'Cancel successful';
+ if(is_array($resp) && isset($resp['status']) && $resp['status'] == 'error') {
+ $message = json_decode($resp['message'], true)['message'];
+ $statusCode = $resp['code'] ?? 500;
+ $data = $resp;
+ } else {
+ $data = is_string($resp) ? (array) json_decode($resp, true) : (array) $resp;
+ }
+
+ $status = self::RESPONSE_STATUS_ERROR;
+
+ if (!isset($data['status']) || $data['status'] !== 'error') {
+ $this->payment?->update([
+ 'status' => $this->getStatusEnum()::CANCELLED,
+ 'response' => $data,
+ ]);
+ $status = self::RESPONSE_STATUS_SUCCESS;
+ }
+
+ return [
+ 'type' => 'cancel',
+ 'status' => $status,
+ 'code' => $statusCode,
+ 'message' => $message,
+ 'id' => $this->payment->id ?? ($params['payment_id'] ?? null),
+ 'payment_service' => $this->service,
+ 'order_data' => json_encode($data),
+ ];
+ }
+
+ /** POST /orders/{id}/refund (full/partial; amount in minor units) */
+ public function refund(array|object $params)
+ {
+ $refundRequest = $this->validateRefundRequest($params);
+
+ if (!$refundRequest['validated']) return $refundRequest;
+
+ $params = (array) $params;
+ $payment = $refundRequest['payment'] ?? null;
+ $orderId = $params['id'] ?? ($payment->response->id ?? null);
+ $currency = $params['currency'] ?? ($payment->response->currency ?? null);
+
+ if (!$orderId) {
+ return array_merge($refundRequest, ['message' => 'order_id is required for refund']);
+ }
+
+ if (!$currency) {
+ return array_merge($refundRequest, ['message' => 'currency is required for refund']);
+ }
+
+ $payload = [];
+ if (!empty($params['amount'])) $payload['amount'] = (int) $params['amount']; // minor
+ if (!empty($currency)) $payload['currency'] = $currency;
+
+ $resp = $this->postReq($this->url, '/orders/'.urlencode($orderId).'/refund', $payload, $this->headers, 'json', $this->mode);
+
+ $data = is_string($resp) ? (array) json_decode($resp, true) : (array) $resp;
+
+ $status = self::RESPONSE_STATUS_SUCCESS;
+ $message = 'Refunded successfully';
+ $revolutMessageObject = isset($data['message']) ? json_decode($data['message'], true) : [];
+
+ $paymentPayload = [];
+
+
+ if(isset($data['code'])) {
+ $status = self::RESPONSE_STATUS_ERROR;
+ } else {
+ $payment->update([
+ 'status' => $this->getStatusEnum()::REFUNDED,
+ 'response' => $data,
+ ]);
+ }
+
+ if(isset($revolutMessageObject['message'])) {
+ $message = $revolutMessageObject['message'];
+ }
+
+ return array_merge($refundRequest, [
+ 'status' => $status,
+ 'order_data' => $data,
+ 'message' => $message,
+ ]);
+ }
+
+ /** Map Revolut states to your enum */
+ protected function mapRevolutStatus(string $revolutStatus): string
+ {
+ $statusMap = [
+ 'PENDING' => $this->getStatusEnum()::PENDING,
+ 'AUTHORISED' => $this->getStatusEnum()::PENDING, // wait for capture
+ 'COMPLETED' => $this->getStatusEnum()::COMPLETED,
+ 'FAILED' => $this->getStatusEnum()::FAILED,
+ 'CANCELLED' => $this->getStatusEnum()::CANCELLED,
+ ];
+ return $statusMap[$revolutStatus] ?? $this->getStatusEnum()::FAILED;
+ }
+
+ /** Helper: find Payment by order id for this gateway */
+ protected function findPaymentByOrderId($orderId)
+ {
+ if (!$orderId) return null;
+ $paymentModel = config('payable.model');
+ if (!$paymentModel) return null;
+
+ return $paymentModel::where('order_id', $orderId)
+ ->where('payment_gateway', $this->service)
+ ->latest('id')
+ ->first();
+ }
+
+ /** Keep parent implementation */
+ public function generatePostForm($params, $actionUrl = null)
+ {
+ return parent::generatePostForm($params, $actionUrl);
+ }
+}
diff --git a/src/Services/URequest.php b/src/Services/URequest.php
index 8b4d040..73938b3 100755
--- a/src/Services/URequest.php
+++ b/src/Services/URequest.php
@@ -91,6 +91,7 @@ public function postReq($url, $endPoint, $postFields, $headers, $type, $mode = n
try {
if ($type == 'json') {
// dd($this->client);
+
if (count($headers) < 1) {
$headers = [
'Content-Type' => 'application/json',
@@ -106,6 +107,7 @@ public function postReq($url, $endPoint, $postFields, $headers, $type, $mode = n
// );
}
// dd($postFields, "{$url}{$endPoint}", $this->headers);
+
$res = $this->client->post("{$url}{$endPoint}", [
'headers' => $headers,
'json' => $postFields,
@@ -170,7 +172,32 @@ public function postReq($url, $endPoint, $postFields, $headers, $type, $mode = n
// dd($safeData);
return $safeData;
} catch (\Exception $e) {
- return response()->json(['error' => $e->getMessage()], 500);
+ if($e instanceof \GuzzleHttp\Exception\ClientException) {
+ if($e->getResponse()->getStatusCode() == 422) {
+ return [
+ 'status' => 'error',
+ 'code' => $e->getResponse()->getStatusCode(),
+ 'message' => $e->getResponse()->getBody()->getContents(),
+ ];
+ } else if($e->getResponse()->getStatusCode() == 400) {
+ return [
+ 'status' => 'error',
+ 'code' => $e->getResponse()->getStatusCode(),
+ 'message' => $e->getResponse()->getBody()->getContents(),
+ ];
+ } else if($e->getResponse()->getStatusCode() == 404) {
+ return [
+ 'status' => 'error',
+ 'code' => $e->getResponse()->getStatusCode(),
+ 'message' => $e->getResponse()->getBody()->getContents(),
+ ];
+ }
+ }
+ return [
+ 'status' => 'error',
+ 'code' => $e->getResponse()->getStatusCode(),
+ 'message' => $e->getMessage(),
+ ];
}
}