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

Merge branch 'v5-develop' into v5-stable

This commit is contained in:
David Bomba 2021-08-14 15:11:16 +10:00
commit 240eac5523
156 changed files with 554122 additions and 356573 deletions

View File

@ -1,74 +0,0 @@
# Release notes
## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop)
## Added:
- Client portal: Show message when trying to approve non-approvable quotes
- Client portal: Remove "Approve" button from single quote page if quote is non-approvable
- Client portal: Hide "Pay now" buttons if no gateways are configured
- Client portal: "Download" and "Open in new tab" buttons on documents show page
- Client portal: Make "Invoice Ninja" link clickable in footer
## Fixed:
- Client portal: Showing message instead of blank page when trying to download zero quotes
- Client portal: Fixed bug with payment gateways after checking for required fields
## [v5.2.0-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.2.0-release)
## Added:
- Timezone Offset: Schedule emails based on timezone and time offsets.
- Force client country to system country if none is set.
- GMail Oauth via web
## Fixed:
- Add Cache-control: no-cache to prevent overaggressive caching of assets
- Improved labelling in the settings (client portal)
- Client portal: Multiple accounts access improvements (#5703)
- Client portal: "Credits" updates (#5734)
- Client portal: Make sidebar white color, in order to make logo displaying more simple. (#5753)
- Inject small delay into emails to allow all resources to be produced (ie PDFs) prior to sending
- Fixes for endless reminders not firing
## [v5.1.56-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.56-release)
## Fixed:
- Fix User created/updated/deleted Actvity display format
- Fix for Stripe autobill / token regression
## Added:
- Invoice / Quote / Credit created notifications
- Logout route - deletes all auth tokens
## [v5.1.54-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.54-release)
## Fixed:
- Fixes for e-mails, encoding & parsing invalid HTML
## [v5.1.50-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.50-release)
## Fixed:
- Refactor of e-mail templates
- Client portal: Invoices & recurring invoices are now sorted by date (by default)
## Added:
- Public notes of entities will now show in #footer section of designs (previously totals table).
## [v5.1.47-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.47-release)
### Added:
- Subscriptions are now going to show the frequency in the table (#5412)
- Subscriptions: During upgrade webhook request message will be shown for easier debugging (#5411)
- PDF: Custom fields now will be shared across invoices, quotes & credits (#5410)
- Client portal: Invoices are now sorted in the descending order (#5408)
- Payments: ACH notification after the initial bank account connecting process (#5405)
### Fixed:
- Fixes for counters where patterns without {$counter} could causes endless recursion.
- Fixes for surcharge tax displayed amount on PDF.
- Fixes for custom designs not rendering the custom template
- Fixes for missing bulk actions on Subscriptions
- Fixes CSS padding on the show page for recurring invoices (#5412)
- Fixes for rendering invalid HTML & parsing invalid XML (#5395)
### Removed:
- Removed one-time payments table (#5412)
## v5.1.43
### Fixed:
- Whitelabel regression.

View File

@ -1 +1 @@
5.2.17
5.2.18

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

@ -69,7 +69,7 @@ class Kernel extends ConsoleKernel
/* Run hosted specific jobs */
if (Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota)->daily()->withoutOverlapping();
$schedule->job(new AdjustEmailQuota)->dailyAt('23:00')->withoutOverlapping();
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping();

View File

@ -1,5 +1,4 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -10,6 +9,8 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
use App\Utils\Ninja;
/**
* Simple helper function that will log into "invoiceninja.log" file
* only when extended logging is enabled.
@ -32,7 +33,16 @@ function nlog($output, $context = []): void
$trace = debug_backtrace();
//nlog( debug_backtrace()[1]['function']);
// \Illuminate\Support\Facades\Log::channel('invoiceninja')->info(print_r($trace[1]['class'],1), []);
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
if(Ninja::isHosted()) {
try{
info($output);
}
catch(\Exception $e){
}
}
else
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
}

View File

@ -730,11 +730,11 @@ class BaseController extends Controller
$data = [];
if (Ninja::isSelfHost()) {
$data['report_errors'] = $account->report_errors;
} else {
$data['report_errors'] = true;
}
//pass report errors bool to front end
$data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
//pass referral code to front end
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$this->buildCache();

View File

@ -118,16 +118,22 @@ class PaymentMethodController extends Controller
*/
public function destroy(ClientGatewayToken $payment_method)
{
// $gateway = $this->getClientGateway();
$payment_method->gateway
->driver(auth()->user()->client)
->setPaymentMethod(request()->query('method'))
->detach($payment_method);
if($payment_method->gateway()->exists()){
$payment_method->gateway
->driver(auth()->user()->client)
->setPaymentMethod(request()->query('method'))
->detach($payment_method);
}
try {
event(new MethodDeleted($payment_method, auth('contact')->user()->company, Ninja::eventVars(auth('contact')->user()->id)));
$payment_method->delete();
} catch (Exception $e) {
nlog($e->getMessage());

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

@ -12,7 +12,7 @@
namespace App\Http\Controllers;
use App\DataMapper\Analytics\EmailBounce;
use App\DataMapper\Analytics\EmailSpam;
use App\DataMapper\Analytics\Mail\EmailSpam;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Models\CreditInvitation;

View File

@ -136,6 +136,7 @@ class PreviewController extends BaseController
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
$design = new Design(request()->design['name']);
@ -251,6 +252,7 @@ class PreviewController extends BaseController
'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'),
],
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
@ -362,6 +364,7 @@ class PreviewController extends BaseController
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $invoice->client->company->markdown_enabled,
];
$maker = new PdfMaker($state);

View File

@ -16,6 +16,7 @@ use App\Http\Requests\Setup\CheckDatabaseRequest;
use App\Http\Requests\Setup\CheckMailRequest;
use App\Http\Requests\Setup\StoreSetupRequest;
use App\Jobs\Account\CreateAccount;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Utils\CurlUtils;
@ -279,10 +280,7 @@ class SetupController extends Controller
public function update()
{
// if(Ninja::isHosted())
// return redirect('/');
// if( Ninja::isNinja() || !request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) )
if(!request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) )
return redirect('/');
@ -311,6 +309,8 @@ class SetupController extends Controller
$this->buildCache(true);
SchedulerCheck::dispatchNow();
return redirect('/');
}

View File

@ -354,6 +354,8 @@ class SubscriptionController extends BaseController
event(new SubscriptionWasUpdated($subscription, $subscription->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
nlog($subscription->id);
return $this->itemResponse($subscription);
}

View File

@ -71,11 +71,16 @@ class ContactKeyLogin
}
} elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if(empty($client_contact->email))
if(empty($client_contact->email)) {
$client_contact->email = Str::random(6) . "@example.com"; $client_contact->save();
}
auth()->guard('contact')->login($client_contact, true);
if ($request->query('next')) {
return redirect($request->query('next'));
}
return redirect()->to('client/dashboard');
}
} elseif ($request->has('client_hash') && config('ninja.db.multi_db_enabled')) {
@ -106,7 +111,6 @@ class ContactKeyLogin
}
}
return $next($request);
}
}

View File

@ -54,6 +54,7 @@ class StoreClientRequest extends Request
/* Ensure we have a client name, and that all emails are unique*/
//$rules['name'] = 'required|min:1';
$rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array';
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [
'nullable',

View File

@ -62,6 +62,7 @@ class UpdateClientRequest extends Request
$rules['number'] = Rule::unique('clients')->where('company_id', auth()->user()->company()->id)->ignore($this->client->id);
$rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array';
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [
'nullable',

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

@ -65,54 +65,6 @@ class PaymentWebhookRequest extends Request
return false;
}
/**
* Resolve possible payment in the request.
*
* @return null|\App\Models\Payment
*/
public function getPayment()
{
// For testing purposes we'll slow down the webhook processing by 2 seconds
// to make sure webhook request doesn't came before our processing.
//if (app()->environment() !== 'production') {
sleep(2);
//}
// Some gateways, like Checkout, we can dynamically pass payment hash,
// which we will resolve here and get payment information from it.
if ($this->getPaymentHash()) {
return $this->getPaymentHash()->payment;
}
// While for some gateways, we need to extract the payment source/reference from the webhook request.
// Gateways like this: Stripe
if ($this->has('api_version') && $this->has('type') && $this->has('data')) {
$src = $this->data['object']['id'];
return Payment::where('transaction_reference', $src)->firstOrFail();
}
// If none of previously done logics is correct, we'll just display
// not found page.
return false;
}
/**
* Resolve client from payment hash.
*
* @return null|\App\Models\Client|bool
*/
public function getClient()
{
$hash = $this->getPaymentHash();
if($hash) {
return Client::find($hash->data->client_id)->firstOrFail();
}
return false;
}
/**
* Resolve company from company_key parameter.
*

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

@ -71,7 +71,8 @@ class CreateAccount
$sp794f3f = new Account();
$sp794f3f->fill($this->request);
$sp794f3f->referral_code = Str::random(32);
if(array_key_exists('rc', $this->request))
$sp794f3f->referral_code = $this->request['rc'];
if (! $sp794f3f->key) {
$sp794f3f->key = Str::random(32);

View File

@ -166,6 +166,7 @@ class CreateEntityPdf implements ShouldQueue
'all_pages_header' => $this->entity->client->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'),
],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);

View File

@ -107,7 +107,10 @@ class EmailEntity implements ShouldQueue
/* Set DB */
MultiDB::setDB($this->company->db);
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($this->invitation->contact->preferredLocale());
$t->replace(Ninja::transformTranslations($this->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new TemplateEmail($this->email_entity_builder, $this->invitation->contact, $this->invitation);

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

@ -40,6 +40,7 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Mail;
use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Support\Facades\Cache;
/*Multi Mailer implemented*/
@ -71,9 +72,6 @@ class NinjaMailerJob implements ShouldQueue
public function handle()
{
if($this->preFlightChecksFail())
return;
/*Set the correct database*/
MultiDB::setDb($this->nmo->company->db);
@ -81,6 +79,9 @@ class NinjaMailerJob implements ShouldQueue
/* Serializing models from other jobs wipes the primary key */
$this->company = Company::where('company_key', $this->nmo->company->company_key)->first();
if($this->preFlightChecksFail())
return;
/* Set the email driver */
$this->setMailDriver();
@ -110,11 +111,10 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create(new EmailSuccess($this->nmo->company->company_key))
->batch();
/* Count the amount of emails sent across all the users accounts */
Cache::increment($this->company->account->key);
} catch (\Exception $e) {
// if($e instanceof GuzzleHttp\Exception\ClientException){
// }
nlog("error failed with {$e->getMessage()}");
@ -227,6 +227,15 @@ class NinjaMailerJob implements ShouldQueue
if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false)
return true;
/* GMail users are uncapped */
if(Ninja::isHosted() && $this->nmo->settings->email_sending_method == 'gmail')
return false;
/* On the hosted platform, if the user is over the email quotas, we do not send the email. */
if(Ninja::isHosted() && $this->company->account->emailQuotaExceeded())
return true;
return false;
}
@ -254,4 +263,5 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create($job_failure)
->batch();
}
}
}

View File

@ -13,26 +13,18 @@ namespace App\Jobs\Ninja;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
class AdjustEmailQuota implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
const FREE_PLAN_DAILY_QUOTA = 10;
const PRO_PLAN_DAILY_QUOTA = 50;
const ENTERPRISE_PLAN_DAILY_QUOTA = 200;
const FREE_PLAN_DAILY_CAP = 20;
const PRO_PLAN_DAILY_CAP = 100;
const ENTERPRISE_PLAN_DAILY_CAP = 300;
const DAILY_MULTIPLIER = 1.1;
/**
* Create a new job instance.
*
@ -50,22 +42,28 @@ class AdjustEmailQuota implements ShouldQueue
*/
public function handle()
{
if (! config('ninja.db.multi_db_enabled')) {
$this->adjust();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
if(!Ninja::isHosted())
return;
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->adjust();
$this->adjust();
}
}
}
public function adjust()
{
foreach (Account::cursor() as $account) {
//@TODO once we add in the two columns daily_emails_quota daily_emails_sent_
}
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

@ -70,7 +70,6 @@ class EmailPayment implements ShouldQueue
if ($this->company->is_disabled)
return true;
if ($this->contact->email) {
MultiDB::setDb($this->company->db);

View File

@ -56,6 +56,17 @@ class SendRecurring implements ShouldQueue
*/
public function handle() : void
{
//reset all contacts here
$this->recurring_invoice->client->contacts()->update(['send_email' => false]);
$this->recurring_invoice->invitations->each(function ($invitation){
$contact = $invitation->contact;
$contact->send_email = true;
$contact->save();
});
// Generate Standard Invoice
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
@ -67,7 +78,7 @@ class SendRecurring implements ShouldQueue
$invoice = $invoice->service()
->markSent()
->applyNumber()
->createInvitations()
->createInvitations() //need to only link invitations to those in the recurring invoice
->fillDefaults()
->save();

View File

@ -1398,7 +1398,7 @@ class Import implements ShouldQueue
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $this->user;
NinjaMailerJob::dispatch($nmo);
NinjaMailerJob::dispatch($nmo, true);
$modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34';

View File

@ -30,6 +30,8 @@ class ReminderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders, MakesDates;
public $tries = 1;
public function __construct()
{
}
@ -48,6 +50,7 @@ class ReminderJob implements ShouldQueue
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
}
@ -172,8 +175,10 @@ 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

@ -133,6 +133,15 @@ class StartMigration implements ShouldQueue
Mail::to($this->user->email, $this->user->name())->send(new MigrationFailed($e, $this->company, $e->getMessage()));
if(Ninja::isHosted()){
$migration_failed = new MigrationFailed($e, $this->company, $e->getMessage());
$migration_failed->is_system = true;
Mail::to('contact@invoiceninja.com', 'Failed Migration')->send($migration_failed);
}
if (app()->environment() !== 'production') {
info($e->getMessage());
}

View File

@ -37,6 +37,10 @@ class PaymentEmailEngine extends BaseEmailEngine
private $helpers;
private $payment_template_body;
private $payment_template_subject;
public function __construct($payment, $contact, $template_data = null)
{
$this->payment = $payment;
@ -55,20 +59,22 @@ class PaymentEmailEngine extends BaseEmailEngine
App::setLocale($this->contact->preferredLocale());
$t->replace(Ninja::transformTranslations($this->client->getMergedSettings()));
$this->resolvePaymentTemplate();
if (is_array($this->template_data) && array_key_exists('body', $this->template_data) && strlen($this->template_data['body']) > 0) {
$body_template = $this->template_data['body'];
} elseif (strlen($this->client->getSetting('email_template_payment')) > 0) {
$body_template = $this->client->getSetting('email_template_payment');
} elseif (strlen($this->client->getSetting($this->payment_template_body)) > 0) {
$body_template = $this->client->getSetting($this->payment_template_body);
} else {
$body_template = EmailTemplateDefaults::getDefaultTemplate('email_template_payment', $this->client->locale());
$body_template = EmailTemplateDefaults::getDefaultTemplate($this->payment_template_body, $this->client->locale());
}
if (is_array($this->template_data) && array_key_exists('subject', $this->template_data) && strlen($this->template_data['subject']) > 0) {
$subject_template = $this->template_data['subject'];
} elseif (strlen($this->client->getSetting('email_subject_payment')) > 0) {
$subject_template = $this->client->getSetting('email_subject_payment');
} elseif (strlen($this->client->getSetting($this->payment_template_subject)) > 0) {
$subject_template = $this->client->getSetting($this->payment_template_subject);
} else {
$subject_template = EmailTemplateDefaults::getDefaultTemplate('email_subject_payment', $this->client->locale());
$subject_template = EmailTemplateDefaults::getDefaultTemplate($this->payment_template_subject, $this->client->locale());
}
$this->setTemplate($this->client->getSetting('email_style'))
@ -96,6 +102,34 @@ class PaymentEmailEngine extends BaseEmailEngine
return $this;
}
/**
* Helper method to resolve which payment template
* to use. We need to check the invoice balances to
* determine if this is a partial payment, or full payment.
*
* @return $this
*/
private function resolvePaymentTemplate()
{
$partial = $this->payment->invoices->contains(function ($invoice){
return $invoice->balance > 0;
});
if($partial){
$this->payment_template_body = "email_template_payment_partial";
$this->payment_template_subject = "email_subject_payment_partial";
}
else {
$this->payment_template_body = "email_template_payment";
$this->payment_template_subject = "email_subject_payment";
}
return $this;
}
public function makePaymentVariables()
{
@ -194,12 +228,14 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->payment->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')];
$data['$paymentLink'] = &$data['$view_link'];
$data['$portalButton'] = &$data['$view_link'];
$data['$portalButton'] = ['value' => "<a href='{$this->payment->getPortalLink()}'>".ctrans('texts.login')."</a>", 'label' =>''];
$data['$portal_url'] = &$data['$portalButton'];
$data['$view_url'] = ['value' => $this->payment->getLink(), 'label' => ctrans('texts.view_payment')];
$data['$signature'] = ['value' => $this->settings->email_signature ?: '&nbsp;', 'label' => ''];
$data['$invoices'] = ['value' => $this->formatInvoices(), 'label' => ctrans('texts.invoices')];
$data['$invoice_references'] = ['value' => $this->formatInvoiceReferences(), 'label' => ctrans('texts.invoices')];
return $data;
}
@ -215,6 +251,25 @@ class PaymentEmailEngine extends BaseEmailEngine
return $invoice_list;
}
private function formatInvoiceReferences()
{
$invoice_list = '<br><br>';
foreach ($this->payment->invoices as $invoice) {
$invoice_list .= ctrans('texts.po_number'). " {$invoice->po_number} <br>";
$invoice_list .= ctrans('texts.invoice_number_short') . " {$invoice->number} <br>";
$invoice_list .= ctrans('texts.invoice_amount') ." ". Number::formatMoney($invoice->pivot->amount, $this->client) . "<br>";
$invoice_list .= ctrans('texts.invoice_balance') ." ". Number::formatMoney($invoice->fresh()->balance, $this->client) . "<br>";
$invoice_list .= "-----<br>";
}
return $invoice_list;
}
public function makeValues() :array
{
$data = [];

View File

@ -13,6 +13,8 @@ class MigrationFailed extends Mailable
public $company;
public $is_system = false;
/**
* Create a new message instance.
*
@ -38,6 +40,7 @@ class MigrationFailed extends Mailable
->view('email.migration.failed', [
'logo' => $this->company->present()->logo(),
'settings' => $this->company->settings,
'is_system' => $this->is_system,
]);
}
}

View File

@ -0,0 +1,61 @@
<?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\Mail\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class EmailQuotaExceeded extends Mailable
{
public $company;
public $settings;
public $logo;
public $title;
public $body;
public $whitelabel;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($company)
{
$this->company = $company;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$this->settings = $this->company->settings;
$this->logo = $this->company->present()->logo();
$this->title = ctrans('texts.email_quota_exceeded_subject');
$this->body = ctrans('texts.email_quota_exceeded_body', ['quota' => $this->company->account->getDailyEmailLimit()]);
$this->whitelabel = $this->company->account->isPaid();
$this->replyTo('contact@invoiceninja.com', 'Contact');
return $this->from(config('mail.from.address'), config('mail.from.name'))
->subject(ctrans('texts.email_quota_exceeded_subject'))
->view('email.admin.email_quota_exceeded');
}
}

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-{$db} :: {ucfirst($plan)} :: ".date('M jS, g:ia');
else
$subject = "{$priority}Self Hosted :: Customer Support - [{$plan}] ".date('M jS, g:ia');
$subject = "{$priority}Self Hosted :: {ucfirst($plan)} :: ".date('M jS, g:ia');
return $this->from(config('mail.from.address'), $user->present()->name())
->replyTo($user->email, $user->present()->name())

View File

@ -11,12 +11,16 @@
namespace App\Models;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Ninja\EmailQuotaExceeded;
use App\Models\Presenters\AccountPresenter;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use DateTime;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait;
class Account extends BaseModel
@ -24,6 +28,9 @@ class Account extends BaseModel
use PresentableTrait;
use MakesHash;
private $free_plan_email_quota = 250;
private $paid_plan_email_quota = 500;
/**
* @var string
*/
@ -127,6 +134,9 @@ class Account extends BaseModel
public function getPlan()
{
if(Carbon::parse($this->plan_expires)->lt(now()))
return '';
return $this->plan ?: '';
}
@ -341,4 +351,57 @@ class Account extends BaseModel
}
}
public function getDailyEmailLimit()
{
if($this->isPaid()){
$limit = $this->paid_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50;
}
else{
$limit = $this->free_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100;
}
return min($limit, 5000);
}
public function emailsSent()
{
if(is_null(Cache::get($this->key)))
return 0;
return Cache::get($this->key);
}
public function emailQuotaExceeded() :bool
{
if(is_null(Cache::get($this->key)))
return false;
try {
if(Cache::get($this->key) > $this->getDailyEmailLimit()) {
if(is_null(Cache::get("throttle_notified:{$this->key}"))) {
$nmo = new NinjaMailerObject;
$nmo->mailable = new EmailQuotaExceeded($this->companies()->first());
$nmo->company = $this->companies()->first();
$nmo->settings = $this->companies()->first()->settings;
$nmo->to_user = $this->companies()->first()->owner();
NinjaMailerJob::dispatch($nmo);
Cache::put("throttle_notified:{$this->key}", true, 60 * 24);
}
return true;
}
}
catch(\Exception $e){
\Sentry\captureMessage("I encountered an error with email quotas - defaulting to SEND");
}
return false;
}
}

