A Laravel package for integrating with Netopia Payments (Romania) payment gateway.
- PHP 7.4 or higher
- Laravel 8.0 or higher
- OpenSSL extension
- DOM extension
You can install the package via composer:
composer require aflorea4/laravel-netopia-payments
Publish the configuration file:
php artisan vendor:publish --provider="Aflorea4\NetopiaPayments\NetopiaPaymentsServiceProvider" --tag="config"
This will create a config/netopia.php
file in your app that you can modify to set your configuration.
// config/netopia.php
return [
// The Netopia merchant signature (merchant identifier)
'signature' => env('NETOPIA_SIGNATURE', ''),
// The path to the public key file
'public_key_path' => env('NETOPIA_PUBLIC_KEY_PATH', storage_path('app/keys/netopia/public.cer')),
// The path to the private key file
'private_key_path' => env('NETOPIA_PRIVATE_KEY_PATH', storage_path('app/keys/netopia/private.key')),
// Whether to use the live environment or the sandbox environment
'live_mode' => env('NETOPIA_LIVE_MODE', false),
// The currency to use for payments
'default_currency' => env('NETOPIA_DEFAULT_CURRENCY', 'RON'),
];
Add the following variables to your .env
file:
NETOPIA_SIGNATURE=your-netopia-signature
NETOPIA_PUBLIC_KEY_PATH=/path/to/your/public.cer
NETOPIA_PRIVATE_KEY_PATH=/path/to/your/private.key
NETOPIA_LIVE_MODE=false
NETOPIA_DEFAULT_CURRENCY=RON
Before using the package in production, it's important to verify that your certificate and private key are valid and working correctly. You can use the following commands to check them:
openssl x509 -in /path/to/your/public.cer -text -noout
This command should display information about your certificate, including the issuer, validity period, and public key details. If the command returns an error, your certificate may be invalid or corrupted.
openssl rsa -in /path/to/your/private.key -check
This command should display "RSA key ok" if your private key is valid. If it asks for a password, your key may be password-protected, which is not supported by this package.
To verify that your public certificate and private key are a matching pair (which is essential for encryption/decryption to work):
# Extract the modulus from the certificate
openssl x509 -in /path/to/your/public.cer -noout -modulus | md5sum
# Extract the modulus from the private key
openssl rsa -in /path/to/your/private.key -noout -modulus | md5sum
Both commands should produce the same MD5 hash. If they don't match, your certificate and private key are not a valid pair, which will cause encryption/decryption failures.
use Aflorea4\NetopiaPayments\Facades\NetopiaPayments;
// Create a payment request
$paymentData = NetopiaPayments::createPaymentRequest(
'ORDER123', // Order ID
100.00, // Amount
'RON', // Currency
route('payment.return'), // Return URL
route('netopia.confirm'), // Confirm URL
[
'type' => 'person', // 'person' or 'company'
'firstName' => 'John',
'lastName' => 'Doe',
'email' => '[email protected]',
'address' => '123 Main St',
'mobilePhone' => '0712345678',
],
'Payment for Order #123' // Description
);
// The payment data contains:
// - env_key: The encrypted envelope key (REQUIRED)
// - data: The encrypted payment data (REQUIRED)
// - cipher: The cipher algorithm used for encryption (REQUIRED, defaults to 'aes-256-cbc')
// - iv: The initialization vector for AES encryption (REQUIRED when using aes-256-cbc)
// - url: The payment URL (sandbox or live)
//
// IMPORTANT: All parameters (env_key, data, cipher, and iv) must be included in your payment form
// submission to Netopia. The cipher parameter should be set to 'aes-256-cbc' for modern implementations.
// Redirect to the payment page
return view('payment.redirect', [
'paymentData' => $paymentData,
]);
Create a view file resources/views/payment/redirect.blade.php
:
<!DOCTYPE html>
<html>
<head>
<title>Redirecting to payment...</title>
</head>
<body>
<h1>Redirecting to payment...</h1>
<p>Please wait while we redirect you to the payment page.</p>
<form id="netopiaForm" action="{{ $paymentData['url'] }}" method="post">
<input
type="hidden"
name="env_key"
value="{{ $paymentData['env_key'] }}"
/>
<input type="hidden" name="data" value="{{ $paymentData['data'] }}" />
<input type="hidden" name="cipher" value="{{ $paymentData['cipher'] ?? 'aes-256-cbc' }}" />
<input type="hidden" name="iv" value="{{ $paymentData['iv'] ?? '' }}" />
<button type="submit" style="display: none;">Pay Now</button>
</form>
<script>
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("netopiaForm").submit();
});
</script>
</body>
</html>
The package automatically registers routes for handling payment notifications:
POST /netopia/confirm
- For Instant Payment Notifications (IPN)GET /netopia/return
- For redirecting the user after payment
These routes are handled by the package's internal controller and will dispatch events that you can listen for in your application. You don't need to create these routes yourself.
You can listen for the following events to handle payment notifications:
use Aflorea4\NetopiaPayments\Events\NetopiaPaymentConfirmed;
use Aflorea4\NetopiaPayments\Events\NetopiaPaymentPending;
use Aflorea4\NetopiaPayments\Events\NetopiaPaymentCanceled;
// In your EventServiceProvider.php
protected $listen = [
NetopiaPaymentConfirmed::class => [
// Your listeners here
],
NetopiaPaymentPending::class => [
// Your listeners here
],
NetopiaPaymentCanceled::class => [
// Your listeners here
],
];
namespace App\Listeners;
use Aflorea4\NetopiaPayments\Events\NetopiaPaymentConfirmed;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Models\Order;
class HandleConfirmedPayment implements ShouldQueue
{
use InteractsWithQueue;
/**
* Handle the event.
*
* @param NetopiaPaymentConfirmed $event
* @return void
*/
public function handle(NetopiaPaymentConfirmed $event)
{
// Get the payment response
$response = $event->response;
// Find the order
$order = Order::where('order_id', $response->orderId)->first();
if ($order) {
// Update the order status
$order->status = 'paid';
$order->payment_amount = $response->processedAmount;
$order->save();
// Additional logic...
}
}
}
Here's a complete example of how to handle a test transaction of 1.00 RON without using queues or listeners:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Aflorea4\NetopiaPayments\Facades\NetopiaPayments;
use Illuminate\Support\Facades\Log;
class PaymentController extends Controller
{
/**
* Initiate a test payment
*/
public function initiatePayment()
{
// Create a payment request for 1.00 RON
$paymentData = NetopiaPayments::createPaymentRequest(
'TEST'.time(), // Order ID with timestamp to make it unique
1.00, // Amount (1.00 RON for testing)
'RON', // Currency
route('payment.return'), // Return URL
route('payment.confirm'), // Confirm URL
[
'type' => 'person',
'firstName' => 'Test',
'lastName' => 'User',
'email' => '[email protected]',
'address' => 'Test Address',
'mobilePhone' => '0700000000',
],
'Test payment of 1.00 RON' // Description
);
// Redirect to the payment page
return view('payment.redirect', [
'paymentData' => $paymentData,
]);
}
/**
* Handle the payment confirmation (IPN)
*/
public function confirmPayment(Request $request)
{
try {
// Process the payment response
$response = NetopiaPayments::processResponse(
$request->input('env_key'),
$request->input('data'),
null,
$request->input('iv')
);
// Log the payment response
Log::info('Payment confirmation received', [
'orderId' => $response->orderId,
'action' => $response->action,
'errorCode' => $response->errorCode ?? null,
'errorMessage' => $response->errorMessage ?? null,
'processedAmount' => $response->processedAmount ?? null,
]);
// Handle the payment based on the action
if ($response->isConfirmed()) {
// Payment was confirmed/approved
// Update your order status in the database
// For example:
// Order::where('order_id', $response->orderId)->update(['status' => 'paid']);
// Your business logic here...
} elseif ($response->isPending()) {
// Payment is pending
// Update your order status in the database
// For example:
// Order::where('order_id', $response->orderId)->update(['status' => 'pending']);
} elseif ($response->isCanceled()) {
// Payment was canceled or failed
// Update your order status in the database
// For example:
// Order::where('order_id', $response->orderId)->update(['status' => 'canceled']);
}
// Return the appropriate response to Netopia
return response(
NetopiaPayments::generatePaymentResponse(),
200,
['Content-Type' => 'application/xml']
);
} catch (\Exception $e) {
// Log the error
Log::error('Payment confirmation error', [
'message' => $e->getMessage(),
]);
// Return an error response to Netopia
return response(
NetopiaPayments::generatePaymentResponse(1, 1, $e->getMessage()),
200,
['Content-Type' => 'application/xml']
);
}
}
/**
* Handle the return from payment page
*/
public function returnFromPayment(Request $request)
{
// This is where the user is redirected after the payment
// You can show a thank you page or order summary
// Check if all required payment data is in the request
if ($request->has('env_key') && $request->has('data') && $request->has('iv')) {
try {
// Process the payment response
$response = NetopiaPayments::processResponse(
$request->input('env_key'),
$request->input('data')
);
// Show different messages based on the payment status
if ($response->isConfirmed()) {
return view('payment.success', ['orderId' => $response->orderId]);
} elseif ($response->isPending()) {
return view('payment.pending', ['orderId' => $response->orderId]);
} else {
return view('payment.failed', ['orderId' => $response->orderId]);
}
} catch (\Exception $e) {
// Log the error
Log::error('Payment return error', [
'message' => $e->getMessage(),
]);
return view('payment.error', ['message' => $e->getMessage()]);
}
}
// Fallback if no payment data is present
return view('payment.complete');
}
}
// routes/web.php
Route::get('/payment/initiate', [PaymentController::class, 'initiatePayment'])->name('payment.initiate');
Note about routes: The example below registers custom routes for payment confirmation and return:
// Custom routes if you want to handle payment processing in your own controller
Route::post('/payment/confirm', [PaymentController::class, 'confirmPayment'])->name('payment.confirm');
Route::get('/payment/return', [PaymentController::class, 'returnFromPayment'])->name('payment.return');
These custom routes are different from the auto-registered package routes (/netopia/confirm
and /netopia/return
). Use custom routes when:
- You want complete control over the payment flow
- You need custom logic that isn't covered by the event listeners
- You're not using the package's event system
If you're using the package's event system, you can use the auto-registered routes instead.
Create the redirect view as shown in the main usage example, and create success, pending, failed, and error views as needed.
When testing in the sandbox environment, you can use the following test card details to simulate a successful transaction:
Card Number: 4111 1111 1111 1111
Expiry Date: Any future date (e.g., 12/25)
CVV: Any 3 digits (e.g., 123)
Cardholder Name: Any name
3D Secure Password: 123456
- Make sure your
.env
file hasNETOPIA_LIVE_MODE=false
to use the sandbox environment - Visit
/payment/initiate
to start a test payment of 1.00 RON - You'll be redirected to the Netopia sandbox payment page
- Enter the test card details provided above
- Complete the 3D Secure verification with password
123456
- The payment will be processed, and you'll be redirected back to your return URL
- The confirmation endpoint will also receive the payment notification
This approach doesn't use queues or event listeners, making it simpler for testing and development. All payment processing happens synchronously in the controller methods.
The package uses the following security measures:
- Request authentication using an API Signature included in the request
- Data encryption using RSA keys with AES-256-CBC for symmetric encryption
- Secure Sockets Layer (SSL) data transport
As of version 0.2.6, this package exclusively uses AES-256-CBC encryption for all payment data. This provides stronger security compared to older cipher methods like RC4. When processing payments, the initialization vector (IV) parameter is now required for all decryption operations.
This package uses PEST for testing. To run the tests, you can use the following command:
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.
The test suite includes:
- Unit tests for the NetopiaPayments class
- Feature tests for payment request generation
- Feature tests for payment response processing
- Integration tests for the controller
If you want to add more tests, you can create them in the tests
directory. The package uses PEST's expressive syntax for writing tests. Here's an example of how to write a test for the NetopiaPayments class:
it('can create a payment request', function () {
$netopiaPayments = new \Aflorea4\NetopiaPayments\NetopiaPayments();
$paymentData = $netopiaPayments->createPaymentRequest(
'ORDER123',
100.00,
'RON',
'https://example.com/return',
'https://example.com/confirm',
[
'type' => 'person',
'firstName' => 'John',
'lastName' => 'Doe',
'email' => '[email protected]',
'address' => '123 Main St',
'mobilePhone' => '0712345678',
],
'Payment for Order #123'
);
expect($paymentData)->toBeArray()
->toHaveKeys(['env_key', 'data', 'url']);
});
The MIT License (MIT). Please see License File for more information.