diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index c1469142d729..8bd0b64eb09f 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -138,6 +138,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; diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index e0b0fbfdcd35..7a77a56e5142 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -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; diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php index ff396181578e..faaeb9052f0a 100644 --- a/app/Models/PaymentType.php +++ b/app/Models/PaymentType.php @@ -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) { diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php index 316ada10063b..2565c4d003b4 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -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; diff --git a/app/PaymentDrivers/Razorpay/Hosted.php b/app/PaymentDrivers/Razorpay/Hosted.php new file mode 100644 index 000000000000..736a31c4e762 --- /dev/null +++ b/app/PaymentDrivers/Razorpay/Hosted.php @@ -0,0 +1,184 @@ +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()); + } +} diff --git a/app/PaymentDrivers/RazorpayPaymentDriver.php b/app/PaymentDrivers/RazorpayPaymentDriver.php new file mode 100644 index 000000000000..927ca27805d4 --- /dev/null +++ b/app/PaymentDrivers/RazorpayPaymentDriver.php @@ -0,0 +1,102 @@ + 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, '.', ''); + } +} diff --git a/composer.json b/composer.json index 9e3eee567db6..f981ecbe61c4 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index cd3141490d06..1f7570d4f7e8 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } diff --git a/database/migrations/2021_10_07_141737_razorpay.php b/database/migrations/2021_10_07_141737_razorpay.php new file mode 100644 index 000000000000..6dcacd66d63e --- /dev/null +++ b/database/migrations/2021_10_07_141737_razorpay.php @@ -0,0 +1,33 @@ +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(); + } +} diff --git a/database/migrations/2021_10_07_155410_add_hosted_page_to_payment_types.php b/database/migrations/2021_10_07_155410_add_hosted_page_to_payment_types.php new file mode 100644 index 000000000000..8aefe97af473 --- /dev/null +++ b/database/migrations/2021_10_07_155410_add_hosted_page_to_payment_types.php @@ -0,0 +1,24 @@ +id = 35; + $type->name = 'Hosted Page'; + $type->gateway_type_id = GatewayType::HOSTED_PAGE; + + $type->save(); + } +} diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index d512c31b7350..ab9431bbf0e5 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -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]); diff --git a/public/js/clients/payments/razorpay-aio.js b/public/js/clients/payments/razorpay-aio.js new file mode 100644 index 000000000000..dbf83802dd09 --- /dev/null +++ b/public/js/clients/payments/razorpay-aio.js @@ -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()}}}); \ No newline at end of file diff --git a/public/js/clients/payments/razorpay-aio.js.LICENSE.txt b/public/js/clients/payments/razorpay-aio.js.LICENSE.txt new file mode 100644 index 000000000000..c357ff50e6ab --- /dev/null +++ b/public/js/clients/payments/razorpay-aio.js.LICENSE.txt @@ -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 + */ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index e52f5e227431..d6a844a098c8 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -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", diff --git a/resources/js/clients/payments/razorpay-aio.js b/resources/js/clients/payments/razorpay-aio.js new file mode 100644 index 000000000000..2ae159a3e7f6 --- /dev/null +++ b/resources/js/clients/payments/razorpay-aio.js @@ -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(); +}; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 60386c99badb..7611693792fe 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4319,6 +4319,7 @@ $LANG = array( 'sepa_mandat' => 'By providing your IBAN and confirming this payment, you are authorizing Rocketship Inc. and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.', 'ideal' => 'iDEAL', 'bank_account_holder' => 'Bank Account Holder', + 'aio_checkout' => 'All-in-one checkout', ); return $LANG; diff --git a/resources/views/portal/ninja2020/gateways/razorpay/hosted/authorize.blade.php b/resources/views/portal/ninja2020/gateways/razorpay/hosted/authorize.blade.php new file mode 100644 index 000000000000..4f78e4bb4fd4 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/razorpay/hosted/authorize.blade.php @@ -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 \ No newline at end of file diff --git a/resources/views/portal/ninja2020/gateways/razorpay/hosted/pay.blade.php b/resources/views/portal/ninja2020/gateways/razorpay/hosted/pay.blade.php new file mode 100644 index 000000000000..43a76890f809 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/razorpay/hosted/pay.blade.php @@ -0,0 +1,36 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.aio_checkout'), 'card_title' => +ctrans('texts.aio_checkout')]) + +@section('gateway_head') + +@endsection + +@section('gateway_content') +
+ @csrf + + + + + + + + + +
+ + + + @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') + + +@endsection diff --git a/webpack.mix.js b/webpack.mix.js index 01fcbf04df88..906679251a59 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -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');