View File

@ -50,6 +50,7 @@ class Company extends BaseModel
protected $presenter = CompanyPresenter::class;
protected $fillable = [
'markdown_enabled',
'calculate_expense_tax_by_amount',
'invoice_expense_documents',
'invoice_task_documents',

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

@ -16,6 +16,7 @@ use App\Services\Ledger\LedgerService;
use App\Services\Payment\PaymentService;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Payment\Refundable;
@ -28,7 +29,8 @@ class Payment extends BaseModel
use MakesDates;
use SoftDeletes;
use Refundable;
use Inviteable;
const STATUS_PENDING = 1;
const STATUS_CANCELLED = 2;
const STATUS_FAILED = 3;

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;
@ -113,4 +114,107 @@ class SystemLog extends Model
return $query;
}
public function getCategoryName()
{
switch ($this->category_id) {
case self::CATEGORY_GATEWAY_RESPONSE:
return "Gateway";
case self::CATEGORY_MAIL:
return "Mail";
case self::CATEGORY_WEBHOOK:
return "Webhook";
case self::CATEGORY_PDF:
return "PDF";
case self::CATEGORY_SECURITY:
return "Security";
default:
return 'undefined';
}
}
public function getEventName()
{
switch ($this->event_id) {
case self::EVENT_PAYMENT_RECONCILIATION_FAILURE:
return "Payment reco failure";
case self::EVENT_PAYMENT_RECONCILIATION_SUCCESS:
return "Payment reco success";
case self::EVENT_GATEWAY_SUCCESS:
return "Success";
case self::EVENT_GATEWAY_FAILURE:
return "Failure";
case self::EVENT_GATEWAY_ERROR:
return "Error";
case self::EVENT_MAIL_SEND:
return "Send";
case self::EVENT_MAIL_RETRY_QUEUE:
return "Retry";
case self::EVENT_MAIL_BOUNCED:
return "Bounced";
case self::EVENT_MAIL_SPAM_COMPLAINT:
return "Spam";
case self::EVENT_MAIL_DELIVERY:
return "Delivery";
case self::EVENT_WEBHOOK_RESPONSE:
return "Webhook Response";
case self::EVENT_PDF_RESPONSE:
return "Pdf Response";
case self::EVENT_AUTHENTICATION_FAILURE:
return "Auth Failure";
case self::EVENT_USER:
return "User";
default:
return 'undefined';
}
}
public function getTypeName()
{
switch ($this->type_id) {
case self::TYPE_QUOTA_EXCEEDED:
return "Quota Exceeded";
case self::TYPE_UPSTREAM_FAILURE:
return "Upstream Failure";
case self::TYPE_WEBHOOK_RESPONSE:
return "Webhook";
case self::TYPE_PDF_FAILURE:
return "Failure";
case self::TYPE_PDF_SUCCESS:
return "Success";
case self::TYPE_MODIFIED:
return "Modified";
case self::TYPE_DELETED:
return "Deleted";
case self::TYPE_LOGIN_SUCCESS:
return "Login Success";
case self::TYPE_LOGIN_FAILURE:
return "Login Failure";
case self::TYPE_PAYPAL:
return "PayPal";
case self::TYPE_STRIPE:
return "Stripe";
case self::TYPE_LEDGER:
return "Ledger";
case self::TYPE_FAILURE:
return "Failure";
case self::TYPE_CHECKOUT:
return "Checkout";
case self::TYPE_AUTHORIZE:
return "Auth.net";
case self::TYPE_CUSTOM:
return "Custom";
case self::TYPE_BRAINTREE:
return "Braintree";
case self::TYPE_WEPAY:
return "WePay";
case self::TYPE_PAYFAST:
return "Payfast";
default:
return 'undefined';
}
}
}

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

