Added support for Nicky.me payment gateway

This commit is contained in:
Pinga 2025-01-20 14:51:59 +02:00
parent efdf2e2418
commit aabe29fb5c
7 changed files with 388 additions and 1 deletions

View file

@ -334,6 +334,82 @@ class FinancialsController extends Controller
} }
} }
public function createNickyPayment(Request $request, Response $response)
{
$postData = $request->getParsedBody();
$amount = $postData['amount']; // Make sure to validate and sanitize this amount
// Registrar ID and unique identifier
$registrarId = $_SESSION['auth_registrar_id'];
// Generate a 10-character alphanumeric random string for the invoice reference
$invoiceReference = strtoupper(bin2hex(random_bytes(5))); // 10 characters, all caps
// Map currency to Nicky's blockchainAssetId
$blockchainAssetId = match ($_SESSION['_currency']) {
'USD' => 'USD.USD',
'EUR' => 'EUR.EUR',
default => throw new Exception('Unsupported currency: ' . $_SESSION['_currency']),
};
// Prepare the payload for the API
$data = [
'blockchainAssetId' => $blockchainAssetId,
'amountExpectedNative' => $amount,
'billDetails' => [
'invoiceReference' => $invoiceReference,
'description' => 'Deposit for registrar ' . $registrarId,
],
'requester' => [
'email' => $_SESSION['auth_email'],
'name' => $_SESSION['auth_username'],
],
'sendNotification' => true,
'successUrl' => envi('APP_URL') . '/payment-success-nicky',
'cancelUrl' => envi('APP_URL') . '/payment-cancel',
];
$url = 'https://api-public.pay.nicky.me/api/public/PaymentRequestPublicApi/create';
$apiKey = envi('NICKY_API_KEY');
$client = new Client();
try {
$apiResponse = $client->request('POST', $url, [
'headers' => [
'x-api-key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => $data,
]);
$body = json_decode($apiResponse->getBody()->getContents(), true);
if (isset($body['bill']['shortId'])) {
$paymentUrl = "https://pay.nicky.me/home?paymentId=" . $body['bill']['shortId'];
// Store the shortId in the session or database for future reference
$_SESSION['nicky_shortId'] = $body['bill']['shortId'];
// Return the payment URL as JSON
$response->getBody()->write(json_encode(['invoice_url' => $paymentUrl]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
} else {
throw new Exception('API response does not contain a payment URL.');
}
} catch (GuzzleException $e) {
unset($_SESSION['nicky_shortId']);
$errorResponse = [
'error' => 'We encountered an issue while processing your payment.',
'details' => $e->getMessage(),
];
$response->getBody()->write(json_encode($errorResponse));
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
}
}
public function successStripe(Request $request, Response $response) public function successStripe(Request $request, Response $response)
{ {
$session_id = $request->getQueryParams()['session_id'] ?? null; $session_id = $request->getQueryParams()['session_id'] ?? null;
@ -564,6 +640,117 @@ class FinancialsController extends Controller
return view($response,'admin/financials/success-crypto.twig'); return view($response,'admin/financials/success-crypto.twig');
} }
public function successNicky(Request $request, Response $response)
{
$client = new Client();
$sessionShortId = $_SESSION['nicky_shortId'] ?? null;
if (!$sessionShortId) {
$this->container->get('flash')->addMessage('info', 'No payment reference found in session.');
return view($response, 'admin/financials/success-nicky.twig');
}
$url = 'https://api-public.pay.nicky.me/api/public/PaymentRequestPublicApi/get-by-short-id?shortId=' . urlencode($sessionShortId);
$apiKey = envi('NICKY_API_KEY');
try {
$apiResponse = $client->request('GET', $url, [
'headers' => [
'x-api-key' => $apiKey,
'Content-Type' => 'application/json',
],
]);
$statusCode = $apiResponse->getStatusCode();
$responseBody = json_decode($apiResponse->getBody()->getContents(), true);
if ($statusCode === 200 && isset($responseBody['status'])) {
$status = $responseBody['status'];
$amount = $responseBody['amountNative'] ?? 0;
$paymentId = $responseBody['id'] ?? null;
$description = $responseBody['bill']['description'] ?? 'No description';
if ($status === "None" || $status === "PaymentValidationRequired" || $status === "PaymentPending") {
return view($response, 'admin/financials/success-nicky.twig', [
'status' => $status,
'paymentId' => $paymentId
]);
} elseif ($status === "Finished") {
// Record the successful transaction in the database
$db = $this->container->get('db');
$registrarId = $_SESSION['auth_registrar_id'];
$currentDateTime = new \DateTime();
$date = $currentDateTime->format('Y-m-d H:i:s.v');
$db->beginTransaction();
try {
$db->insert(
'statement',
[
'registrar_id' => $registrarId,
'date' => $date,
'command' => 'create',
'domain_name' => 'deposit',
'length_in_months' => 0,
'fromS' => $date,
'toS' => $date,
'amount' => $amount,
]
);
$db->insert(
'payment_history',
[
'registrar_id' => $registrarId,
'date' => $date,
'description' => 'Registrar balance deposit via Nicky ('.$paymentId.')',
'amount' => $amount,
]
);
$db->exec(
'UPDATE registrar SET accountBalance = (accountBalance + ?) WHERE id = ?',
[
$amount,
$registrarId,
]
);
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
$this->container->get('flash')->addMessage('error', 'Transaction recording failed: ' . $e->getMessage());
return $response->withHeader('Location', '/payment-success-nicky')->withStatus(302);
}
unset($_SESSION['nicky_shortId']);
// Redirect to success page with details
return view($response, 'admin/financials/success-nicky.twig', [
'status' => $status,
'paymentId' => $paymentId,
]);
} else {
unset($_SESSION['nicky_shortId']);
// Handle unexpected statuses
return view($response, 'admin/financials/success-nicky.twig', [
'status' => $status,
'paymentId' => $paymentId,
]);
}
} else {
unset($_SESSION['nicky_shortId']);
$this->container->get('flash')->addMessage('error', 'Failed to retrieve payment information.');
return $response->withHeader('Location', '/payment-success-nicky')->withStatus(302);
}
} catch (GuzzleException $e) {
$this->container->get('flash')->addMessage('error', 'Request failed: ' . $e->getMessage());
return $response->withHeader('Location', '/payment-success-nicky')->withStatus(302);
}
}
public function webhookAdyen(Request $request, Response $response) public function webhookAdyen(Request $request, Response $response)
{ {
$data = json_decode($request->getBody()->getContents(), true); $data = json_decode($request->getBody()->getContents(), true);

View file

@ -41,4 +41,6 @@ ADYEN_HMAC_KEY='adyen-hmac-key'
NOW_API_KEY='now-api-key' NOW_API_KEY='now-api-key'
NICKY_API_KEY='nicky-api-key'
TEST_TLDS=.test,.com.test TEST_TLDS=.test,.com.test

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -4,6 +4,46 @@
{% block content %} {% block content %}
<link href="/assets/css/sweetalert2.min.css" rel="stylesheet"> <link href="/assets/css/sweetalert2.min.css" rel="stylesheet">
<style>
.payment-provider-custom {
display: inline-block;
background-image: url('/assets/nicky.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* Fullscreen overlay */
#loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8); /* Whiteout effect */
z-index: 9999; /* Ensure it appears above all content */
display: flex;
align-items: center;
justify-content: center;
}
/* Spinner container for alignment */
.spinner-container {
text-align: center;
}
/* Spinner size adjustment */
.spinner-border {
width: 3rem;
height: 3rem;
}
</style>
<div id="loading-overlay" style="display: none;">
<div class="spinner-container">
<div class="spinner-border text-primary" role="status"></div>
<p class="text-secondary mt-3">Processing your payment, please wait...</p>
</div>
</div>
<div class="page-wrapper"> <div class="page-wrapper">
<!-- Page header --> <!-- Page header -->
<div class="page-header d-print-none"> <div class="page-header d-print-none">
@ -67,6 +107,18 @@
</div> </div>
</div> </div>
</label> </label>
<label class="form-selectgroup-item flex-fill">
<input type="radio" name="paymentMethod" value="nicky" class="form-selectgroup-input">
<div class="form-selectgroup-label d-flex align-items-center p-3">
<div class="me-3">
<span class="form-selectgroup-check"></span>
</div>
<div>
<span class="payment payment-provider-custom payment-xs me-2"></span>
{{ __('Crypto') }}: Nicky
</div>
</div>
</label>
<label class="form-selectgroup-item flex-fill"> <label class="form-selectgroup-item flex-fill">
<input type="radio" name="paymentMethod" value="crypto" class="form-selectgroup-input"> <input type="radio" name="paymentMethod" value="crypto" class="form-selectgroup-input">
<div class="form-selectgroup-label d-flex align-items-center p-3"> <div class="form-selectgroup-label d-flex align-items-center p-3">
@ -75,7 +127,7 @@
</div> </div>
<div> <div>
<span class="payment payment-provider-bitcoin-dark payment-xs me-2"></span> <span class="payment payment-provider-bitcoin-dark payment-xs me-2"></span>
{{ __('Crypto') }} {{ __('Crypto') }}: NOW
</div> </div>
</div> </div>
</label> </label>
@ -123,6 +175,8 @@ document.getElementById('depositForm').addEventListener('submit', function (e) {
handleAdyenPayment(formData); handleAdyenPayment(formData);
} else if (paymentMethod === 'crypto') { } else if (paymentMethod === 'crypto') {
handleCryptoPayment(formData); handleCryptoPayment(formData);
} else if (paymentMethod === 'nicky') {
handleNickyPayment(formData);
} else { } else {
Swal.fire({ Swal.fire({
icon: 'error', icon: 'error',
@ -223,5 +277,55 @@ function handleCryptoPayment(formData) {
}); });
}); });
} }
// Function to handle Nicky payment
function handleNickyPayment(formData) {
// Show the loading overlay
const overlay = document.getElementById('loading-overlay');
overlay.style.display = 'flex';
fetch('/create-nicky-payment', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
// Hide the loading overlay
overlay.style.display = 'none';
if (data.error) {
console.error('Nicky Error:', data.error);
Swal.fire({
icon: 'error',
title: 'Payment Error',
text: data.error
});
} else if (data.invoice_url) {
console.log('Nicky payment success:', data);
window.location.href = data.invoice_url;
} else {
console.error('Unexpected API response:', data);
Swal.fire({
icon: 'error',
title: 'Unexpected Response',
text: 'The payment gateway did not return a valid payment URL.'
});
}
})
.catch(error => {
// Hide the loading overlay
overlay.style.display = 'none';
console.error('Error in Nicky payment:', error);
Swal.fire({
icon: 'error',
title: 'Unexpected Error',
text: 'An unexpected error occurred. Please try again later. Ensure the payment gateway is properly configured.'
});
});
}
</script> </script>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,64 @@
{% extends "layouts/app.twig" %}
{% block title %}{{ __('Nicky Payment Status') }}{% endblock %}
{% block content %}
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<!-- Page pre-title -->
<div class="page-pretitle">
{{ __('Overview') }}
</div>
<h2 class="page-title">
{{ __('Nicky Payment Status') }}
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="col-12">
{% include 'partials/flash.twig' %}
{% if status is defined and paymentId is defined %}
<div class="card">
<div class="card-body">
<h4>{{ __('Payment Status:') }} {{ status }}</h4>
{% if status == 'None' or status == 'PaymentValidationRequired' or status == 'PaymentPending' %}
<p class="text-primary">
Your payment is currently <strong>in progress</strong>. Please <a href="#" onclick="window.location.reload(); return false;"><u>check back</u></a> later for updates.
</p>
{% elseif status == 'Canceled' %}
<p class="text-danger">
Your payment has been <strong>cancelled</strong>. Please initiate a new payment if you wish to proceed.
</p>
{% else %}
<p class="text-success">
Your payment has been <strong>successfully processed</strong> and completed.
</p>
{% endif %}
</div>
</div>
{% else %}
<div class="card">
<div class="card-body">
<h4>{{ __('Payment Details Not Found') }}</h4>
<p class="text-secondary">{{ __('It appears the payment details could not be found in your session. This may happen if the session has expired or if you accessed this page directly without completing the payment process.') }}</p>
<p class="text-secondary">{{ __('To proceed, please restart the payment process. If you have already completed a payment and believe this is an error, check your email for the payment confirmation details or contact our support team for assistance.') }}</p>
<p class="text-secondary">{{ __('We recommend initiating a new payment to ensure your transaction is properly credited.') }}</p>
<p class="text-secondary"><a href="/deposit" class="btn btn-primary">{{ __('Restart Payment') }}</a></p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% include 'partials/footer.twig' %}
</div>
{% endblock %}

View file

@ -117,9 +117,11 @@ $app->group('', function ($route) {
$route->map(['GET', 'POST'], '/create-payment', FinancialsController::class .':createStripePayment')->setName('createStripePayment'); $route->map(['GET', 'POST'], '/create-payment', FinancialsController::class .':createStripePayment')->setName('createStripePayment');
$route->map(['GET', 'POST'], '/create-adyen-payment', FinancialsController::class .':createAdyenPayment')->setName('createAdyenPayment'); $route->map(['GET', 'POST'], '/create-adyen-payment', FinancialsController::class .':createAdyenPayment')->setName('createAdyenPayment');
$route->map(['GET', 'POST'], '/create-crypto-payment', FinancialsController::class .':createCryptoPayment')->setName('createCryptoPayment'); $route->map(['GET', 'POST'], '/create-crypto-payment', FinancialsController::class .':createCryptoPayment')->setName('createCryptoPayment');
$route->map(['GET', 'POST'], '/create-nicky-payment', FinancialsController::class .':createNickyPayment')->setName('createNickyPayment');
$route->map(['GET', 'POST'], '/payment-success', FinancialsController::class .':successStripe')->setName('successStripe'); $route->map(['GET', 'POST'], '/payment-success', FinancialsController::class .':successStripe')->setName('successStripe');
$route->map(['GET', 'POST'], '/payment-success-adyen', FinancialsController::class .':successAdyen')->setName('successAdyen'); $route->map(['GET', 'POST'], '/payment-success-adyen', FinancialsController::class .':successAdyen')->setName('successAdyen');
$route->map(['GET', 'POST'], '/payment-success-crypto', FinancialsController::class .':successCrypto')->setName('successCrypto'); $route->map(['GET', 'POST'], '/payment-success-crypto', FinancialsController::class .':successCrypto')->setName('successCrypto');
$route->map(['GET', 'POST'], '/payment-success-nicky', FinancialsController::class .':successNicky')->setName('successNicky');
$route->map(['GET', 'POST'], '/payment-cancel', FinancialsController::class .':cancel')->setName('cancel'); $route->map(['GET', 'POST'], '/payment-cancel', FinancialsController::class .':cancel')->setName('cancel');
$route->get('/transactions', FinancialsController::class .':transactions')->setName('transactions'); $route->get('/transactions', FinancialsController::class .':transactions')->setName('transactions');
$route->get('/overview', FinancialsController::class .':overview')->setName('overview'); $route->get('/overview', FinancialsController::class .':overview')->setName('overview');

View file

@ -1086,6 +1086,8 @@ ADYEN_HMAC_KEY='adyen-hmac-key'
NOW_API_KEY='now-api-key' NOW_API_KEY='now-api-key'
NICKY_API_KEY='nicky-api-key'
TEST_TLDS=.test,.com.test TEST_TLDS=.test,.com.test
``` ```