mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 05:02:36 +01:00
Fixes for Molli
This commit is contained in:
commit
bc81043973
@ -743,6 +743,7 @@ class CreateSingleAccount extends Command
|
||||
$cg->save();
|
||||
}
|
||||
|
||||
|
||||
if (config('ninja.testvars.paytrace.decrypted') && ($this->gateway == 'all' || $this->gateway == 'paytrace')) {
|
||||
$cg = new CompanyGateway;
|
||||
$cg->company_id = $company->id;
|
||||
@ -753,6 +754,29 @@ class CreateSingleAccount extends Command
|
||||
$cg->require_shipping_address = true;
|
||||
$cg->update_details = true;
|
||||
$cg->config = encrypt(config('ninja.testvars.paytrace.decrypted'));
|
||||
|
||||
$cg->save();
|
||||
|
||||
|
||||
$gateway_types = $cg->driver(new Client)->gatewayTypes();
|
||||
|
||||
$fees_and_limits = new stdClass;
|
||||
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
|
||||
|
||||
$cg->fees_and_limits = $fees_and_limits;
|
||||
$cg->save();
|
||||
}
|
||||
|
||||
if (config('ninja.testvars.mollie') && ($this->gateway == 'all' || $this->gateway == 'mollie')) {
|
||||
$cg = new CompanyGateway;
|
||||
$cg->company_id = $company->id;
|
||||
$cg->user_id = $user->id;
|
||||
$cg->gateway_key = '1bd651fb213ca0c9d66ae3c336dc77e8';
|
||||
$cg->require_cvv = true;
|
||||
$cg->require_billing_address = true;
|
||||
$cg->require_shipping_address = true;
|
||||
$cg->update_details = true;
|
||||
$cg->config = encrypt(config('ninja.testvars.mollie'));
|
||||
$cg->save();
|
||||
|
||||
$gateway_types = $cg->driver(new Client)->gatewayTypes();
|
||||
|
27
app/Http/Controllers/Gateways/Mollie3dsController.php
Normal file
27
app/Http/Controllers/Gateways/Mollie3dsController.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Gateways;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Gateways\Mollie\Mollie3dsRequest;
|
||||
use App\Models\PaymentHash;
|
||||
|
||||
class Mollie3dsController extends Controller
|
||||
{
|
||||
public function index(Mollie3dsRequest $request)
|
||||
{
|
||||
return $request->getCompanyGateway()
|
||||
->driver($request->getClient())
|
||||
->process3dsConfirmation($request);
|
||||
}
|
||||
}
|
@ -13,26 +13,14 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Payments\PaymentWebhookRequest;
|
||||
use App\Libraries\MultiDB;
|
||||
use Auth;
|
||||
|
||||
class PaymentWebhookController extends Controller
|
||||
{
|
||||
public function __invoke(PaymentWebhookRequest $request, string $company_key, string $company_gateway_id)
|
||||
public function __invoke(PaymentWebhookRequest $request)
|
||||
{
|
||||
|
||||
$payment = $request->getPayment();
|
||||
|
||||
if(!$payment)
|
||||
return response()->json(['message' => 'Payment record not found.'], 400);
|
||||
|
||||
$client = is_null($payment) ? $request->getClient() : $payment->client;
|
||||
|
||||
if(!$client)
|
||||
return response()->json(['message' => 'Client record not found.'], 400);
|
||||
|
||||
return $request->getCompanyGateway()
|
||||
->driver($client)
|
||||
->processWebhookRequest($request, $payment);
|
||||
return $request
|
||||
->getCompanyGateway()
|
||||
->driver()
|
||||
->processWebhookRequest($request);
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,11 @@ class PaymentResponseRequest extends FormRequest
|
||||
return PaymentHash::whereRaw('BINARY `hash`= ?', [$input['payment_hash']])->first();
|
||||
}
|
||||
|
||||
public function shouldStoreToken(): bool
|
||||
{
|
||||
return (bool) $this->store_card;
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
if ($this->has('store_card')) {
|
||||
|
73
app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php
Normal file
73
app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\Gateways\Mollie;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientGatewayToken;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanyGateway;
|
||||
use App\Models\PaymentHash;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class Mollie3dsRequest extends FormRequest
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public function getCompany(): ?Company
|
||||
{
|
||||
return Company::where('company_key', $this->company_key)->first();
|
||||
}
|
||||
|
||||
public function getCompanyGateway(): ?CompanyGateway
|
||||
{
|
||||
return CompanyGateway::find($this->decodePrimaryKey($this->company_gateway_id));
|
||||
}
|
||||
|
||||
public function getPaymentHash(): ?PaymentHash
|
||||
{
|
||||
return PaymentHash::where('hash', $this->hash)->first();
|
||||
}
|
||||
|
||||
public function getClient(): ?Client
|
||||
{
|
||||
return Client::find($this->getPaymentHash()->data->client_id);
|
||||
}
|
||||
|
||||
public function getPaymentId(): ?string
|
||||
{
|
||||
return $this->getPaymentHash()->data->payment_id;
|
||||
}
|
||||
}
|
@ -122,7 +122,7 @@ class CompanyGateway extends BaseModel
|
||||
}
|
||||
|
||||
/* This is the public entry point into the payment superclass */
|
||||
public function driver(Client $client)
|
||||
public function driver(Client $client = null)
|
||||
{
|
||||
$class = static::driver_class();
|
||||
|
||||
|
@ -106,10 +106,15 @@ class Gateway extends StaticModel
|
||||
break;
|
||||
case 50:
|
||||
return [
|
||||
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
|
||||
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], //Braintree
|
||||
GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true]
|
||||
];
|
||||
break;
|
||||
case 7:
|
||||
return [
|
||||
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], // Mollie
|
||||
];
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
break;
|
||||
|
@ -20,7 +20,6 @@ class PaymentHash extends Model
|
||||
protected $casts = [
|
||||
'data' => 'object',
|
||||
];
|
||||
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
@ -41,4 +40,12 @@ class PaymentHash extends Model
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'fee_invoice_id', 'id');
|
||||
}
|
||||
|
||||
public function withData(string $property, $value): self
|
||||
{
|
||||
$this->data = array_merge((array) $this->data, [$property => $value]);
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,8 @@ class SystemLog extends Model
|
||||
const TYPE_WEPAY = 309;
|
||||
const TYPE_PAYFAST = 310;
|
||||
const TYPE_PAYTRACE = 311;
|
||||
|
||||
const TYPE_MOLLIE = 312;
|
||||
|
||||
const TYPE_QUOTA_EXCEEDED = 400;
|
||||
const TYPE_UPSTREAM_FAILURE = 401;
|
||||
|
||||
|
239
app/PaymentDrivers/Mollie/CreditCard.php
Normal file
239
app/PaymentDrivers/Mollie/CreditCard.php
Normal file
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace App\PaymentDrivers\Mollie;
|
||||
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
|
||||
use App\Jobs\Mail\PaymentFailureMailer;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Models\ClientGatewayToken;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\Payment;
|
||||
use App\Models\PaymentType;
|
||||
use App\Models\SystemLog;
|
||||
use App\PaymentDrivers\MolliePaymentDriver;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CreditCard
|
||||
{
|
||||
/**
|
||||
* @var MolliePaymentDriver
|
||||
*/
|
||||
protected $mollie;
|
||||
|
||||
public function __construct(MolliePaymentDriver $mollie)
|
||||
{
|
||||
$this->mollie = $mollie;
|
||||
|
||||
$this->mollie->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for credit card payments.
|
||||
*
|
||||
* @param array $data
|
||||
* @return Factory|View
|
||||
*/
|
||||
public function paymentView(array $data)
|
||||
{
|
||||
$data['gateway'] = $this->mollie;
|
||||
|
||||
return render('gateways.mollie.credit_card.pay', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payment object.
|
||||
*
|
||||
* @param PaymentResponseRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function paymentResponse(PaymentResponseRequest $request)
|
||||
{
|
||||
$amount = $this->mollie->convertToMollieAmount((float) $this->mollie->payment_hash->data->amount_with_fee);
|
||||
|
||||
$this->mollie->payment_hash
|
||||
->withData('gateway_type_id', GatewayType::CREDIT_CARD)
|
||||
->withData('client_id', $this->mollie->client->id);
|
||||
|
||||
if (!empty($request->token)) {
|
||||
try {
|
||||
$cgt = ClientGatewayToken::where('token', $request->token)->firstOrFail();
|
||||
|
||||
$payment = $this->mollie->gateway->payments->create([
|
||||
'amount' => [
|
||||
'currency' => $this->mollie->client->currency()->code,
|
||||
'value' => $amount,
|
||||
],
|
||||
'mandateId' => $request->token,
|
||||
'customerId' => $cgt->gateway_customer_reference,
|
||||
'sequenceType' => 'recurring',
|
||||
'description' => \sprintf('Hash: %s', $this->mollie->payment_hash->hash),
|
||||
'webhookUrl' => $this->mollie->company_gateway->webhookUrl(),
|
||||
]);
|
||||
|
||||
if ($payment->status === 'paid') {
|
||||
$this->mollie->logSuccessfulGatewayResponse(
|
||||
['response' => $payment, 'data' => $this->mollie->payment_hash],
|
||||
SystemLog::TYPE_MOLLIE
|
||||
);
|
||||
|
||||
return $this->processSuccessfulPayment($payment);
|
||||
}
|
||||
|
||||
if ($payment->status === 'open') {
|
||||
$this->mollie->payment_hash->withData('payment_id', $payment->id);
|
||||
|
||||
return redirect($payment->getCheckoutUrl());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->processUnsuccessfulPayment($e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$data = [
|
||||
'amount' => [
|
||||
'currency' => $this->mollie->client->currency()->code,
|
||||
'value' => $amount,
|
||||
],
|
||||
'description' => \sprintf('Hash: %s', $this->mollie->payment_hash->hash),
|
||||
'redirectUrl' => route('mollie.3ds_redirect', [
|
||||
'company_key' => $this->mollie->client->company->company_key,
|
||||
'company_gateway_id' => $this->mollie->company_gateway->hashed_id,
|
||||
'hash' => $this->mollie->payment_hash->hash,
|
||||
]),
|
||||
'webhookUrl' => $this->mollie->company_gateway->webhookUrl(),
|
||||
'cardToken' => $request->gateway_response,
|
||||
];
|
||||
|
||||
if ($request->shouldStoreToken()) {
|
||||
$customer = $this->mollie->gateway->customers->create([
|
||||
'name' => $this->mollie->client->name,
|
||||
'email' => $this->mollie->client->present()->email(),
|
||||
'metadata' => [
|
||||
'id' => $this->mollie->client->hashed_id,
|
||||
],
|
||||
]);
|
||||
|
||||
$data['customerId'] = $customer->id;
|
||||
$data['sequenceType'] = 'first';
|
||||
|
||||
$this->mollie->payment_hash
|
||||
->withData('mollieCustomerId', $customer->id)
|
||||
->withData('shouldStoreToken', true);
|
||||
}
|
||||
|
||||
$payment = $this->mollie->gateway->payments->create($data);
|
||||
|
||||
if ($payment->status === 'paid') {
|
||||
$this->mollie->logSuccessfulGatewayResponse(
|
||||
['response' => $payment, 'data' => $this->mollie->payment_hash],
|
||||
SystemLog::TYPE_MOLLIE
|
||||
);
|
||||
|
||||
return $this->processSuccessfulPayment($payment);
|
||||
}
|
||||
|
||||
if ($payment->status === 'open') {
|
||||
$this->mollie->payment_hash->withData('payment_id', $payment->id);
|
||||
|
||||
return redirect($payment->getCheckoutUrl());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->processUnsuccessfulPayment($e);
|
||||
|
||||
throw new PaymentFailed($e->getMessage(), $e->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment)
|
||||
{
|
||||
$payment_hash = $this->mollie->payment_hash;
|
||||
|
||||
if (property_exists($payment_hash->data, 'shouldStoreToken') && $payment_hash->data->shouldStoreToken) {
|
||||
try {
|
||||
$mandates = \iterator_to_array($this->mollie->gateway->mandates->listForId($payment_hash->data->mollieCustomerId));
|
||||
} catch (\Mollie\Api\Exceptions\ApiException $e) {
|
||||
return $this->processUnsuccessfulPayment($e);
|
||||
}
|
||||
|
||||
$payment_meta = new \stdClass;
|
||||
$payment_meta->exp_month = (string) $mandates[0]->details->cardExpiryDate;
|
||||
$payment_meta->exp_year = (string) '';
|
||||
$payment_meta->brand = (string) $mandates[0]->details->cardLabel;
|
||||
$payment_meta->last4 = (string) $mandates[0]->details->cardNumber;
|
||||
$payment_meta->type = GatewayType::CREDIT_CARD;
|
||||
|
||||
$this->mollie->storeGatewayToken([
|
||||
'token' => $mandates[0]->id,
|
||||
'payment_method_id' => GatewayType::CREDIT_CARD,
|
||||
'payment_meta' => $payment_meta,
|
||||
], ['gateway_customer_reference' => $payment_hash->data->mollieCustomerId]);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'gateway_type_id' => GatewayType::CREDIT_CARD,
|
||||
'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total,
|
||||
'payment_type' => PaymentType::CREDIT_CARD_OTHER,
|
||||
'transaction_reference' => $payment->id,
|
||||
];
|
||||
|
||||
$payment_record = $this->mollie->createPayment($data, Payment::STATUS_COMPLETED);
|
||||
|
||||
SystemLogger::dispatch(
|
||||
['response' => $payment, 'data' => $data],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_SUCCESS,
|
||||
SystemLog::TYPE_MOLLIE,
|
||||
$this->mollie->client,
|
||||
$this->mollie->client->company,
|
||||
);
|
||||
|
||||
return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]);
|
||||
}
|
||||
|
||||
public function processUnsuccessfulPayment(\Exception $e)
|
||||
{
|
||||
PaymentFailureMailer::dispatch(
|
||||
$this->mollie->client,
|
||||
$e->getMessage(),
|
||||
$this->mollie->client->company,
|
||||
$this->mollie->payment_hash->data->amount_with_fee
|
||||
);
|
||||
|
||||
SystemLogger::dispatch(
|
||||
$e->getMessage(),
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_MOLLIE,
|
||||
$this->mollie->client,
|
||||
$this->mollie->client->company,
|
||||
);
|
||||
|
||||
throw new PaymentFailed($e->getMessage(), $e->getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show authorization page.
|
||||
*
|
||||
* @param array $data
|
||||
* @return Factory|View
|
||||
*/
|
||||
public function authorizeView(array $data)
|
||||
{
|
||||
return render('gateways.mollie.credit_card.authorize', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle authorization response.
|
||||
*
|
||||
* @param mixed $request
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function authorizeResponse($request): RedirectResponse
|
||||
{
|
||||
return redirect()->route('client.payment_methods.index');
|
||||
}
|
||||
}
|
354
app/PaymentDrivers/MolliePaymentDriver.php
Normal file
354
app/PaymentDrivers/MolliePaymentDriver.php
Normal file
@ -0,0 +1,354 @@
|
||||
<?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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\PaymentDrivers;
|
||||
|
||||
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
|
||||
use App\Http\Requests\Gateways\Mollie\Mollie3dsRequest;
|
||||
use App\Http\Requests\Payments\PaymentWebhookRequest;
|
||||
use App\Jobs\Mail\PaymentFailureMailer;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Models\ClientGatewayToken;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\PaymentHash;
|
||||
use App\Models\PaymentType;
|
||||
use App\Models\SystemLog;
|
||||
use App\PaymentDrivers\Mollie\CreditCard;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Mollie\Api\Exceptions\ApiException;
|
||||
use Mollie\Api\MollieApiClient;
|
||||
|
||||
class MolliePaymentDriver extends BaseDriver
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
public $refundable = true;
|
||||
|
||||
/**
|
||||
* @var true
|
||||
*/
|
||||
public $token_billing = true;
|
||||
|
||||
/**
|
||||
* @var true
|
||||
*/
|
||||
public $can_authorise_credit_card = true;
|
||||
|
||||
/**
|
||||
* @var MollieApiClient
|
||||
*/
|
||||
public $gateway;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $payment_method;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public static $methods = [
|
||||
GatewayType::CREDIT_CARD => CreditCard::class,
|
||||
];
|
||||
|
||||
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
|
||||
|
||||
public function init(): self
|
||||
{
|
||||
$this->gateway = new MollieApiClient();
|
||||
|
||||
$this->gateway->setApiKey(
|
||||
$this->company_gateway->getConfigField('apiKey'),
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function gatewayTypes(): array
|
||||
{
|
||||
$types = [];
|
||||
|
||||
$types[] = GatewayType::CREDIT_CARD;
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
try {
|
||||
$payment = $this->gateway->payments->get($payment->transaction_reference);
|
||||
|
||||
$refund = $this->gateway->payments->refund($payment, [
|
||||
'amount' => [
|
||||
'currency' => $this->client->currency()->code,
|
||||
'value' => $this->convertToMollieAmount((float) $amount),
|
||||
],
|
||||
]);
|
||||
|
||||
if ($refund->status === 'refunded') {
|
||||
SystemLogger::dispatch(
|
||||
['server_response' => $refund, 'data' => request()->all()],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_SUCCESS,
|
||||
SystemLog::TYPE_MOLLIE,
|
||||
$this->client,
|
||||
$this->client->company
|
||||
);
|
||||
|
||||
return [
|
||||
'transaction_reference' => $refund->id,
|
||||
'transaction_response' => json_encode($refund),
|
||||
'success' => $refund->status === 'refunded' ? true : false,
|
||||
'description' => $refund->description,
|
||||
'code' => 200,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'transaction_reference' => $refund->id,
|
||||
'transaction_response' => json_encode($refund),
|
||||
'success' => true,
|
||||
'description' => $refund->description,
|
||||
'code' => 0,
|
||||
];
|
||||
} catch (ApiException $e) {
|
||||
SystemLogger::dispatch(
|
||||
['server_response' => $refund, 'data' => request()->all()],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_MOLLIE,
|
||||
$this->client,
|
||||
$this->client->companyk
|
||||
);
|
||||
|
||||
nlog($e->getMessage());
|
||||
|
||||
return [
|
||||
'transaction_reference' => null,
|
||||
'transaction_response' => $e->getMessage(),
|
||||
'success' => false,
|
||||
'description' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
|
||||
{
|
||||
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
|
||||
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();
|
||||
|
||||
if ($invoice) {
|
||||
$description = "Invoice {$invoice->number} for {$amount} for client {$this->client->present()->name()}";
|
||||
} else {
|
||||
$description = "Payment with no invoice for amount {$amount} for client {$this->client->present()->name()}";
|
||||
}
|
||||
|
||||
$request = new PaymentResponseRequest();
|
||||
$request->setMethod('POST');
|
||||
$request->request->add(['payment_hash' => $payment_hash->hash]);
|
||||
|
||||
$this->init();
|
||||
|
||||
try {
|
||||
$payment = $this->gateway->payments->create([
|
||||
'amount' => [
|
||||
'currency' => $this->client->currency()->code,
|
||||
'value' => $this->convertToMollieAmount($amount),
|
||||
],
|
||||
'mandateId' => $cgt->token,
|
||||
'customerId' => $cgt->gateway_customer_reference,
|
||||
'sequenceType' => 'recurring',
|
||||
'description' => $description,
|
||||
'webhookUrl' => $this->company_gateway->webhookUrl(),
|
||||
]);
|
||||
|
||||
if ($payment->status === 'paid') {
|
||||
$this->confirmGatewayFee($request);
|
||||
|
||||
$data = [
|
||||
'payment_method' => $cgt->token,
|
||||
'payment_type' => PaymentType::CREDIT_CARD_OTHER,
|
||||
'amount' => $amount,
|
||||
'transaction_reference' => $payment->id,
|
||||
'gateway_type_id' => GatewayType::CREDIT_CARD,
|
||||
];
|
||||
|
||||
$payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
|
||||
|
||||
SystemLogger::dispatch(
|
||||
['response' => $payment, 'data' => $data],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_SUCCESS,
|
||||
SystemLog::TYPE_MOLLIE,
|
||||
$this->client
|
||||
);
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
$this->unWindGatewayFees($payment_hash);
|
||||
|
||||
PaymentFailureMailer::dispatch(
|
||||
$this->client,
|
||||
$payment->details,
|
||||
$this->client->company,
|
||||
$amount
|
||||
);
|
||||
|
||||
$message = [
|
||||
'server_response' => $payment,
|
||||
'data' => $payment_hash->data,
|
||||
];
|
||||
|
||||
SystemLogger::dispatch(
|
||||
$message,
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_CHECKOUT,
|
||||
$this->client
|
||||
);
|
||||
|
||||
return false;
|
||||
} catch (ApiException $e) {
|
||||
$this->unWindGatewayFees($payment_hash);
|
||||
|
||||
$data = [
|
||||
'status' => '',
|
||||
'error_type' => '',
|
||||
'error_code' => $e->getCode(),
|
||||
'param' => '',
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
|
||||
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_MOLLIE, $this->client, $this->client->company);
|
||||
}
|
||||
}
|
||||
|
||||
public function processWebhookRequest(PaymentWebhookRequest $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'id' => ['required', 'starts_with:tr'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
$this->init();
|
||||
|
||||
$codes = [
|
||||
'open' => Payment::STATUS_PENDING,
|
||||
'canceled' => Payment::STATUS_CANCELLED,
|
||||
'pending' => Payment::STATUS_PENDING,
|
||||
'expired' => Payment::STATUS_CANCELLED,
|
||||
'failed' => Payment::STATUS_FAILED,
|
||||
'paid' => Payment::STATUS_COMPLETED,
|
||||
];
|
||||
|
||||
try {
|
||||
$payment = $this->gateway->payments->get($request->id);
|
||||
|
||||
$record = Payment::where('transaction_reference', $payment->id)->firstOrFail();
|
||||
$record->status_id = $codes[$payment->status];
|
||||
$record->save();
|
||||
|
||||
return response()->json([], 200);
|
||||
} catch (ApiException $e) {
|
||||
return response()->json(['message' => $e->getMessage(), 'gatewayStatusCode' => $e->getCode()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function process3dsConfirmation(Mollie3dsRequest $request)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
$this->setPaymentHash($request->getPaymentHash());
|
||||
|
||||
try {
|
||||
$payment = $this->gateway->payments->get($request->getPaymentId());
|
||||
|
||||
return (new CreditCard($this))->processSuccessfulPayment($payment);
|
||||
} catch (\Mollie\Api\Exceptions\ApiException $e) {
|
||||
return (new CreditCard($this))->processUnsuccessfulPayment($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function detach(ClientGatewayToken $token)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
try {
|
||||
$this->gateway->mandates->revokeForId($token->gateway_customer_reference, $token->token);
|
||||
} catch (\Mollie\Api\Exceptions\ApiException $e) {
|
||||
SystemLogger::dispatch(
|
||||
[
|
||||
'server_response' => $e->getMessage(),
|
||||
'data' => request()->all(),
|
||||
],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_MOLLIE,
|
||||
$this->client,
|
||||
$this->client->company
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the amount to the format that Mollie supports.
|
||||
*
|
||||
* @param mixed|float $amount
|
||||
* @return string
|
||||
*/
|
||||
public function convertToMollieAmount($amount): string
|
||||
{
|
||||
return \number_format((float) $amount, 2, '.', '');
|
||||
}
|
||||
}
|
@ -62,6 +62,7 @@
|
||||
"league/omnipay": "^3.1",
|
||||
"livewire/livewire": "^2.4",
|
||||
"maennchen/zipstream-php": "^1.2",
|
||||
"mollie/mollie-api-php": "^2.36",
|
||||
"nwidart/laravel-modules": "^8.0",
|
||||
"omnipay/paypal": "^3.0",
|
||||
"payfast/payfast-php-sdk": "^1.1",
|
||||
|
93
composer.lock
generated
93
composer.lock
generated
@ -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": "d2beb37ff5fbee59ad4bb792e944eb10",
|
||||
"content-hash": "275a9dd3910b6ec79607b098406dc6c7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "asm/php-ansible",
|
||||
@ -4386,6 +4386,97 @@
|
||||
},
|
||||
"time": "2019-07-17T11:01:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mollie/mollie-api-php",
|
||||
"version": "v2.36.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mollie/mollie-api-php.git",
|
||||
"reference": "19f69c116d47a3600f0ed629e0df925a43d3a8f5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/19f69c116d47a3600f0ed629e0df925a43d3a8f5",
|
||||
"reference": "19f69c116d47a3600f0ed629e0df925a43d3a8f5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/ca-bundle": "^1.1",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"eloquent/liberator": "^2.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"guzzlehttp/guzzle": "^6.3 || ^7.0",
|
||||
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.1 || ^8.5"
|
||||
},
|
||||
"suggest": {
|
||||
"mollie/oauth2-mollie-php": "Use OAuth to authenticate with the Mollie API. This is needed for some endpoints. Visit https://docs.mollie.com/ for more information."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mollie\\Api\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mollie B.V.",
|
||||
"email": "info@mollie.com"
|
||||
}
|
||||
],
|
||||
"description": "Mollie API client library for PHP. Mollie is a European Payment Service provider and offers international payment methods such as Mastercard, VISA, American Express and PayPal, and local payment methods such as iDEAL, Bancontact, SOFORT Banking, SEPA direct debit, Belfius Direct Net, KBC Payment Button and various gift cards such as Podiumcadeaukaart and fashioncheque.",
|
||||
"homepage": "https://www.mollie.com/en/developers",
|
||||
"keywords": [
|
||||
"Apple Pay",
|
||||
"CBC",
|
||||
"Przelewy24",
|
||||
"api",
|
||||
"bancontact",
|
||||
"banktransfer",
|
||||
"belfius",
|
||||
"belfius direct net",
|
||||
"charges",
|
||||
"creditcard",
|
||||
"direct debit",
|
||||
"fashioncheque",
|
||||
"gateway",
|
||||
"gift cards",
|
||||
"ideal",
|
||||
"inghomepay",
|
||||
"intersolve",
|
||||
"kbc",
|
||||
"klarna",
|
||||
"mistercash",
|
||||
"mollie",
|
||||
"paylater",
|
||||
"payment",
|
||||
"payments",
|
||||
"paypal",
|
||||
"paysafecard",
|
||||
"podiumcadeaukaart",
|
||||
"recurring",
|
||||
"refunds",
|
||||
"sepa",
|
||||
"service",
|
||||
"sliceit",
|
||||
"sofort",
|
||||
"sofortbanking",
|
||||
"subscriptions"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/mollie/mollie-api-php/issues",
|
||||
"source": "https://github.com/mollie/mollie-api-php/tree/v2.36.1"
|
||||
},
|
||||
"time": "2021-06-23T12:55:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "moneyphp/money",
|
||||
"version": "v3.3.1",
|
||||
|
@ -89,6 +89,7 @@ return [
|
||||
'password' => env('PAYTRACE_P',''),
|
||||
'decrypted' => env('PAYTRACE_KEYS', ''),
|
||||
],
|
||||
'mollie' => env('MOLLIE_KEYS', ''),
|
||||
],
|
||||
'contact' => [
|
||||
'email' => env('MAIL_FROM_ADDRESS'),
|
||||
|
@ -0,0 +1,50 @@
|
||||
<?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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
use App\Models\Gateway;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ActivateMolliePaymentDriver extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if($mollie = Gateway::find(7))
|
||||
{
|
||||
$mollie->visible = true;
|
||||
|
||||
$fields = json_decode($mollie->fields);
|
||||
$fields->testMode = false;
|
||||
$fields->profileId = '';
|
||||
|
||||
$mollie->fields = json_encode($fields);
|
||||
|
||||
$mollie->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ class PaymentLibrariesSeeder extends Seeder
|
||||
['id' => 4, 'name' => 'FirstData Connect', 'provider' => 'FirstData_Connect', 'key' => '4e0ed0d34552e6cb433506d1ac03a418', 'fields' => '{"storeId":"","sharedSecret":"","testMode":false}'],
|
||||
['id' => 5, 'name' => 'Migs ThreeParty', 'provider' => 'Migs_ThreeParty', 'key' => '513cdc81444c87c4b07258bc2858d3fa', 'fields' => '{"merchantId":"","merchantAccessCode":"","secureHash":""}'],
|
||||
['id' => 6, 'name' => 'Migs TwoParty', 'provider' => 'Migs_TwoParty', 'key' => '99c2a271b5088951334d1302e038c01a', 'fields' => '{"merchantId":"","merchantAccessCode":"","secureHash":""}'],
|
||||
['id' => 7, 'name' => 'Mollie', 'provider' => 'Mollie', 'is_offsite' => true, 'sort_order' => 8, 'key' => '1bd651fb213ca0c9d66ae3c336dc77e8', 'fields' => '{"apiKey":""}'],
|
||||
['id' => 7, 'name' => 'Mollie', 'provider' => 'Mollie', 'is_offsite' => true, 'sort_order' => 8, 'key' => '1bd651fb213ca0c9d66ae3c336dc77e8', 'fields' => '{"apiKey":"","profileId":"","testMode":false}'],
|
||||
['id' => 8, 'name' => 'MultiSafepay', 'provider' => 'MultiSafepay', 'key' => 'c3dec814e14cbd7d86abd92ce6789f8c', 'fields' => '{"accountId":"","siteId":"","siteCode":"","testMode":false}'],
|
||||
['id' => 9, 'name' => 'Netaxept', 'provider' => 'Netaxept', 'key' => '070dffc5ca94f4e66216e44028ebd52d', 'fields' => '{"merchantId":"","password":"","testMode":false}'],
|
||||
['id' => 10, 'name' => 'NetBanx', 'provider' => 'NetBanx', 'key' => '334d419939c06bd99b4dfd8a49243f0f', 'fields' => '{"accountNumber":"","storeId":"","storePassword":"","testMode":false}'],
|
||||
@ -96,7 +96,7 @@ class PaymentLibrariesSeeder extends Seeder
|
||||
|
||||
Gateway::query()->update(['visible' => 0]);
|
||||
|
||||
Gateway::whereIn('id', [1,15,20,39,46,55,50])->update(['visible' => 1]);
|
||||
Gateway::whereIn('id', [1,7,15,20,39,46,55,50])->update(['visible' => 1]);
|
||||
|
||||
if (Ninja::isHosted()) {
|
||||
Gateway::whereIn('id', [20])->update(['visible' => 0]);
|
||||
|
1098
package-lock.json
generated
1098
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2
public/css/app.css
vendored
2
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
2
public/js/clients/payments/mollie-credit-card.js
vendored
Normal file
2
public/js/clients/payments/mollie-credit-card.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/*! For license information please see mollie-credit-card.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=22)}({22:function(e,t,n){e.exports=n("i12I")},i12I:function(e,t){function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function e(){var t,n;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.mollie=Mollie(null===(t=document.querySelector("meta[name=mollie-profileId]"))||void 0===t?void 0:t.content,{testmode:null===(n=document.querySelector("meta[name=mollie-testmode]"))||void 0===n?void 0:n.content,locale:"en_US"})}var t,r,o;return t=e,(r=[{key:"createCardHolderInput",value:function(){var e=this.mollie.createComponent("cardHolder");e.mount("#card-holder");var t=document.getElementById("card-holder-error");return e.addEventListener("change",(function(e){e.error&&e.touched?t.textContent=e.error:t.textContent=""})),this}},{key:"createCardNumberInput",value:function(){var e=this.mollie.createComponent("cardNumber");e.mount("#card-number");var t=document.getElementById("card-number-error");return e.addEventListener("change",(function(e){e.error&&e.touched?t.textContent=e.error:t.textContent=""})),this}},{key:"createExpiryDateInput",value:function(){var e=this.mollie.createComponent("expiryDate");e.mount("#expiry-date");var t=document.getElementById("expiry-date-error");return e.addEventListener("change",(function(e){e.error&&e.touched?t.textContent=e.error:t.textContent=""})),this}},{key:"createCvvInput",value:function(){var e=this.mollie.createComponent("verificationCode");e.mount("#cvv");var t=document.getElementById("cvv-error");return e.addEventListener("change",(function(e){e.error&&e.touched?t.textContent=e.error:t.textContent=""})),this}},{key:"handlePayNowButton",value:function(){if(document.getElementById("pay-now").disabled=!0,""!==document.querySelector("input[name=token]").value)return document.querySelector("input[name=gateway_response]").value="",document.getElementById("server-response").submit();this.mollie.createToken().then((function(e){var t=e.token,n=e.error;if(n){document.getElementById("pay-now").disabled=!1;var r=document.getElementById("errors");return r.innerText=n.message,void(r.hidden=!1)}var o=document.querySelector('input[name="token-billing-checkbox"]:checked');o&&(document.querySelector('input[name="store_card"]').value=o.value),document.querySelector("input[name=gateway_response]").value=t,document.querySelector("input[name=token]").value="",document.getElementById("server-response").submit()}))}},{key:"handle",value:function(){var e=this;this.createCardHolderInput().createCardNumberInput().createExpiryDateInput().createCvvInput(),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(e){return e.addEventListener("click",(function(e){document.getElementById("mollie--payment-container").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token}))})),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",(function(e){document.getElementById("mollie--payment-container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value=""})),document.getElementById("pay-now").addEventListener("click",(function(){return e.handlePayNowButton()}))}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()}});
|
@ -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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
|
||||
"/css/app.css": "/css/app.css?id=5d7e42fa72eef8af62f5",
|
||||
"/css/app.css": "/css/app.css?id=56fdeb0a3b78b00b9a52",
|
||||
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
|
||||
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
|
||||
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
|
||||
@ -11,6 +11,7 @@
|
||||
"/js/clients/payments/braintree-paypal.js": "/js/clients/payments/braintree-paypal.js?id=c35db3cbb65806ab6a8a",
|
||||
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=5469146cd629ea1b5c20",
|
||||
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=065e5450233cc5b47020",
|
||||
"/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=c8d3808a4c02d1392e96",
|
||||
"/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",
|
||||
@ -22,7 +23,5 @@
|
||||
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=5c35d28cf0a3286e7c45",
|
||||
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=fc3055d6a099f523ea98",
|
||||
"/js/setup/setup.js": "/js/setup/setup.js?id=8d454e7090f119552a6c",
|
||||
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad",
|
||||
"/js/admin.js": "/js/admin.js",
|
||||
"/css/admin.css": "/css/admin.css"
|
||||
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad"
|
||||
}
|
||||
|
169
resources/js/clients/payments/mollie-credit-card.js
vendored
Normal file
169
resources/js/clients/payments/mollie-credit-card.js
vendored
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
class _Mollie {
|
||||
constructor() {
|
||||
this.mollie = Mollie(
|
||||
document.querySelector('meta[name=mollie-profileId]')?.content,
|
||||
{
|
||||
testmode: document.querySelector('meta[name=mollie-testmode]')
|
||||
?.content,
|
||||
locale: 'en_US',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createCardHolderInput() {
|
||||
let cardHolder = this.mollie.createComponent('cardHolder');
|
||||
cardHolder.mount('#card-holder');
|
||||
|
||||
let cardHolderError = document.getElementById('card-holder-error');
|
||||
|
||||
cardHolder.addEventListener('change', function(event) {
|
||||
if (event.error && event.touched) {
|
||||
cardHolderError.textContent = event.error;
|
||||
} else {
|
||||
cardHolderError.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
createCardNumberInput() {
|
||||
let cardNumber = this.mollie.createComponent('cardNumber');
|
||||
cardNumber.mount('#card-number');
|
||||
|
||||
let cardNumberError = document.getElementById('card-number-error');
|
||||
|
||||
cardNumber.addEventListener('change', function(event) {
|
||||
if (event.error && event.touched) {
|
||||
cardNumberError.textContent = event.error;
|
||||
} else {
|
||||
cardNumberError.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
createExpiryDateInput() {
|
||||
let expiryDate = this.mollie.createComponent('expiryDate');
|
||||
expiryDate.mount('#expiry-date');
|
||||
|
||||
let expiryDateError = document.getElementById('expiry-date-error');
|
||||
|
||||
expiryDate.addEventListener('change', function(event) {
|
||||
if (event.error && event.touched) {
|
||||
expiryDateError.textContent = event.error;
|
||||
} else {
|
||||
expiryDateError.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
createCvvInput() {
|
||||
let verificationCode = this.mollie.createComponent('verificationCode');
|
||||
verificationCode.mount('#cvv');
|
||||
|
||||
let verificationCodeError = document.getElementById('cvv-error');
|
||||
|
||||
verificationCode.addEventListener('change', function(event) {
|
||||
if (event.error && event.touched) {
|
||||
verificationCodeError.textContent = event.error;
|
||||
} else {
|
||||
verificationCodeError.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
handlePayNowButton() {
|
||||
document.getElementById('pay-now').disabled = true;
|
||||
|
||||
if (document.querySelector('input[name=token]').value !== '') {
|
||||
document.querySelector('input[name=gateway_response]').value = '';
|
||||
|
||||
return document.getElementById('server-response').submit();
|
||||
}
|
||||
|
||||
this.mollie.createToken().then(function(result) {
|
||||
let token = result.token;
|
||||
let error = result.error;
|
||||
|
||||
if (error) {
|
||||
document.getElementById('pay-now').disabled = false;
|
||||
|
||||
let errorsContainer = document.getElementById('errors');
|
||||
errorsContainer.innerText = error.message;
|
||||
errorsContainer.hidden = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let tokenBillingCheckbox = document.querySelector(
|
||||
'input[name="token-billing-checkbox"]:checked'
|
||||
);
|
||||
|
||||
if (tokenBillingCheckbox) {
|
||||
document.querySelector('input[name="store_card"]').value =
|
||||
tokenBillingCheckbox.value;
|
||||
}
|
||||
|
||||
document.querySelector(
|
||||
'input[name=gateway_response]'
|
||||
).value = token;
|
||||
document.querySelector('input[name=token]').value = '';
|
||||
|
||||
document.getElementById('server-response').submit();
|
||||
});
|
||||
}
|
||||
|
||||
handle() {
|
||||
this.createCardHolderInput()
|
||||
.createCardNumberInput()
|
||||
.createExpiryDateInput()
|
||||
.createCvvInput();
|
||||
|
||||
Array.from(
|
||||
document.getElementsByClassName('toggle-payment-with-token')
|
||||
).forEach((element) =>
|
||||
element.addEventListener('click', (element) => {
|
||||
document
|
||||
.getElementById('mollie--payment-container')
|
||||
.classList.add('hidden');
|
||||
document.getElementById('save-card--container').style.display =
|
||||
'none';
|
||||
document.querySelector('input[name=token]').value =
|
||||
element.target.dataset.token;
|
||||
})
|
||||
);
|
||||
|
||||
document
|
||||
.getElementById('toggle-payment-with-credit-card')
|
||||
.addEventListener('click', (element) => {
|
||||
document
|
||||
.getElementById('mollie--payment-container')
|
||||
.classList.remove('hidden');
|
||||
document.getElementById('save-card--container').style.display =
|
||||
'grid';
|
||||
document.querySelector('input[name=token]').value = '';
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById('pay-now')
|
||||
.addEventListener('click', () => this.handlePayNowButton());
|
||||
}
|
||||
}
|
||||
|
||||
new _Mollie().handle();
|
@ -4295,6 +4295,7 @@ $LANG = array(
|
||||
'lang_Arabic' => 'Arabic',
|
||||
'lang_Persian' => 'Persian',
|
||||
'lang_Latvian' => 'Latvian',
|
||||
'expiry_date' => 'Expiry date',
|
||||
);
|
||||
|
||||
return $LANG;
|
||||
|
@ -0,0 +1,8 @@
|
||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.credit_card'), 'card_title' =>
|
||||
ctrans('texts.credit_card')])
|
||||
|
||||
@section('gateway_content')
|
||||
@component('portal.ninja2020.components.general.card-element-single')
|
||||
{{ __('texts.payment_method_cannot_be_authorized_first') }}
|
||||
@endcomponent
|
||||
@endsection
|
@ -0,0 +1,90 @@
|
||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.credit_card'), 'card_title' =>
|
||||
ctrans('texts.credit_card')])
|
||||
|
||||
@section('gateway_head')
|
||||
<meta name="mollie-testmode" content="{{ $gateway->company_gateway->getConfigField('testMode') }}">
|
||||
<meta name="mollie-profileId" content="{{ $gateway->company_gateway->getConfigField('profileId') }}">
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||
<script src="{{ asset('js/clients/payments/card-js.min.js') }}"></script>
|
||||
|
||||
<link href="{{ asset('css/card-js.min.css') }}" rel="stylesheet" type="text/css">
|
||||
@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="token">
|
||||
</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.credit_card') }}
|
||||
@endcomponent
|
||||
|
||||
@include('portal.ninja2020.gateways.includes.payment_details')
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
|
||||
@if (count($tokens) > 0)
|
||||
@foreach ($tokens as $token)
|
||||
<label class="mr-4">
|
||||
<input type="radio" data-token="{{ $token->token }}" name="payment-type"
|
||||
class="form-radio cursor-pointer toggle-payment-with-token" />
|
||||
<span class="ml-1 cursor-pointer">**** {{ optional($token->meta)->last4 }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
<label>
|
||||
<input type="radio" id="toggle-payment-with-credit-card" class="form-radio cursor-pointer" name="payment-type"
|
||||
checked />
|
||||
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
|
||||
</label>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element-single')
|
||||
<div class="flex flex-col" id="mollie--payment-container">
|
||||
<label for="card-number">
|
||||
<span class="text-xs text-gray-900 uppercase">{{ ctrans('texts.card_number') }}</span>
|
||||
<div class="input w-full" type="text" id="card-number"></div>
|
||||
<div class="text-xs text-red-500 mt-1 block" id="card-number-error"></div>
|
||||
</label>
|
||||
|
||||
<label for="card-holder" class="block mt-2">
|
||||
<span class="text-xs text-gray-900 uppercase">{{ ctrans('texts.name') }}</span>
|
||||
<div class="input w-full" type="text" id="card-holder"></div>
|
||||
<div class="text-xs text-red-500 mt-1 block" id="card-holder-error"></div>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-12 gap-4 mt-2">
|
||||
<label for="expiry-date" class="col-span-4">
|
||||
<span class="text-xs text-gray-900 uppercase">{{ ctrans('texts.expiry_date') }}</span>
|
||||
<div class="input w-full" type="text" id="expiry-date"></div>
|
||||
<div class="text-xs text-red-500 mt-1 block" id="expiry-date-error"></div>
|
||||
</label>
|
||||
|
||||
<label for="cvv" class="col-span-8">
|
||||
<span class="text-xs text-gray-900 uppercase">{{ ctrans('texts.cvv') }}</span>
|
||||
<div class="input w-full border" type="text" id="cvv"></div>
|
||||
<div class="text-xs text-red-500 mt-1 block" id="cvv-error"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endcomponent
|
||||
|
||||
@include('portal.ninja2020.gateways.includes.save_card')
|
||||
@include('portal.ninja2020.gateways.includes.pay_now')
|
||||
@endsection
|
||||
|
||||
@section('gateway_footer')
|
||||
<script src="https://js.mollie.com/v1/mollie.js"></script>
|
||||
<script src="{{ asset('js/clients/payments/mollie-credit-card.js') }}"></script>
|
||||
@endsection
|
@ -42,3 +42,4 @@ Route::get('stripe/signup/{token}', 'StripeConnectController@initialize')->name(
|
||||
Route::get('stripe/completed', 'StripeConnectController@completed')->name('stripe_connect.return');
|
||||
|
||||
Route::get('checkout/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', 'Gateways\Checkout3dsController@index')->name('checkout.3ds_redirect');
|
||||
Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', 'Gateways\Mollie3dsController@index')->name('mollie.3ds_redirect');
|
||||
|
133
tests/Browser/ClientPortal/Gateways/Mollie/CreditCardTest.php
Normal file
133
tests/Browser/ClientPortal/Gateways/Mollie/CreditCardTest.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace Tests\Browser\ClientPortal\Gateways\Mollie;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\Browser\Pages\ClientPortal\Login;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class CreditCardTest extends DuskTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
foreach (static::$browsers as $browser) {
|
||||
$browser->driver->manage()->deleteAllCookies();
|
||||
}
|
||||
|
||||
// $this->disableCompanyGateways();
|
||||
|
||||
// CompanyGateway::where('gateway_key', '3758e7f7c6f4cecf0f4f348b9a00f456')->restore();
|
||||
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visit(new Login())
|
||||
->auth();
|
||||
});
|
||||
}
|
||||
|
||||
public function testPayWithNewCreditCard()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.invoices.index')
|
||||
->click('@pay-now')
|
||||
->press('Pay Now')
|
||||
->clickLink('Credit Card')
|
||||
->pause(5000)
|
||||
->withinFrame('iframe[name=cardNumber-input]', function (Browser $browser) {
|
||||
$browser->type('#cardNumber', '4242424242424242');
|
||||
})
|
||||
->withinFrame('iframe[name=cardHolder-input]', function (Browser $browser) {
|
||||
$browser->type('#cardHolder', 'Invoice Ninja Test Suite');
|
||||
})
|
||||
->withinFrame('iframe[name=expiryDate-input]', function (Browser $browser) {
|
||||
$browser->type('#expiryDate', '12/29');
|
||||
})
|
||||
->withinFrame('iframe[name=verificationCode-input]', function (Browser $browser) {
|
||||
$browser->type('#verificationCode', '100');
|
||||
})
|
||||
->press('Pay Now')
|
||||
->waitForText('Details of the payment', 60);
|
||||
});
|
||||
}
|
||||
|
||||
public function testPayWithNewCreditCardAndSaveForFutureUse()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.invoices.index')
|
||||
->click('@pay-now')
|
||||
->press('Pay Now')
|
||||
->clickLink('Credit Card')
|
||||
->pause(5000)
|
||||
->withinFrame('iframe[name=cardNumber-input]', function (Browser $browser) {
|
||||
$browser->type('#cardNumber', '4242424242424242');
|
||||
})
|
||||
->withinFrame('iframe[name=cardHolder-input]', function (Browser $browser) {
|
||||
$browser->type('#cardHolder', 'Invoice Ninja Test Suite');
|
||||
})
|
||||
->withinFrame('iframe[name=expiryDate-input]', function (Browser $browser) {
|
||||
$browser->type('#expiryDate', '12/29');
|
||||
})
|
||||
->withinFrame('iframe[name=verificationCode-input]', function (Browser $browser) {
|
||||
$browser->type('#verificationCode', '100');
|
||||
})
|
||||
->radio('#proxy_is_default', true)
|
||||
->press('Pay Now')
|
||||
->waitForText('Details of the payment', 60)
|
||||
->visitRoute('client.payment_methods.index')
|
||||
->clickLink('View')
|
||||
->assertSee('4242');
|
||||
});
|
||||
}
|
||||
|
||||
public function testPayWithSavedCreditCard()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.invoices.index')
|
||||
->click('@pay-now')
|
||||
->press('Pay Now')
|
||||
->clickLink('Credit Card')
|
||||
->click('.toggle-payment-with-token')
|
||||
->press('Pay Now')
|
||||
->waitForText('Details of the payment', 60);
|
||||
});
|
||||
}
|
||||
|
||||
public function testAddingPaymentMethodShouldntBePossible()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.payment_methods.index')
|
||||
->press('Add Payment Method')
|
||||
->clickLink('Credit Card')
|
||||
->assertSee('This payment method can be can saved for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.');
|
||||
});
|
||||
}
|
||||
|
||||
public function testRemoveCreditCard()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.payment_methods.index')
|
||||
->clickLink('View')
|
||||
->press('Remove Payment Method')
|
||||
->waitForText('Confirmation')
|
||||
->click('@confirm-payment-removal')
|
||||
->assertSee('Payment method has been successfully removed.');
|
||||
});
|
||||
}
|
||||
}
|
32
tests/Unit/MollieAmountFormatTest.php
Normal file
32
tests/Unit/MollieAmountFormatTest.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?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://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MollieAmountFormatTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @covers \App\PaymentDrivers\MolliePaymentDriver::convertToMollieAmount()
|
||||
*/
|
||||
public function testFormatterIsWorkingCorrectly()
|
||||
{
|
||||
$this->assertEquals('1000.00', \number_format((float) 1000, 2, '.', ''));
|
||||
|
||||
$this->assertEquals('1000.00', \number_format((float) "1000", 2, '.', ''));
|
||||
|
||||
$this->assertEquals('1000.00', \number_format((float) "1000.00", 2, '.', ''));
|
||||
|
||||
$this->assertEquals('1000.00', \number_format((float) "1000.00000", 2, '.', ''));
|
||||
}
|
||||
}
|
4
webpack.mix.js
vendored
4
webpack.mix.js
vendored
@ -85,6 +85,10 @@ mix.js("resources/js/app.js", "public/js")
|
||||
.js(
|
||||
"resources/js/clients/payments/paytrace-credit-card.js",
|
||||
"public/js/clients/payments/paytrace-credit-card.js"
|
||||
)
|
||||
.js(
|
||||
"resources/js/clients/payments/mollie-credit-card.js",
|
||||
"public/js/clients/payments/mollie-credit-card.js"
|
||||
);
|
||||
|
||||
mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css');
|
||||
|
Loading…
Reference in New Issue
Block a user