@ -332,7 +332,7 @@ class CheckoutComPaymentDriver extends BaseDriver
}
}
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)
public function processWebhookRequest(PaymentWebhookRequest $request)
{
return true;
}

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

@ -68,8 +68,15 @@ class ACH
$client_gateway_token = $this->storePaymentMethod($source, $request->input('method'), $customer);
$verification = route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::BANK_TRANSFER], false);
$mailer = new NinjaMailerObject();
$mailer->mailable = new ACHVerificationNotification(auth('contact')->user()->client->company, route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]));
$mailer->mailable = new ACHVerificationNotification(
auth('contact')->user()->client->company,
route('client.contact_login', ['contact_key' => auth('contact')->user()->contact_key, 'next' => $verification])
);
$mailer->company = auth('contact')->user()->client->company;
$mailer->settings = auth('contact')->user()->client->company->settings;
$mailer->to_user = auth('contact')->user();
@ -250,9 +257,10 @@ class ACH
{
try {
$payment_meta = new \stdClass;
$payment_meta->brand = (string)sprintf('%s (%s)', $method->bank_name, ctrans('texts.ach'));
$payment_meta->last4 = (string)$method->last4;
$payment_meta->brand = (string) \sprintf('%s (%s)', $method->bank_name, ctrans('texts.ach'));
$payment_meta->last4 = (string) $method->last4;
$payment_meta->type = GatewayType::BANK_TRANSFER;
$payment_meta->state = 'unauthorized';
$data = [
'payment_meta' => $payment_meta,

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

@ -195,8 +195,9 @@ class StripePaymentDriver extends BaseDriver
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
}
if($this->company_gateway->require_postal_code)
if($this->company_gateway->require_postal_code) {
$fields[] = ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'];
}
if ($this->company_gateway->require_shipping_address) {
$fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required'];
@ -387,21 +388,23 @@ class StripePaymentDriver extends BaseDriver
return $this->payment_method->processVerification($request, $payment_method);
}
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment)
public function processWebhookRequest(PaymentWebhookRequest $request)
{
if ($request->type == 'source.chargeable') {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
if ($request->type === 'charge.succeeded' || $request->type === 'source.chargeable') {
foreach ($request->data as $transaction) {
$payment = Payment::query()
->where('transaction_reference', $transaction['id'])
->where('company_id', $request->getCompany()->id)
->first();
if ($payment) {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
}
}
}
if ($request->type == 'charge.succeeded') {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
}
// charge.failed, charge.refunded
return response([], 200);
return response()->json([], 200);
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)

View File

@ -168,6 +168,8 @@ class WePayPaymentDriver extends BaseDriver
$input = $request->all();
$config = $this->company_gateway->getConfig();
$accountId = $this->company_gateway->getConfigField('accountId');
foreach (array_keys($input) as $key) {

View File

@ -153,6 +153,7 @@ class ActivityRepository extends BaseRepository
'all_pages_header' => $entity->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity->client->getSetting('all_pages_footer'),
],
'process_markdown' => $entity->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);

View File

@ -126,7 +126,7 @@ class ApplyPayment extends AbstractService
});
$this->invoice->service()->applyNumber()->save();
$this->invoice->service()->applyNumber()->workFlow()->save();
return $this->invoice;
}

