1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-14 07:02:34 +01:00

Merge pull request #6794 from beganovich/v5-razorpay

Razorpay: All in one checkout
This commit is contained in:
David Bomba 2021-10-09 08:21:35 +11:00 committed by GitHub
commit 40a7562b52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 570 additions and 3 deletions

View File

@ -137,6 +137,11 @@ class Gateway extends StaticModel
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true] // GoCardless
];
break;
case 58:
return [
GatewayType::HOSTED_PAGE => ['refund' => false, 'token_billing' => false] // Razorpay
];
break;
default:
return [];
break;

View File

@ -28,6 +28,7 @@ class GatewayType extends StaticModel
const KBC = 11;
const BANCONTACT = 12;
const IDEAL = 13;
const HOSTED_PAGE = 14; // For gateways that contain multiple methods.
public function gateway()
{
@ -66,6 +67,8 @@ class GatewayType extends StaticModel
return ctrans('texts.bancontact');
case self::IDEAL:
return ctrans('texts.ideal');
case self::HOSTED_PAGE:
return ctrans('texts.aio_checkout');
default:
return 'Undefined.';
break;

View File

@ -46,6 +46,7 @@ class PaymentType extends StaticModel
const KBC = 35;
const BANCONTACT = 36;
const IDEAL = 37;
const HOSTED_PAGE = 38;
public static function parseCardType($cardName)
{

View File

@ -74,6 +74,7 @@ class SystemLog extends Model
const TYPE_EWAY = 313;
const TYPE_SQUARE = 320;
const TYPE_GOCARDLESS = 321;
const TYPE_RAZORPAY = 322;
const TYPE_QUOTA_EXCEEDED = 400;
const TYPE_UPSTREAM_FAILURE = 401;

View File

@ -0,0 +1,184 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Razorpay;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\RazorpayPaymentDriver;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Razorpay\Api\Errors\SignatureVerificationError;
class Hosted implements MethodInterface
{
protected RazorpayPaymentDriver $razorpay;
public function __construct(RazorpayPaymentDriver $razorpay)
{
$this->razorpay = $razorpay;
$this->razorpay->init();
}
/**
* Show the authorization page for Razorpay.
*
* @param array $data
* @return View
*/
public function authorizeView(array $data): View
{
return render('gateways.razorpay.hosted.authorize', $data);
}
/**
* Handle the authorization page for Razorpay.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Payment view for the Razorpay.
*
* @param array $data
* @return View
*/
public function paymentView(array $data): View
{
$order = $this->razorpay->gateway->order->create([
'currency' => $this->razorpay->client->currency()->code,
'amount' => $this->razorpay->convertToRazorpayAmount((float) $this->razorpay->payment_hash->data->amount_with_fee),
]);
$this->razorpay->payment_hash->withData('order_id', $order->id);
$this->razorpay->payment_hash->withData('order_amount', $order->amount);
$data['gateway'] = $this->razorpay;
$data['options'] = [
'key' => $this->razorpay->company_gateway->getConfigField('apiKey'),
'amount' => $this->razorpay->convertToRazorpayAmount((float) $this->razorpay->payment_hash->data->amount_with_fee),
'currency' => $this->razorpay->client->currency()->code,
'name' => $this->razorpay->company_gateway->company->present()->name(),
'order_id' => $order->id,
];
return render('gateways.razorpay.hosted.pay', $data);
}
/**
* Handle payments page for Razorpay.
*
* @param PaymentResponseRequest $request
* @return void
*/
public function paymentResponse(PaymentResponseRequest $request)
{
$request->validate([
'payment_hash' => ['required'],
'razorpay_payment_id' => ['required'],
'razorpay_signature' => ['required'],
]);
if (! property_exists($this->razorpay->payment_hash->data, 'order_id')) {
throw new PaymentFailed('Missing [order_id] property. Please contact the administrator. Reference: ' . $this->razorpay->payment_hash->hash);
}
try {
$attributes = [
'razorpay_order_id' => $this->razorpay->payment_hash->data->order_id,
'razorpay_payment_id' => $request->razorpay_payment_id,
'razorpay_signature' => $request->razorpay_signature,
];
$this->razorpay->gateway->utility->verifyPaymentSignature($attributes);
return $this->processSuccessfulPayment($request->razorpay_payment_id);
}
catch (SignatureVerificationError $exception) {
return $this->processUnsuccessfulPayment($exception);
}
}
/**
* Handle the successful payment for Razorpay.
*
* @param string $payment_id
* @return RedirectResponse
*/
public function processSuccessfulPayment(string $payment_id): RedirectResponse
{
$data = [
'gateway_type_id' => GatewayType::HOSTED_PAGE,
'amount' => array_sum(array_column($this->razorpay->payment_hash->invoices(), 'amount')) + $this->razorpay->payment_hash->fee_total,
'payment_type' => PaymentType::HOSTED_PAGE,
'transaction_reference' => $payment_id,
];
$payment_record = $this->razorpay->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $payment_id, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_RAZORPAY,
$this->razorpay->client,
$this->razorpay->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->razorpay->encodePrimaryKey($payment_record->id)]);
}
/**
* Handle unsuccessful payment for Razorpay.
*
* @param Exception $exception
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulPayment(\Exception $exception): void
{
PaymentFailureMailer::dispatch(
$this->razorpay->client,
$exception->getMessage(),
$this->razorpay->client->company,
$this->razorpay->payment_hash->data->amount_with_fee
);
SystemLogger::dispatch(
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_RAZORPAY,
$this->razorpay->client,
$this->razorpay->client->company,
);
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\PaymentDrivers\Razorpay\Hosted;
use App\Utils\Traits\MakesHash;
class RazorpayPaymentDriver extends BaseDriver
{
use MakesHash;
public $refundable = false;
public $token_billing = false;
public $can_authorise_credit_card = false;
public \Razorpay\Api\Api $gateway;
public $payment_method;
public static $methods = [
GatewayType::HOSTED_PAGE => Hosted::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_RAZORPAY;
public function init(): self
{
$this->gateway = new \Razorpay\Api\Api(
$this->company_gateway->getConfigField('apiKey'),
$this->company_gateway->getConfigField('apiSecret'),
);
return $this;
}
public function gatewayTypes(): array
{
return [
GatewayType::HOSTED_PAGE,
];
}
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
public function authorizeView(array $data)
{
return $this->payment_method->authorizeView($data);
}
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request);
}
public function processPaymentView(array $data)
{
return $this->payment_method->paymentView($data);
}
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request);
}
public function refund(Payment $payment, $amount, $return_client_response = false) {}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) {}
/**
* Convert the amount to the format that Razorpay supports.
*
* @param mixed|float $amount
* @return int
*/
public function convertToRazorpayAmount($amount): int
{
return \number_format((float) $amount * 100, 0, '.', '');
}
}

