1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 13:12:50 +01:00

Merge pull request #6445 from turbo124/v5-develop

Mollie Payment Gateway
This commit is contained in:
David Bomba 2021-08-12 14:39:31 +10:00 committed by GitHub
commit 5357731a46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 2928 additions and 592 deletions

View File

@ -742,6 +742,51 @@ class CreateSingleAccount extends Command
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.paytrace.decrypted') && ($this->gateway == 'all' || $this->gateway == 'paytrace')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'bbd736b3254b0aabed6ad7fda1298c88';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$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();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
}
private function createRecurringInvoice($client)

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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')) {

View 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;
}
}

View File

@ -134,7 +134,7 @@ class Request extends FormRequest
}
}
if (isset($input['contacts'])) {
if (isset($input['contacts']) && is_array($input['contacts'])) {
foreach ($input['contacts'] as $key => $contact) {
if (array_key_exists('id', $contact) && is_numeric($contact['id'])) {
unset($input['contacts'][$key]['id']);

View File

@ -573,7 +573,7 @@ class CSVImport implements ShouldQueue {
}
private function findUser( $user_hash ) {
$user = User::where( 'company_id', $this->company->id )
$user = User::where( 'account_id', $this->company->account->id )
->where( \DB::raw( 'CONCAT_WS(" ", first_name, last_name)' ), 'like', '%' . $user_hash . '%' )
->first();

View File

@ -60,6 +60,7 @@ class AdjustEmailQuota implements ShouldQueue
{
Account::query()->cursor()->each(function ($account){
nlog("resetting email quota for {$account->key}");
Cache::forget($account->key);
Cache::forget("throttle_notified:{$account->key}");
});

View File

@ -175,8 +175,9 @@ class ReminderJob implements ShouldQueue
$invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$invoice = $invoice->calc()->getInvoice();
$invoice = $invoice->calc()->getInvoice()->save();
$invoice->service()->deletePdf();
nlog("adjusting client balance and invoice balance by ". ($invoice->balance - $temp_invoice_balance));
$invoice->client->service()->updateBalance($invoice->balance - $temp_invoice_balance)->save();
$invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}");

View File

@ -60,11 +60,12 @@ class SupportMessageSent extends Mailable
$company = auth()->user()->company();
$user = auth()->user();
$db = str_replace("db-ninja-", "", $company->db);
if(Ninja::isHosted())
$subject = "{$priority}Hosted-{$company->db} :: Customer Support - {$plan} ".date('M jS, g:ia');
$subject = "{$priority}Hosted-{$company->db} :: {$plan} :: ".date('M jS, g:ia');
else
$subject = "{$priority}Self Hosted :: Customer Support - [{$plan}] ".date('M jS, g:ia');
$subject = "{$priority}Self Hosted :: {$plan} :: ".date('M jS, g:ia');
return $this->from(config('mail.from.address'), $user->present()->name())
->replyTo($user->email, $user->present()->name())

View File

@ -69,16 +69,20 @@ class CompanyGateway extends BaseModel
// const TYPE_CUSTOM = 306;
// const TYPE_BRAINTREE = 307;
// const TYPE_WEPAY = 309;
// const TYPE_PAYFAST = 310;
// const TYPE_PAYTRACE = 311;
public $gateway_consts = [
'38f2c48af60c7dd69e04248cbb24c36e' => 300,
'd14dd26a37cecc30fdd65700bfb55b23' => 301,
'd14dd26a47cecc30fdd65700bfb67b34' => 301,
'3758e7f7c6f4cecf0f4f348b9a00f456' => 304,
'3b6621f970ab18887c4f6dca78d3f8bb' => 305,
'54faab2ab6e3223dbe848b1686490baa' => 306,
'd14dd26a47cecc30fdd65700bfb67b34' => 301,
'8fdeed552015b3c7b44ed6c8ebd9e992' => 309,
'f7ec488676d310683fb51802d076d713' => 307,
'8fdeed552015b3c7b44ed6c8ebd9e992' => 309,
'd6814fc83f45d2935e7777071e629ef9' => 310,
'bbd736b3254b0aabed6ad7fda1298c88' => 311,
];
protected $touches = [];
@ -118,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();

View File

@ -81,6 +81,9 @@ class Gateway extends StaticModel
case 1:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]];//Authorize.net
break;
case 1:
return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => false]];//Payfast
break;
case 15:
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal
break;
@ -95,16 +98,23 @@ class Gateway extends StaticModel
case 39:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout
break;
case 46:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Paytrace
case 49:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true]]; //WePay
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;