View File

@ -0,0 +1,128 @@
<?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\Services\Invoice;
use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Jobs\Invoice\InvoiceWorkflowSettings;
use App\Jobs\Payment\EmailPayment;
use App\Libraries\Currency\Conversion\CurrencyApi;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Services\Client\ClientService;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Support\Carbon;
class ApplyPaymentAmount extends AbstractService
{
use GeneratesCounter;
private $invoice;
private $amount;
public function __construct(Invoice $invoice, $amount)
{
$this->invoice = $invoice;
$this->amount = $amount;
}
public function run()
{
if ($this->invoice->status_id == Invoice::STATUS_DRAFT) {
$this->invoice->service()->markSent();
}
/*Don't double pay*/
if ($this->invoice->statud_id == Invoice::STATUS_PAID) {
return $this->invoice;
}
if($this->amount == 0)
return $this->invoice;
/* Create Payment */
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
$payment->amount = $this->amount;
$payment->applied = min($this->amount, $this->invoice->balance);
$payment->number = $this->getNextPaymentNumber($this->invoice->client);
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->client_id = $this->invoice->client_id;
$payment->transaction_reference = ctrans('texts.manual_entry');
$payment->currency_id = $this->invoice->client->getSetting('currency_id');
$payment->is_manual = true;
/* Create a payment relationship to the invoice entity */
$payment->save();
$this->setExchangeRate($payment);
$payment->invoices()->attach($this->invoice->id, [
'amount' => $payment->amount,
]);
$this->invoice->next_send_date = null;
$this->invoice->service()
->setExchangeRate()
->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount)
->setCalculatedStatus()
->applyNumber()
->deletePdf()
->save();
if ($this->invoice->client->getSetting('client_manual_payment_notification'))
$payment->service()->sendEmail();
/* Update Invoice balance */
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
event(new InvoiceWasPaid($this->invoice, $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
$payment->ledger()
->updatePaymentBalance($payment->amount * -1);
$this->invoice
->client
->service()
->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount)
->save();
$this->invoice->service()->workFlow()->save();
return $this->invoice;
}
private function setExchangeRate(Payment $payment)
{
$client_currency = $payment->client->getSetting('currency_id');
$company_currency = $payment->client->company->settings->currency_id;
if ($company_currency != $client_currency) {
$exchange_rate = new CurrencyApi();
$payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date));
//$payment->exchange_currency_id = $client_currency; // 23/06/2021
$payment->exchange_currency_id = $company_currency;
$payment->save();
}
}
}