View File

@ -72,6 +72,7 @@
"payfast/payfast-php-sdk": "^1.1",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1",
"razorpay/razorpay": "2.*",
"sentry/sentry-laravel": "^2",
"square/square": "13.0.0.20210721",
"stripe/stripe-php": "^7.50",

126
composer.lock generated
View File

@ -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": "96908a391244cbc96eefbb130bd7bed9",
"content-hash": "dc4f3d21b0f54361b6d4b85674fc900e",
"packages": [
{
"name": "apimatic/jsonmapper",
@ -7789,6 +7789,68 @@
],
"time": "2021-09-25T23:10:38+00:00"
},
{
"name": "razorpay/razorpay",
"version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/razorpay/razorpay-php.git",
"reference": "f562c919d153c343428c9a4e8d4e0848f334aef4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/razorpay/razorpay-php/zipball/f562c919d153c343428c9a4e8d4e0848f334aef4",
"reference": "f562c919d153c343428c9a4e8d4e0848f334aef4",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.3.0",
"rmccue/requests": "v1.8.0"
},
"require-dev": {
"phpunit/phpunit": "~4.8|~5.0",
"raveren/kint": "1.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Razorpay\\Api\\": "src/",
"Razorpay\\Tests\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Abhay Rana",
"email": "nemo@razorpay.com",
"homepage": "https://captnemo.in",
"role": "Developer"
},
{
"name": "Shashank Kumar",
"email": "shashank@razorpay.com",
"role": "Developer"
}
],
"description": "Razorpay PHP Client Library",
"homepage": "https://docs.razorpay.com",
"keywords": [
"api",
"client",
"php",
"razorpay"
],
"support": {
"email": "contact@razorpay.com",
"issues": "https://github.com/Razorpay/razorpay-php/issues",
"source": "https://github.com/Razorpay/razorpay-php"
},
"time": "2021-09-16T06:18:12+00:00"
},
{
"name": "react/promise",
"version": "v2.8.0",
@ -7839,6 +7901,66 @@
},
"time": "2020-05-12T15:16:56+00:00"
},
{
"name": "rmccue/requests",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress/Requests.git",
"reference": "afbe4790e4def03581c4a0963a1e8aa01f6030f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress/Requests/zipball/afbe4790e4def03581c4a0963a1e8aa01f6030f1",
"reference": "afbe4790e4def03581c4a0963a1e8aa01f6030f1",
"shasum": ""
},
"require": {
"php": ">=5.2"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7",
"php-parallel-lint/php-console-highlighter": "^0.5.0",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpcompatibility/php-compatibility": "^9.0",
"phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5",
"requests/test-server": "dev-master",
"squizlabs/php_codesniffer": "^3.5",
"wp-coding-standards/wpcs": "^2.0"
},
"type": "library",
"autoload": {
"psr-0": {
"Requests": "library/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Ryan McCue",
"homepage": "http://ryanmccue.info"
}
],
"description": "A HTTP library written in PHP, for human beings.",
"homepage": "http://github.com/WordPress/Requests",
"keywords": [
"curl",
"fsockopen",
"http",
"idna",
"ipv6",
"iri",
"sockets"
],
"support": {
"issues": "https://github.com/WordPress/Requests/issues",
"source": "https://github.com/WordPress/Requests/tree/v1.8.0"
},
"time": "2021-04-27T11:05:25+00:00"
},
{
"name": "sabre/uri",
"version": "2.2.1",
@ -15962,5 +16084,5 @@
"platform-dev": {
"php": "^7.3|^7.4|^8.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

View File

@ -0,0 +1,33 @@
<?php
use App\Models\Gateway;
use App\Models\GatewayType;
use Illuminate\Database\Migrations\Migration;
class Razorpay extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$gateway = new Gateway();
$gateway->id = 58;
$gateway->name = 'Razorpay';
$gateway->key = 'hxd6gwg3ekb9tb3v9lptgx1mqyg69zu9';
$gateway->provider = 'Razorpay';
$gateway->is_offsite = false;
$configuration = new \stdClass;
$configuration->apiKey = '';
$configuration->apiSecret = '';
$gateway->fields = \json_encode($configuration);
$gateway->visible = true;
$gateway->site_url = 'https://razorpay.com';
$gateway->default_gateway_type_id = GatewayType::HOSTED_PAGE;
$gateway->save();
}
}

