From fe181a08081d50402b9bc987422546654abae970 Mon Sep 17 00:00:00 2001
From: ramazanayyildiz <48105049+ramazanayyildiz@users.noreply.github.com>
Date: Mon, 25 Aug 2025 22:57:22 +0300
Subject: [PATCH 1/7] feat(revolut): add revolut payment gateway integration
- Add revolut config to payable.php
- Implement RevolutService with widget flow support
- Update PaymentTestController with revolut test method
---
config/payable.php | 14 +
.../Controllers/RamazanTestController.php | 101 ++++--
src/Services/RevolutService.php | 328 ++++++++++++++++++
3 files changed, 406 insertions(+), 37 deletions(-)
create mode 100644 src/Services/RevolutService.php
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/Http/Controllers/RamazanTestController.php b/src/Http/Controllers/RamazanTestController.php
index c44c14e..8798bdd 100644
--- a/src/Http/Controllers/RamazanTestController.php
+++ b/src/Http/Controllers/RamazanTestController.php
@@ -1,45 +1,44 @@
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/Services/RevolutService.php b/src/Services/RevolutService.php
new file mode 100644
index 0000000..091220a
--- /dev/null
+++ b/src/Services/RevolutService.php
@@ -0,0 +1,328 @@
+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 validateParams($params)
+ {
+ $required = ['order_id', 'currency'];
+ $missing = array_diff($required, array_keys($params));
+ if (!empty($missing)) {
+ return 'These keys are missing for Revolut: '.implode(', ', $missing);
+ }
+ return true;
+ }
+
+ /** 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
+ {
+ $amountMinor = $this->normalizeAmountMinor($params);
+
+ return [
+ 'amount' => $amountMinor,
+ 'currency' => $params['currency'] ?? 'EUR',
+ 'merchant_order_ext_ref' => $params['order_id'],
+ 'merchant_customer_ext_ref' => $params['user_id'] ?? null,
+ 'description' => $params['description'] ?? ('Payment for order '.$params['order_id']),
+ 'customer' => [
+ 'email' => $params['user_email'] ?? null,
+ ],
+ 'metadata' => [
+ 'order_id' => $params['order_id'],
+ 'user_email' => $params['user_email'] ?? null,
+ 'ip' => $params['user_ip'] ?? null,
+ ],
+ // Optional: 'capture_mode' => 'automatic' | 'manual'
+ ];
+ }
+
+ /** POST /orders */
+ protected function createOrder(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 retrieveOrder(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);
+ $order = $this->createOrder($payload);
+
+ // Persist PENDING with raw provider response
+ $this->payment?->update([
+ 'status' => $this->getStatusEnum()::PENDING,
+ 'response' => $order,
+ ]);
+
+ return [
+ 'token' => $order['token'] ?? null, // REQUIRED by RevolutCheckout()
+ 'order_id' => $order['id'] ?? null,
+ 'env' => $this->mode === 'production' ? 'production' : '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 [
+ '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 $params)
+ {
+ $orderId = $params['order_id'] ?? $this->payment->order_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);
+ $data = is_string($resp) ? (array) json_decode($resp, true) : (array) $resp;
+
+ $status = self::RESPONSE_STATUS_ERROR;
+ if (!isset($data['error'])) {
+ $this->payment?->update([
+ 'status' => $this->getStatusEnum()::CANCELLED,
+ 'response' => $data,
+ ]);
+ $status = self::RESPONSE_STATUS_SUCCESS;
+ }
+
+ return [
+ 'type' => 'cancel',
+ 'status' => $status,
+ '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['order_id'] ?? ($payment->order_id ?? null);
+
+ if (!$orderId) {
+ return array_merge($refundRequest, ['message' => 'order_id is required for refund']);
+ }
+
+ $payload = [];
+ if (!empty($params['amount'])) $payload['amount'] = (int) $params['amount']; // minor
+
+ $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_ERROR;
+ $message = 'Refund failed';
+
+ if (!isset($data['error'])) {
+ $payment?->update([
+ 'status' => $this->getStatusEnum()::REFUNDED,
+ 'response' => $data,
+ ]);
+ $status = self::RESPONSE_STATUS_SUCCESS;
+ $message = 'Refunded successfully';
+ }
+
+ return array_merge($refundRequest, [
+ 'status' => $status,
+ 'order_data' => json_encode($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);
+ }
+}
\ No newline at end of file
From 1040a0dea19d56f80f2c254a66385ba7f5e1ef2c Mon Sep 17 00:00:00 2001
From: ramazanayyildiz <48105049+ramazanayyildiz@users.noreply.github.com>
Date: Mon, 25 Aug 2025 23:09:22 +0300
Subject: [PATCH 2/7] docs: add revolut payment integration guide
Add comprehensive documentation for implementing Revolut payments including backend controller, Vue component, Blade template, and JavaScript setup. The guide covers installation, configuration, and usage examples for both frontend and backend implementations.
---
REVOLUT_IMPLEMENTATION.md | 338 ++++++++++++++++++++++++++++++++++++++
1 file changed, 338 insertions(+)
create mode 100644 REVOLUT_IMPLEMENTATION.md
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
+
+
+
+
+
+
+ Complete Your Payment
+