View File

@ -85,6 +85,7 @@ class GenerateDeliveryNote
'contact' => $this->contact,
], 'delivery_note'),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $this->invoice->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);

View File

@ -20,7 +20,9 @@ use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Task;
use App\Repositories\BaseRepository;
use App\Services\Client\ClientService;
use App\Services\Invoice\ApplyPaymentAmount;
use App\Services\Invoice\UpdateReminder;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
@ -50,6 +52,13 @@ class InvoiceService
return $this;
}
public function applyPaymentAmount($amount)
{
$this->invoice = (new ApplyPaymentAmount($this->invoice, $amount))->run();
return $this;
}
/**
* Applies the invoice number.
* @return $this InvoiceService object
@ -271,9 +280,8 @@ class InvoiceService
{
if ((int)$this->invoice->balance == 0) {
InvoiceWorkflowSettings::dispatchNow($this->invoice);
$this->setStatus(Invoice::STATUS_PAID);
$this->setStatus(Invoice::STATUS_PAID)->workFlow();
// InvoiceWorkflowSettings::dispatchNow($this->invoice);
}
if ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
@ -449,6 +457,18 @@ class InvoiceService
return $this;
}
public function workFlow()
{
if ($this->invoice->status_id == Invoice::STATUS_PAID && $this->invoice->client->getSetting('auto_archive_invoice')) {
/* Throws: Payment amount xxx does not match invoice totals. */
$base_repository = new BaseRepository();
$base_repository->archive($this->invoice);
}
return $this;
}
/**
* Saves the invoice.
* @return Invoice object

View File

@ -50,7 +50,8 @@ class MarkInvoiceDeleted extends AbstractService
private function adjustLedger()
{
$this->invoice->ledger()->updatePaymentBalance($this->adjustment_amount * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals
// $this->invoice->ledger()->updatePaymentBalance($this->adjustment_amount * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals
$this->invoice->ledger()->updatePaymentBalance($this->invoice->balance * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals
return $this;
}

View File

@ -95,7 +95,8 @@ class MarkPaid extends AbstractService
->updatePaidToDate($payment->amount)
->save();
InvoiceWorkflowSettings::dispatchNow($this->invoice);
$this->invoice->service()->workFlow()->save();
// InvoiceWorkflowSettings::dispatchNow($this->invoice);
return $this->invoice;
}

View File

@ -44,6 +44,10 @@ class TriggeredActions extends AbstractService
$this->invoice = $this->invoice->service()->markPaid()->save();
}
if ($this->request->has('amount_paid') && is_numeric($this->request->input('amount_paid')) ) {
$this->invoice = $this->invoice->service()->applyPaymentAmount($this->request->input('amount_paid'))->save();
}
if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') {
$this->sendEmail();
}
@ -52,6 +56,7 @@ class TriggeredActions extends AbstractService
$this->invoice = $this->invoice->service()->markSent()->save();
}
return $this->invoice;
}

View File

@ -232,7 +232,7 @@ class RefundPayment
if (isset($this->refund_data['invoices']) && count($this->refund_data['invoices']) > 0) {
foreach ($this->refund_data['invoices'] as $refunded_invoice) {
$invoice = Invoice::find($refunded_invoice['invoice_id']);
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
$invoice->service()->updateBalance($refunded_invoice['amount'])->save();
$invoice->ledger()->updateInvoiceBalance($refunded_invoice['amount'], "Refund of payment # {$this->payment->number}")->save();

View File

@ -85,8 +85,6 @@ class UpdateInvoicePayment
->deletePdf()
->save();
InvoiceWorkflowSettings::dispatchNow($invoice);
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
});

View File

@ -348,7 +348,7 @@ class Design extends BaseDesign
$items = $this->transformLineItems($this->entity->line_items, $type);
$this->processMarkdownOnLineItems($items);
$this->processNewLines($items);
if (count($items) == 0) {
return [];

View File

@ -331,7 +331,7 @@ document.addEventListener('DOMContentLoaded', function() {
return $converter->convertToHtml($markdown);
}
public function processMarkdownOnLineItems(array &$items)
public function processMarkdownOnLineItems(array &$items): void
{
foreach ($items as $key => $item) {
foreach ($item as $variable => $value) {
@ -341,4 +341,15 @@ document.addEventListener('DOMContentLoaded', function() {
$items[$key] = $item;
}
}
public function processNewLines(array &$items): void
{
foreach ($items as $key => $item) {
foreach ($item as $variable => $value) {
$item[$variable] = nl2br($value);
}
$items[$key] = $item;
}
}
}

View File

@ -92,11 +92,11 @@ trait PdfMakerUtilities
$contains_html = false;
if ($child['element'] !== 'script') {
$child['content'] = $this->commonmark->convertToHtml($child['content'] ?? '');
if (array_key_exists('process_markdown', $this->data) && $this->data['process_markdown']) {
$child['content'] = $this->commonmark->convertToHtml($child['content'] ?? '');
}
}
// $child['content'] = array_key_exists('content', $child) ? nl2br($child['content']) : '';
if (isset($child['content'])) {
if (isset($child['is_empty']) && $child['is_empty'] === true) {
continue;

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

@ -49,16 +49,15 @@ class RecurringService
public function start()
{
//make sure next_send_date is either now or in the future else return.
// if(Carbon::parse($this->recurring_entity->next_send_date)->lt(now()))
// return $this;
if ($this->recurring_entity->remaining_cycles == 0) {
return $this;
}
$this->createInvitations()->setStatus(RecurringInvoice::STATUS_ACTIVE);
// $this->createInvitations()->setStatus(RecurringInvoice::STATUS_ACTIVE);
$this->setStatus(RecurringInvoice::STATUS_ACTIVE);
return $this;
}

View File

@ -777,7 +777,15 @@ class SubscriptionService
*/
public function products()
{
return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->product_ids)))->get();
if(!$this->subscription->product_ids)
return collect();
$keys = $this->transformKeys(explode(",", $this->subscription->product_ids));
if(is_array($keys))
return Product::whereIn('id', $keys)->get();
else
return Product::where('id', $keys)->get();
}
/**
@ -788,7 +796,18 @@ class SubscriptionService
*/
public function recurring_products()
{
return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->recurring_product_ids)))->get();
if(!$this->subscription->recurring_product_ids)
return collect();
$keys = $this->transformKeys(explode(",", $this->subscription->recurring_product_ids));
if(is_array($keys)){
return Product::whereIn('id', $keys)->get();
}
else{
return Product::where('id', $keys)->get();
}
}
/**
@ -799,10 +818,10 @@ class SubscriptionService
public function getPlans()
{
return Subscription::query()
->where('company_id', $this->subscription->company_id)
->where('group_id', $this->subscription->group_id)
->where('id', '!=', $this->subscription->id)
->get();
->where('company_id', $this->subscription->company_id)
->where('group_id', $this->subscription->group_id)
->where('id', '!=', $this->subscription->id)
->get();
}
/**

View File

@ -80,6 +80,8 @@ class AccountTransformer extends EntityTransformer
'is_scheduler_running' => (bool) $account->is_scheduler_running,
'default_company_id' => (string) $this->encodePrimaryKey($account->default_company_id),
'disable_auto_update' => (bool) config('ninja.disable_auto_update'),
'emails_sent' => (int) $account->emailsSent(),
'email_quota' => (int) $account->getDailyEmailLimit(),
];
}

View File

@ -159,6 +159,7 @@ class CompanyTransformer extends EntityTransformer
'default_password_timeout' => (int) $company->default_password_timeout,
'invoice_task_datelog' => (bool) $company->invoice_task_datelog,
'show_task_end_date' => (bool) $company->show_task_end_date,
'markdown_enabled' => (bool) $company->markdown_enabled,
];
}

View File

@ -168,7 +168,12 @@ class HtmlEngine
$data['$invoice.discount'] = ['value' => Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.discount')];
$data['$discount'] = &$data['$invoice.discount'];
$data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')];
$data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
if($this->entity->uses_inclusive_taxes)
$data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
else
$data['$net_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
$data['$invoice.subtotal'] = &$data['$subtotal'];
if ($this->entity->partial > 0) {
@ -274,6 +279,7 @@ class HtmlEngine
$data['$client_address'] = ['value' => $this->client->present()->address() ?: '&nbsp;', 'label' => ctrans('texts.address')];
$data['$client.address'] = &$data['$client_address'];
$data['$client.postal_code'] = ['value' => $this->client->postal_code ?: '&nbsp;', 'label' => ctrans('texts.postal_code')];
$data['$client.city'] = ['value' => $this->client->city ?: '&nbsp;', 'label' => ctrans('texts.city')];
$data['$client.id_number'] = &$data['$id_number'];
$data['$client.vat_number'] = &$data['$vat_number'];
$data['$client.website'] = &$data['$website'];

View File

@ -193,6 +193,7 @@ class Phantom
'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'),
],
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);

View File

@ -358,6 +358,24 @@ trait MakesInvoiceValues
':MONTH' => Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F'),
':YEAR' => now()->year,
':QUARTER' => 'Q' . now()->quarter,
':WEEK_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subDays(7)->translatedFormat($this->client->date_format()),
ctrans('texts.to'),
Carbon::now()->translatedFormat($this->client->date_format())
),
':WEEK_AHEAD' => \sprintf(
'%s %s %s',
Carbon::now()->addDays(7)->translatedFormat($this->client->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(14)->translatedFormat($this->client->date_format())
),
':WEEK' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($this->client->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(7)->translatedFormat($this->client->date_format())
),
],
'raw' => [
':MONTH' => now()->month,

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

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.2.17',
'app_tag' => '5.2.17',
'app_version' => '5.2.18',
'app_tag' => '5.2.18',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -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

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddMarkdownEnabledColumnToCompaniesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Blueprint $table) {
$table->boolean('markdown_enabled')->default(1);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@ -0,0 +1,55 @@
<?php
use App\Models\Language;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddMoreLanguages extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Language::unguard();
$language = Language::find(30);
if(!$language){
Language::create(['id' => 30, 'name' => 'Arabic', 'locale' => 'ar']);
}
$language = Language::find(31);
if(!$language){
Language::create(['id' => 31, 'name' => 'Persian', 'locale' => 'fa']);
}
$language = Language::find(32);
if(!$language){
Language::create(['id' => 32, 'name' => 'Latvian', 'locale' => 'lv_LV']);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -52,6 +52,10 @@ class LanguageSeeder extends Seeder
['id' => 26, 'name' => 'Thai', 'locale' => 'th'],
['id' => 27, 'name' => 'Macedonian', 'locale' => 'mk_MK'],
['id' => 28, 'name' => 'Chinese - Taiwan', 'locale' => 'zh_TW'],
['id' => 29, 'name' => 'Russian (Russia)', 'locale' => 'ru_RU'],
['id' => 30, 'name' => 'Arabic', 'locale' => 'ar'],
['id' => 31, 'name' => 'Persian', 'locale' => 'fa'],
['id' => 32, 'name' => 'Latvian', 'locale' => 'lv_LV'],
];
foreach ($languages as $language) {

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]);

12193
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@
"card-validator": "^6.2.0",
"cross-env": "^7.0.3",
"jsignature": "^2.1.3",
"json-formatter-js": "^2.3.4",
"laravel-mix": "^5.0.9",
"linkify-urls": "^3.1.1",
"lodash": "^4.17.21",

File diff suppressed because it is too large Load Diff

187606
public/css/admin.css vendored

File diff suppressed because one or more lines are too long

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -23,16 +23,16 @@ const RESOURCES = {
"assets/assets/images/logo.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/AssetManifest.json": "753bba1dee0531d5fad970b5ce1d296d",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/NOTICES": "687b68d41e137cfbdee105c0b9be3e9d",
"assets/NOTICES": "f44f710ef9af0b68d977d458631873e1",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
"version.json": "3f9e03374a3e78d2cab3afd8723d0993",
"version.json": "46d4015fc9abcefe5371cafcf2084173",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"main.dart.js": "02238eed5a325865e74b8a1e7afd03a6",
"main.dart.js": "7bcab146a5f6ade3cd027cc9e429f732",
"/": "d389ab59423a76b2aaaa683ed382c78e"
};
@ -50,7 +50,7 @@ self.addEventListener("install", (event) => {
return event.waitUntil(
caches.open(TEMP).then((cache) => {
return cache.addAll(
CORE.map((value) => new Request(value + '?revision=' + RESOURCES[value], {'cache': 'reload'})));
CORE.map((value) => new Request(value, {'cache': 'reload'})));
})
);
});

167
public/js/admin.js vendored
View File

@ -1 +1,166 @@
(()=>{var r,e={847:()=>{},113:()=>{}},o={};function n(r){var t=o[r];if(void 0!==t)return t.exports;var a=o[r]={exports:{}};return e[r](a,a.exports,n),a.exports}n.m=e,r=[],n.O=(e,o,t,a)=>{if(!o){var v=1/0;for(p=0;p<r.length;p++){for(var[o,t,a]=r[p],l=!0,i=0;i<o.length;i++)(!1&a||v>=a)&&Object.keys(n.O).every((r=>n.O[r](o[i])))?o.splice(i--,1):(l=!1,a<v&&(v=a));l&&(r.splice(p--,1),e=t())}return e}a=a||0;for(var p=r.length;p>0&&r[p-1][2]>a;p--)r[p]=r[p-1];r[p]=[o,t,a]},n.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{var r={467:0,703:0};n.O.j=e=>0===r[e];var e=(e,o)=>{var t,a,[v,l,i]=o,p=0;for(t in l)n.o(l,t)&&(n.m[t]=l[t]);if(i)var f=i(n);for(e&&e(o);p<v.length;p++)a=v[p],n.o(r,a)&&r[a]&&r[a][0](),r[v[p]]=0;return n.O(f)},o=self.webpackChunk=self.webpackChunk||[];o.forEach(e.bind(null,0)),o.push=e.bind(null,o.push.bind(o))})(),n.O(void 0,[703],(()=>n(847)));var t=n.O(void 0,[703],(()=>n(113)));t=n.O(t)})();
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./Resources/assets/js/app.js":
/*!************************************!*\
!*** ./Resources/assets/js/app.js ***!
\************************************/
/***/ (() => {
/***/ }),
/***/ "./Resources/assets/css/app.css":
/*!**************************************!*\
!*** ./Resources/assets/css/app.css ***!
\**************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
// extracted by mini-css-extract-plugin
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ (() => {
/******/ var deferred = [];
/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
/******/ if(chunkIds) {
/******/ priority = priority || 0;
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
/******/ deferred[i] = [chunkIds, fn, priority];
/******/ return;
/******/ }
/******/ var notFulfilled = Infinity;
/******/ for (var i = 0; i < deferred.length; i++) {
/******/ var [chunkIds, fn, priority] = deferred[i];
/******/ var fulfilled = true;
/******/ for (var j = 0; j < chunkIds.length; j++) {
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
/******/ chunkIds.splice(j--, 1);
/******/ } else {
/******/ fulfilled = false;
/******/ if(priority < notFulfilled) notFulfilled = priority;
/******/ }
/******/ }
/******/ if(fulfilled) {
/******/ deferred.splice(i--, 1)
/******/ result = fn();
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "/js/admin": 0,
/******/ "css/admin": 0
/******/ };
/******/
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkIds[i]] = 0;
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
/******/ __webpack_require__.O(undefined, ["css/admin"], () => (__webpack_require__("./Resources/assets/js/app.js")))
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["css/admin"], () => (__webpack_require__("./Resources/assets/css/app.css")))
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
/******/
/******/ })()
;

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
*/