View File

@ -0,0 +1,24 @@
<?php
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Database\Migrations\Migration;
class AddHostedPageToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$type = new PaymentType();
$type->id = 35;
$type->name = 'Hosted Page';
$type->gateway_type_id = GatewayType::HOSTED_PAGE;
$type->save();
}
}

View File

@ -81,6 +81,7 @@ class PaymentLibrariesSeeder extends Seeder
['id' => 54, 'name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'],
['id' => 55, 'name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 21, 'key' => '54faab2ab6e3223dbe848b1686490baa', 'fields' => '{"name":"","text":""}'],
['id' => 57, 'name' => 'Square', 'provider' => 'Square', 'is_offsite' => false, 'sort_order' => 21, 'key' => '65faab2ab6e3223dbe848b1686490baz', 'fields' => '{"accessToken":"","applicationId":"","locationId":"","testMode":false}'],
['id' => 58, 'name' => 'Razorpay', 'provider' => 'Razorpay', 'is_offsite' => false, 'sort_order' => 21, 'key' => 'hxd6gwg3ekb9tb3v9lptgx1mqyg69zu9', 'fields' => '{"apiKey":"","apiSecret":""}'],
];
foreach ($gateways as $gateway) {
@ -97,7 +98,7 @@ class PaymentLibrariesSeeder extends Seeder
Gateway::query()->update(['visible' => 0]);
Gateway::whereIn('id', [1,7,11,15,20,39,46,55,50,57,52])->update(['visible' => 1]);
Gateway::whereIn('id', [1,7,11,15,20,39,46,55,50,57,52,58])->update(['visible' => 1]);
if (Ninja::isHosted()) {
Gateway::whereIn('id', [20])->update(['visible' => 0]);

View File

@ -0,0 +1,2 @@
/*! For license information please see razorpay-aio.js.LICENSE.txt */
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=27)}({27:function(e,t,n){e.exports=n("tIvh")},tIvh:function(e,t){var n,r=JSON.parse(null===(n=document.querySelector("meta[name=razorpay-options]"))||void 0===n?void 0:n.content);r.handler=function(e){document.getElementById("razorpay_payment_id").value=e.razorpay_payment_id,document.getElementById("razorpay_signature").value=e.razorpay_signature,document.getElementById("server-response").submit()};var o=new Razorpay(r);document.getElementById("pay-now").onclick=function(e){e.target.parentElement.disabled=!0,o.open()}}});