View File

@ -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;
}
}

View File

@ -68,8 +68,9 @@ class SystemLog extends Model
const TYPE_BRAINTREE = 307;
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;

View File

@ -431,6 +431,61 @@ class BaseDriver extends AbstractPaymentDriver
return false;
}
/*Generic Global unsuccessful transaction method when the client is present*/
public function processUnsuccessfulTransaction($response, $client_present = true)
{
$error = $response['error'];
$error_code = $response['error_code'];
$this->unWindGatewayFees($this->payment_hash);
PaymentFailureMailer::dispatch($this->client, $error, $this->client->company, $this->payment_hash->data->amount_with_fee);
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer( (new ClientPaymentFailureObject($this->client, $error, $this->client->company, $this->payment_hash))->build() );
$nmo->company = $this->client->company;
$nmo->settings = $this->client->company->settings;
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get();
$invoices->each(function ($invoice){
$invoice->service()->deletePdf();
});
$invoices->first()->invitations->each(function ($invitation) use ($nmo){
if ($invitation->contact->send_email && $invitation->contact->email) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);
}
});
$message = [
'server_response' => $response,
'data' => $this->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
$this::SYSTEM_LOG_TYPE,
$this->client,
$this->client->company,
);
if($client_present)
throw new PaymentFailed($error, 500);
}
public function checkRequirements()
{
if ($this->company_gateway->require_billing_address) {

View 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');
}
}

View 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, '.', '');
}
}

View File