File diff suppressed because one or more lines are too long

194
public/js/jsformatter.css vendored Normal file
View File

@ -0,0 +1,194 @@
.json-formatter-row {
font-family: monospace;
}
.json-formatter-row,
.json-formatter-row a,
.json-formatter-row a:hover {
color: black;
text-decoration: none;
}
.json-formatter-row .json-formatter-row {
margin-left: 1rem;
}
.json-formatter-row .json-formatter-children.json-formatter-empty {
opacity: 0.5;
margin-left: 1rem;
}
.json-formatter-row .json-formatter-children.json-formatter-empty:after {
display: none;
}
.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {
content: "No properties";
}
.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {
content: "[]";
}
.json-formatter-row .json-formatter-string,
.json-formatter-row .json-formatter-stringifiable {
color: green;
white-space: pre;
word-wrap: break-word;
}
.json-formatter-row .json-formatter-number {
color: blue;
}
.json-formatter-row .json-formatter-boolean {
color: red;
}
.json-formatter-row .json-formatter-null {
color: #855A00;
}
.json-formatter-row .json-formatter-undefined {
color: #ca0b69;
}
.json-formatter-row .json-formatter-function {
color: #FF20ED;
}
.json-formatter-row .json-formatter-date {
background-color: rgba(0, 0, 0, 0.05);
}
.json-formatter-row .json-formatter-url {
text-decoration: underline;
color: blue;
cursor: pointer;
}
.json-formatter-row .json-formatter-bracket {
color: blue;
}
.json-formatter-row .json-formatter-key {
color: #00008B;
padding-right: 0.2rem;
}
.json-formatter-row .json-formatter-toggler-link {
cursor: pointer;
}
.json-formatter-row .json-formatter-toggler {
line-height: 1.2rem;
font-size: 0.7rem;
vertical-align: middle;
opacity: 0.6;
cursor: pointer;
padding-right: 0.2rem;
}
.json-formatter-row .json-formatter-toggler:after {
display: inline-block;
transition: transform 100ms ease-in;
content: "►";
}
.json-formatter-row > a > .json-formatter-preview-text {
opacity: 0;
transition: opacity 0.15s ease-in;
font-style: italic;
}
.json-formatter-row:hover > a > .json-formatter-preview-text {
opacity: 0.6;
}
.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {
transform: rotate(90deg);
}
.json-formatter-row.json-formatter-open > .json-formatter-children:after {
display: inline-block;
}
.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {
display: none;
}
.json-formatter-row.json-formatter-open.json-formatter-empty:after {
display: block;
}
.json-formatter-dark.json-formatter-row {
font-family: monospace;
}
.json-formatter-dark.json-formatter-row,
.json-formatter-dark.json-formatter-row a,
.json-formatter-dark.json-formatter-row a:hover {
color: white;
text-decoration: none;
}
.json-formatter-dark.json-formatter-row .json-formatter-row {
margin-left: 1rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty {
opacity: 0.5;
margin-left: 1rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty:after {
display: none;
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-object:after {
content: "No properties";
}
.json-formatter-dark.json-formatter-row .json-formatter-children.json-formatter-empty.json-formatter-array:after {
content: "[]";
}
.json-formatter-dark.json-formatter-row .json-formatter-string,
.json-formatter-dark.json-formatter-row .json-formatter-stringifiable {
color: #31F031;
white-space: pre;
word-wrap: break-word;
}
.json-formatter-dark.json-formatter-row .json-formatter-number {
color: #66C2FF;
}
.json-formatter-dark.json-formatter-row .json-formatter-boolean {
color: #EC4242;
}
.json-formatter-dark.json-formatter-row .json-formatter-null {
color: #EEC97D;
}
.json-formatter-dark.json-formatter-row .json-formatter-undefined {
color: #ef8fbe;
}
.json-formatter-dark.json-formatter-row .json-formatter-function {
color: #FD48CB;
}
.json-formatter-dark.json-formatter-row .json-formatter-date {
background-color: rgba(255, 255, 255, 0.05);
}
.json-formatter-dark.json-formatter-row .json-formatter-url {
text-decoration: underline;
color: #027BFF;
cursor: pointer;
}
.json-formatter-dark.json-formatter-row .json-formatter-bracket {
color: #9494FF;
}
.json-formatter-dark.json-formatter-row .json-formatter-key {
color: #23A0DB;
padding-right: 0.2rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-toggler-link {
cursor: pointer;
}
.json-formatter-dark.json-formatter-row .json-formatter-toggler {
line-height: 1.2rem;
font-size: 0.7rem;
vertical-align: middle;
opacity: 0.6;
cursor: pointer;
padding-right: 0.2rem;
}
.json-formatter-dark.json-formatter-row .json-formatter-toggler:after {
display: inline-block;
transition: transform 100ms ease-in;
content: "►";
}
.json-formatter-dark.json-formatter-row > a > .json-formatter-preview-text {
opacity: 0;
transition: opacity 0.15s ease-in;
font-style: italic;
}
.json-formatter-dark.json-formatter-row:hover > a > .json-formatter-preview-text {
opacity: 0.6;
}
.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-toggler-link .json-formatter-toggler:after {
transform: rotate(90deg);
}
.json-formatter-dark.json-formatter-row.json-formatter-open > .json-formatter-children:after {
display: inline-block;
}
.json-formatter-dark.json-formatter-row.json-formatter-open > a > .json-formatter-preview-text {
display: none;
}
.json-formatter-dark.json-formatter-row.json-formatter-open.json-formatter-empty:after {
display: block;
}

344177
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More