View File

@ -0,0 +1,9 @@
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -15,6 +15,7 @@
"/js/clients/payments/eway-credit-card.js": "/js/clients/payments/eway-credit-card.js?id=08ea84e9451abd434cff",
"/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js?id=73b66e88e2daabcd6549",
"/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js?id=c2b5f7831e1a46dd5fb2",
"/js/clients/payments/razorpay-aio.js": "/js/clients/payments/razorpay-aio.js?id=817ab3b2b94ee37b14eb",
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js?id=070c86b293b532c5a56c",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=81c2623fc1e5769b51c7",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=665ddf663500767f1a17",

View File

@ -0,0 +1,29 @@
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
let options = JSON.parse(
document.querySelector('meta[name=razorpay-options]')?.content
);
options.handler = function(response) {
document.getElementById('razorpay_payment_id').value =
response.razorpay_payment_id;
document.getElementById('razorpay_signature').value =
response.razorpay_signature;
document.getElementById('server-response').submit();
};
let razorpay = new Razorpay(options);
document.getElementById('pay-now').onclick = function(event) {
event.target.parentElement.disabled = true;
razorpay.open();
};

View File

@ -4317,6 +4317,7 @@ $LANG = array(
'kbc_cbc' => 'KBC/CBC',
'bancontact' => 'Bancontact',
'ideal' => 'iDEAL',
'aio_checkout' => 'All-in-one checkout',
);
return $LANG;

View File

@ -0,0 +1,8 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.aio_checkout'), 'card_title' =>
ctrans('texts.aio_checkout')])
@section('gateway_content')
@component('portal.ninja2020.components.general.card-element-single')
{{ __('texts.payment_method_cannot_be_preauthorized') }}
@endcomponent
@endsection

View File

@ -0,0 +1,36 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.aio_checkout'), 'card_title' =>
ctrans('texts.aio_checkout')])
@section('gateway_head')
<meta name="razorpay-options" content="{{ \json_encode($options) }}">
@endsection
@section('gateway_content')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="store_card">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="razorpay_payment_id" id="razorpay_payment_id">
<input type="hidden" name="razorpay_signature" id="razorpay_signature">
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
{{ ctrans('texts.aio_checkout') }}
@endcomponent
@include('portal.ninja2020.gateways.includes.payment_details')
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@section('gateway_footer')
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<script src="{{ asset('js/clients/payments/razorpay-aio.js') }}"></script>
@endsection

4
webpack.mix.js vendored
View File

@ -106,6 +106,10 @@ mix.js("resources/js/app.js", "public/js")
"resources/js/clients/statements/view.js",
"public/js/clients/statements/view.js",
)
.js(
"resources/js/clients/payments/razorpay-aio.js",
"public/js/clients/payments/razorpay-aio.js"
)
mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css');