@ -0,0 +1,258 @@
<?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\PayTrace;
use App\Exceptions\PaymentFailed;
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\PayFastPaymentDriver;
use App\PaymentDrivers\PaytracePaymentDriver;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class CreditCard
{
use MakesHash;
public $paytrace;
public function __construct(PaytracePaymentDriver $paytrace)
{
$this->paytrace = $paytrace;
}
public function authorizeView($data)
{
$data['client_key'] = $this->paytrace->getAuthToken();
$data['gateway'] = $this->paytrace;
return render('gateways.paytrace.authorize', $data);
}
// +"success": true
// +"response_code": 160
// +"status_message": "The customer profile for PLS5U60OoLUfQXzcmtJYNefPA0gTthzT/11 was successfully created."
// +"customer_id": "PLS5U60OoLUfQXzcmtJYNefPA0gTthzT"
//if(!$response->success)
//handle failure
public function authorizeResponse($request)
{
$data = $request->all();
$response = $this->createCustomer($data);
return redirect()->route('client.payment_methods.index');
}
// "_token" => "Vl1xHflBYQt9YFSaNCPTJKlY5x3rwcFE9kvkw71I"
// "company_gateway_id" => "1"
// "HPF_Token" => "e484a92c-90ed-4468-ac4d-da66824c75de"
// "enc_key" => "zqz6HMHCXALWdX5hyBqrIbSwU7TBZ0FTjjLB3Cp0FQY="
// "amount" => "Amount"
// "q" => "/client/payment_methods"
// "method" => "1"
// ]
// "customer_id":"customer789",
// "hpf_token":"e369847e-3027-4174-9161-fa0d4e98d318",
// "enc_key":"lI785yOBMet4Rt9o4NLXEyV84WBU3tdStExcsfoaOoo=",
// "integrator_id":"xxxxxxxxxx",
// "billing_address":{
// "name":"Mark Smith",
// "street_address":"8320 E. West St.",
// "city":"Spokane",
// "state":"WA",
// "zip":"85284"
// }
private function createCustomer($data)
{
$post_data = [
'customer_id' => Str::random(32),
'hpf_token' => $data['HPF_Token'],
'enc_key' => $data['enc_key'],
'integrator_id' => $this->paytrace->company_gateway->getConfigField('integratorId'),
'billing_address' => $this->buildBillingAddress(),
];
$response = $this->paytrace->gatewayRequest('/v1/customer/pt_protect_create', $post_data);
$cgt = [];
$cgt['token'] = $response->customer_id;
$cgt['payment_method_id'] = GatewayType::CREDIT_CARD;
$profile = $this->getCustomerProfile($response->customer_id);
$payment_meta = new \stdClass;
$payment_meta->exp_month = $profile->credit_card->expiration_month;
$payment_meta->exp_year = $profile->credit_card->expiration_year;
$payment_meta->brand = 'CC';
$payment_meta->last4 = $profile->credit_card->masked_number;
$payment_meta->type = GatewayType::CREDIT_CARD;
$cgt['payment_meta'] = $payment_meta;
$token = $this->paytrace->storeGatewayToken($cgt, []);
return $response;
}
private function getCustomerProfile($customer_id)
{
$profile = $this->paytrace->gatewayRequest('/v1/customer/export', [
'integrator_id' => $this->paytrace->company_gateway->getConfigField('integratorId'),
'customer_id' => $customer_id,
// 'include_bin' => true,
]);
return $profile->customers[0];
}
private function buildBillingAddress()
{
return [
'name' => $this->paytrace->client->present()->name(),
'street_address' => $this->paytrace->client->address1,
'city' => $this->paytrace->client->city,
'state' => $this->paytrace->client->state,
'zip' => $this->paytrace->client->postal_code
];
}
public function paymentView($data)
{
$data['client_key'] = $this->paytrace->getAuthToken();
$data['gateway'] = $this->paytrace;
return render('gateways.paytrace.pay', $data);
}
public function paymentResponse(Request $request)
{
$response_array = $request->all();
if($request->token){
$token = ClientGatewayToken::find($this->decodePrimaryKey($request->token));
return $this->processTokenPayment($token->token, $request);
}
if ($request->has('store_card') && $request->input('store_card') === true) {
$response = $this->createCustomer($request->all());
return $this->processTokenPayment($response->customer_id, $request);
}
//process a regular charge here:
$data = [
'hpf_token' => $response_array['HPF_Token'],
'enc_key' => $response_array['enc_key'],
'integrator_id' => $this->paytrace->company_gateway->getConfigField('integratorId'),
'billing_address' => $this->buildBillingAddress(),
'amount' => $request->input('amount_with_fee'),
'invoice_id' => $this->harvestInvoiceId(),
];
$response = $this->paytrace->gatewayRequest('/v1/transactions/sale/pt_protect', $data);
if($response->success)
return $this->processSuccessfulPayment($response);
return $this->processUnsuccessfulPayment($response);
}
public function processTokenPayment($token, $request)
{
$data = [
'customer_id' => $token,
'integrator_id' => $this->paytrace->company_gateway->getConfigField('integratorId'),
'amount' => $request->input('amount_with_fee'),
];
$response = $this->paytrace->gatewayRequest('/v1/transactions/sale/by_customer', $data);
if($response->success){
$this->paytrace->logSuccessfulGatewayResponse(['response' => $response, 'data' => $this->paytrace->payment_hash], SystemLog::TYPE_PAYTRACE);
return $this->processSuccessfulPayment($response);
}
return $this->processUnsuccessfulPayment($response);
}
private function harvestInvoiceId()
{
$_invoice = collect($this->paytrace->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
if($invoice)
return ctrans('texts.invoice_number') . "# " . $invoice->number;
return ctrans('texts.invoice_number') . "####";
}
private function processSuccessfulPayment($response)
{
$amount = array_sum(array_column($this->paytrace->payment_hash->invoices(), 'amount')) + $this->paytrace->payment_hash->fee_total;
$payment_record = [];
$payment_record['amount'] = $amount;
$payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER;
$payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD;
$payment_record['transaction_reference'] = $response->transaction_id;
$payment = $this->paytrace->createPayment($payment_record, Payment::STATUS_COMPLETED);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
}
private function processUnsuccessfulPayment($response)
{
$error = $response->status_message;
if(property_exists($response, 'approval_message') && $response->approval_message)
$error .= " - {$response->approval_message}";
$error_code = property_exists($response, 'approval_message') ? $response->approval_message : 'Undefined code';
$data = [
'response' => $response,
'error' => $error,
'error_code' => $error_code,
];
return $this->paytrace->processUnsuccessfulTransaction($data);
}
}

View File

@ -0,0 +1,234 @@
<?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\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\PayTrace\CreditCard;
use App\Utils\CurlUtils;
use App\Utils\Traits\MakesHash;
class PaytracePaymentDriver extends BaseDriver
{
use MakesHash;
public $refundable = true;
public $token_billing = true;
public $can_authorise_credit_card = true;
public $gateway;
public $payment_method;
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class, //maps GatewayType => Implementation class
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYTRACE; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model
public function init()
{
return $this; /* This is where you boot the gateway with your auth credentials*/
}
/* Returns an array of gateway types for the payment gateway */
public function gatewayTypes(): array
{
$types = [];
$types[] = GatewayType::CREDIT_CARD;
return $types;
}
/* Sets the payment method initialized */
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); //this is your custom implementation from here
}
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request); //this is your custom implementation from here
}
public function processPaymentView(array $data)
{
return $this->payment_method->paymentView($data); //this is your custom implementation from here
}
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request); //this is your custom implementation from here
}
public function refund(Payment $payment, $amount, $return_client_response = false)
{
// $cgt = ClientGatewayToken::where('company_gateway_id', $payment->company_gateway_id)
// ->where('gateway_type_id', $payment->gateway_type_id)
// ->first();
$data = [
'amount' => $amount,
//'customer_id' => $cgt->token,
'transaction_id' => $payment->transaction_reference,
'integrator_id' => '959195xd1CuC'
];
$response = $this->gatewayRequest('/v1/transactions/refund/for_transaction', $data);
if($response && $response->success)
{
SystemLogger::dispatch(['server_response' => $response, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_PAYTRACE, $this->client, $this->client->company);
return [
'transaction_reference' => $response->transaction_id,
'transaction_response' => json_encode($response),
'success' => true,
'description' => $response->status_message,
'code' => $response->response_code,
];
}
SystemLogger::dispatch(['server_response' => $response, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_PAYTRACE, $this->client, $this->client->company);
return [
'transaction_reference' => null,
'transaction_response' => json_encode($response),
'success' => false,
'description' => $response->status_message,
'code' => 422,
];
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$data = [
'customer_id' => $cgt->token,
'integrator_id' => $this->company_gateway->getConfigField('integratorId'),
'amount' => $amount,
];
$response = $this->gatewayRequest('/v1/transactions/sale/by_customer', $data);
if($response && $response->success)
{
$data = [
'gateway_type_id' => $cgt->gateway_type_id,
'payment_type' => PaymentType::CREDIT_CARD_OTHER,
'transaction_reference' => $response->transaction_id,
'amount' => $amount,
];
$payment = $this->createPayment($data);
$payment->meta = $cgt->meta;
$payment->save();
$payment_hash->payment_id = $payment->id;
$payment_hash->save();
return $payment;
}
$error = $response->status_message;
if(property_exists($response, 'approval_message') && $response->approval_message)
$error .= " - {$response->approval_message}";
$data = [
'response' => $response,
'error' => $error,
'error_code' => 500,
];
$this->processUnsuccessfulTransaction($data, false);
}
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)
{
}
/*Helpers*/
private function generateAuthHeaders()
{
$url = 'https://api.paytrace.com/oauth/token';
$data = [
'grant_type' => 'password',
'username' => $this->company_gateway->getConfigField('username'),
'password' => $this->company_gateway->getConfigField('password')
];
$response = CurlUtils::post($url, $data, $headers = false);
$auth_data = json_decode($response);
$headers = [];
$headers[] = 'Content-type: application/json';
$headers[] = 'Authorization: Bearer '.$auth_data->access_token;
return $headers;
}
public function getAuthToken()
{
$headers = $this->generateAuthHeaders();
$response = CurlUtils::post('https://api.paytrace.com/v1/payment_fields/token/create', [], $headers);
$response = json_decode($response);
if($response)
return $response->clientKey;
return false;
}
public function gatewayRequest($uri, $data, $headers = false)
{
$base_url = "https://api.paytrace.com{$uri}";
$headers = $this->generateAuthHeaders();
$response = CurlUtils::post($base_url, json_encode($data), $headers);
$response = json_decode($response);
if($response)
return $response;
return false;
}
}

View File

@ -74,7 +74,7 @@ class Charge
'confirm' => true,
'description' => $description,
];
nlog($data);
$response = $this->stripe->createPaymentIntent($data, $this->stripe->stripe_connect_auth);
// $response = $local_stripe->paymentIntents->create($data);

View File

@ -15,10 +15,14 @@ namespace App\Services\Quote;
use App\Factory\CloneQuoteToInvoiceFactory;
use App\Models\Quote;
use App\Repositories\InvoiceRepository;
use App\Utils\Traits\MakesHash;
class ConvertQuote
{
use MakesHash;
private $client;
private $invoice_repo;
public function __construct($client)
@ -34,7 +38,7 @@ class ConvertQuote
public function run($quote)
{
$invoice = CloneQuoteToInvoiceFactory::create($quote, $quote->user_id);
$invoice->design_id = $this->client->getSetting('invoice_design_id');
$invoice->design_id = $this->decodePrimaryKey($this->client->getSetting('invoice_design_id'));
$invoice = $this->invoice_repo->save([], $invoice);
$invoice->fresh();

View File

@ -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
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": "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",

View File

@ -46,7 +46,7 @@ return [
'prefix' => '',
'prefix_indexes' => true,
'strict' => env('DB_STRICT', false),
// 'engine' => 'InnoDB ROW_FORMAT=DYNAMIC',
'engine' => 'InnoDB',
],
'sqlite' => [

View File

@ -84,6 +84,12 @@ return [
'test_email' => env('TEST_EMAIL', 'test@example.com'),
'wepay' => env('WEPAY_KEYS', ''),
'braintree' => env('BRAINTREE_KEYS', ''),
'paytrace' => [
'username' => env('PAYTRACE_U', ''),
'password' => env('PAYTRACE_P',''),
'decrypted' => env('PAYTRACE_KEYS', ''),
],
'mollie' => env('MOLLIE_KEYS', ''),
],
'contact' => [
'email' => env('MAIL_FROM_ADDRESS'),

View File

@ -0,0 +1,39 @@
<?php
use App\Models\Gateway;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ActivatePaytracePaymentDriver extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if($paytrace = Gateway::find(46))
{
$fields = json_decode($paytrace->fields);
$fields->integratorId = "";
$paytrace->fields = json_encode($fields);
$paytrace->provider = 'Paytrace';
$paytrace->visible = true;
$paytrace->save();
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -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()
{
//
}
}

View File

@ -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}'],
@ -70,7 +70,7 @@ class PaymentLibrariesSeeder extends Seeder
['id' => 43, 'name' => 'Fasapay', 'provider' => 'Fasapay', 'key' => '1b2cef0e8c800204a29f33953aaf3360', 'fields' => ''],
['id' => 44, 'name' => 'Komoju', 'provider' => 'Komoju', 'key' => '7ea2d40ecb1eb69ef8c3d03e5019028a', 'fields' => '{"apiKey":"","accountId":"","paymentMethod":"credit_card","testMode":false,"locale":"en"}'],
['id' => 45, 'name' => 'Paysafecard', 'provider' => 'Paysafecard', 'key' => '70ab90cd6c5c1ab13208b3cef51c0894', 'fields' => '{"username":"","password":"","testMode":false}'],
['id' => 46, 'name' => 'Paytrace', 'provider' => 'Paytrace_CreditCard', 'key' => 'bbd736b3254b0aabed6ad7fda1298c88', 'fields' => '{"username":"","password":"","testMode":false,"endpoint":"https:\/\/paytrace.com\/api\/default.pay"}'],
['id' => 46, 'name' => 'Paytrace', 'provider' => 'Paytrace', 'key' => 'bbd736b3254b0aabed6ad7fda1298c88', 'fields' => '{"username":"","password":"","integratorId":"","testMode":false,"endpoint":"https:\/\/paytrace.com\/api\/default.pay"}'],
['id' => 47, 'name' => 'Secure Trading', 'provider' => 'SecureTrading', 'key' => '231cb401487b9f15babe04b1ac4f7a27', 'fields' => '{"siteReference":"","username":"","password":"","applyThreeDSecure":false,"accountType":"ECOM"}'],
['id' => 48, 'name' => 'SecPay', 'provider' => 'SecPay', 'key' => 'bad8699d581d9fa040e59c0bb721a76c', 'fields' => '{"mid":"","vpnPswd":"","remotePswd":"","usageType":"","confirmEmail":"","testStatus":"true","mailCustomer":"true","additionalOptions":""}'],
['id' => 49, 'name' => 'WePay', 'provider' => 'WePay', 'is_offsite' => false, 'sort_order' => 3, 'key' => '8fdeed552015b3c7b44ed6c8ebd9e992', 'fields' => '{"accountId":"","accessToken":"","type":"goods","testMode":false,"feePayer":"payee"}'],
@ -96,7 +96,7 @@ class PaymentLibrariesSeeder extends Seeder
Gateway::query()->update(['visible' => 0]);
Gateway::whereIn('id', [1,15,20,39,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

File diff suppressed because it is too large Load Diff

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View 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()}});

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://www.elastic.co/licensing/elastic-license
*/

View File

@ -0,0 +1,2 @@
/*! For license information please see paytrace-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=21)}({"0Swb":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;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.clientKey=null===(t=document.querySelector("meta[name=paytrace-client-key]"))||void 0===t?void 0:t.content}var t,r,o;return t=e,(r=[{key:"creditCardStyles",get:function(){return{font_color:"#111827",border_color:"rgba(210,214,220,1)",label_color:"#111827",label_size:"12pt",background_color:"white",border_style:"solid",font_size:"15pt",height:"30px",width:"100%"}}},{key:"codeStyles",get:function(){return{font_color:"#111827",border_color:"rgba(210,214,220,1)",label_color:"#111827",label_size:"12pt",background_color:"white",border_style:"solid",font_size:"15pt",height:"30px",width:"300px"}}},{key:"expStyles",get:function(){return{font_color:"#111827",border_color:"rgba(210,214,220,1)",label_color:"#111827",label_size:"12pt",background_color:"white",border_style:"solid",font_size:"15pt",height:"30px",width:"85px",type:"dropdown"}}},{key:"updatePayTraceLabels",value:function(){window.PTPayment.getControl("securityCode").label.text(document.querySelector("meta[name=ctrans-cvv]").content),window.PTPayment.getControl("creditCard").label.text(document.querySelector("meta[name=ctrans-card_number]").content),window.PTPayment.getControl("expiration").label.text(document.querySelector("meta[name=ctrans-expires]").content)}},{key:"setupPayTrace",value:function(){return window.PTPayment.setup({styles:{code:this.codeStyles,cc:this.creditCardStyles,exp:this.expStyles},authorization:{clientKey:this.clientKey}})}},{key:"handlePaymentWithCreditCard",value:function(e){var t=this;e.target.parentElement.disabled=!0,document.getElementById("errors").hidden=!0,window.PTPayment.validate((function(n){if(n.length>=1){var r=document.getElementById("errors");return r.textContent=n[0].description,r.hidden=!1,e.target.parentElement.disabled=!1}t.ptInstance.process().then((function(e){document.getElementById("HPF_Token").value=e.message.hpf_token,document.getElementById("enc_key").value=e.message.enc_key;var t=document.querySelector('input[name="token-billing-checkbox"]:checked');t&&(document.querySelector('input[name="store_card"]').value=t.value),document.getElementById("server_response").submit()})).catch((function(e){document.getElementById("errors").textContent=JSON.stringify(e),document.getElementById("errors").hidden=!1,console.log(e)}))}))}},{key:"handlePaymentWithToken",value:function(e){e.target.parentElement.disabled=!0,document.getElementById("server_response").submit()}},{key:"handle",value:function(){var e=this;this.setupPayTrace().then((function(t){var n;e.ptInstance=t,e.updatePayTraceLabels(),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(e){return e.addEventListener("click",(function(e){document.getElementById("paytrace--credit-card-container").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token}))})),null===(n=document.getElementById("toggle-payment-with-credit-card"))||void 0===n||n.addEventListener("click",(function(e){document.getElementById("paytrace--credit-card-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(t){return""===document.querySelector("input[name=token]").value?e.handlePaymentWithCreditCard(t):e.handlePaymentWithToken(t)}))}))}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()},21:function(e,t,n){e.exports=n("0Swb")}});

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://www.elastic.co/licensing/elastic-license
*/

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=f4c07fdabcbe50c9f4be",
"/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,8 @@
"/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",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=a30464874dee84678344",
@ -21,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"
}

View 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();

View File

@ -0,0 +1,186 @@
/**
* 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 PayTraceCreditCard {
constructor() {
this.clientKey = document.querySelector(
'meta[name=paytrace-client-key]'
)?.content;
}
get creditCardStyles() {
return {
font_color: '#111827',
border_color: 'rgba(210,214,220,1)',
label_color: '#111827',
label_size: '12pt',
background_color: 'white',
border_style: 'solid',
font_size: '15pt',
height: '30px',
width: '100%',
};
}
get codeStyles() {
return {
font_color: '#111827',
border_color: 'rgba(210,214,220,1)',
label_color: '#111827',
label_size: '12pt',
background_color: 'white',
border_style: 'solid',
font_size: '15pt',
height: '30px',
width: '300px',
};
}
get expStyles() {
return {
font_color: '#111827',
border_color: 'rgba(210,214,220,1)',
label_color: '#111827',
label_size: '12pt',
background_color: 'white',
border_style: 'solid',
font_size: '15pt',
height: '30px',
width: '85px',
type: 'dropdown',
};
}
updatePayTraceLabels() {
window.PTPayment.getControl('securityCode').label.text(
document.querySelector('meta[name=ctrans-cvv]').content
);
window.PTPayment.getControl('creditCard').label.text(
document.querySelector('meta[name=ctrans-card_number]').content
);
window.PTPayment.getControl('expiration').label.text(
document.querySelector('meta[name=ctrans-expires]').content
);
}
setupPayTrace() {
return window.PTPayment.setup({
styles: {
code: this.codeStyles,
cc: this.creditCardStyles,
exp: this.expStyles,
},
authorization: {
clientKey: this.clientKey,
},
});
}
handlePaymentWithCreditCard(event) {
event.target.parentElement.disabled = true;
document.getElementById('errors').hidden = true;
window.PTPayment.validate((errors) => {
if (errors.length >= 1) {
let errorsContainer = document.getElementById('errors');
errorsContainer.textContent = errors[0].description;
errorsContainer.hidden = false;
return (event.target.parentElement.disabled = false);
}
this.ptInstance
.process()
.then((response) => {
document.getElementById('HPF_Token').value =
response.message.hpf_token;
document.getElementById('enc_key').value =
response.message.enc_key;
let tokenBillingCheckbox = document.querySelector(
'input[name="token-billing-checkbox"]:checked'
);
if (tokenBillingCheckbox) {
document.querySelector(
'input[name="store_card"]'
).value = tokenBillingCheckbox.value;
}
document.getElementById('server_response').submit();
})
.catch((error) => {
document.getElementById(
'errors'
).textContent = JSON.stringify(error);
document.getElementById('errors').hidden = false;
console.log(error);
});
});
}
handlePaymentWithToken(event) {
event.target.parentElement.disabled = true;
document.getElementById('server_response').submit();
}
handle() {
this.setupPayTrace().then((instance) => {
this.ptInstance = instance;
this.updatePayTraceLabels();
Array.from(
document.getElementsByClassName('toggle-payment-with-token')
).forEach((element) =>
element.addEventListener('click', (element) => {
document
.getElementById('paytrace--credit-card-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('paytrace--credit-card-container')
.classList.remove('hidden');
document.getElementById(
'save-card--container'
).style.display = 'grid';
document.querySelector('input[name=token]').value = '';
});
document
.getElementById('pay-now')
.addEventListener('click', (e) => {
if (
document.querySelector('input[name=token]').value === ''
) {
return this.handlePaymentWithCreditCard(e);
}
return this.handlePaymentWithToken(e);
});
});
}
}
new PayTraceCreditCard().handle();

View File

@ -4295,6 +4295,7 @@ $LANG = array(
'lang_Arabic' => 'Arabic',
'lang_Persian' => 'Persian',
'lang_Latvian' => 'Latvian',
'expiry_date' => 'Expiry date',
);
return $LANG;

View File

@ -16,12 +16,12 @@
value="true"/>
<span class="ml-1 cursor-pointer">{{ ctrans('texts.yes') }}</span>
</label>
<labecoml>
<label>
<input type="radio" class="form-radio cursor-pointer" name="token-billing-checkbox"
id="proxy_is_default"
value="false" checked />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.no') }}</span>
</labecoml>
</label>
</dd>
</div>
@else

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,37 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title'
=> ctrans('texts.payment_type_credit_card')])
@section('gateway_head')
<meta name="paytrace-client-key" content="{{ $client_key }}">
<meta name="ctrans-cvv" content="{{ ctrans('texts.cvv') }}">
<meta name="ctrans-card_number" content="{{ ctrans('texts.card_number') }}">
<meta name="ctrans-expires" content="{{ ctrans('texts.expires') }}">
@endsection
@section('gateway_content')
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}"
method="post" id="server_response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="txt" id=HPF_Token name= HPF_Token hidden>
<input type="txt" id=enc_key name= enc_key hidden>
<input type="text" name="token" hidden>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element-single')
<div class="w-screen items-center" id="paytrace--credit-card-container">
<div id="pt_hpf_form"></div>
</div>
@endcomponent
@component('portal.ninja2020.gateways.includes.pay_now')
{{ ctrans('texts.add_payment_method') }}
@endcomponent
@endsection
@section('gateway_footer')
<script src="https://protect.paytrace.com/js/protect.min.js"></script>
<script src="{{ asset('js/clients/payments/paytrace-credit-card.js') }}"></script>
@endsection

View File

@ -0,0 +1,64 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title'
=> ctrans('texts.payment_type_credit_card')])
@section('gateway_head')
<meta name="paytrace-client-key" content="{{ $client_key }}">
<meta name="ctrans-cvv" content="{{ ctrans('texts.cvv') }}">
<meta name="ctrans-card_number" content="{{ ctrans('texts.card_number') }}">
<meta name="ctrans-expires" content="{{ ctrans('texts.expires') }}">
@endsection
@section('gateway_content')
<form action="{{ route('client.payments.response') }}" method="post" id="server_response">
@csrf
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="payment_method_id" value="1">
<input type="hidden" name="token" id="token" />
<input type="hidden" name="store_card" id="store_card" />
<input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}" />
<input type="txt" id="HPF_Token" name="HPF_Token" hidden>
<input type="txt" id="enc_key" name="enc_key" hidden>
</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->hashed_id }}" 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
@endisset
<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
@include('portal.ninja2020.gateways.includes.save_card')
@component('portal.ninja2020.components.general.card-element-single')
<div class="w-screen items-center" id="paytrace--credit-card-container">
<div id="pt_hpf_form"></div>
</div>
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@section('gateway_footer')
<script src='https://protect.paytrace.com/js/protect.min.js'></script>
<script src="{{ asset('js/clients/payments/paytrace-credit-card.js') }}"></script>
@endsection

View File

@ -0,0 +1,29 @@
<div>
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.previous')</span>
</li>
@else
<li class="page-item">
<button type="button" class="page-link" wire:click="previousPage" wire:loading.attr="disabled" rel="prev">@lang('pagination.previous')</button>
</li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<button type="button" class="page-link" wire:click="nextPage" wire:loading.attr="disabled" rel="next">@lang('pagination.next')</button>
</li>
@else
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.next')</span>
</li>
@endif
</ul>
</nav>
@endif
</div>

View File

@ -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');

View 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.');
});
}
}

View File

@ -0,0 +1,62 @@
<?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\PayTrace;
use App\Models\CompanyGateway;
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', 'bbd736b3254b0aabed6ad7fda1298c88')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPayingWithNewCreditCard()
{
$this->markTestSkipped('Credit card not supported.');
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Credit Card')
->withinFrame('iframe', function (Browser $browser) {
$browser
->type('CC', '4012000098765439')
->select('EXP_MM', '12')
->select('EXP_YY', '30')
->type('SEC', '999');
})
->press('Pay Now')
->waitForText('Details of the payment', 60);
});
}
}

View 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, '.', ''));
}
}

8
webpack.mix.js vendored
View File

@ -81,6 +81,14 @@ mix.js("resources/js/app.js", "public/js")
.js(
"resources/js/clients/payment_methods/wepay-bank-account.js",
"public/js/clients/payment_methods/wepay-bank-account.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');