Merge remote-tracking branch 'origin/v5-develop' into v5-develop

# Conflicts:
#	resources/lang/en/texts.php
This commit is contained in:
Lars Kusch 2021-10-08 14:49:02 +02:00
commit 0d1596339e
76 changed files with 532712 additions and 530494 deletions

View File

@ -1 +1 @@
5.3.19
5.3.22

View File

@ -23,6 +23,7 @@ use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\SendFailedEmails;
use App\Jobs\Util\UpdateExchangeRates;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Utils\Ninja;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -69,6 +70,10 @@ class Kernel extends ConsoleKernel
$schedule->job(new SchedulerCheck)->daily()->withoutOverlapping();
$schedule->call(function () {
Account::whereNotNull('id')->update(['is_scheduler_running' => true]);
})->everyFiveMinutes()->withoutOverlapping();
/* Run hosted specific jobs */
if (Ninja::isHosted()) {

View File

@ -28,9 +28,9 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['invoice_id']);
unset($quote_array['id']);
unset($quote_array['invitations']);
unset($quote_array['terms']);
// unset($quote_array['public_notes']);
unset($quote_array['footer']);
//unset($quote_array['terms']);
//unset($quote_array['public_notes']);
//unset($quote_array['footer']);
unset($quote_array['design_id']);
foreach ($quote_array as $key => $value) {

View File

@ -67,9 +67,10 @@ class ContactResetPasswordController extends Controller
$account_id = $request->get('account_id');
$account = Account::find($account_id);
$db = $account->companies->first()->db;
$company = $account->companies->first();
return $this->render('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email, 'account' => $account, 'db' => $db]
['token' => $token, 'email' => $request->email, 'account' => $account, 'db' => $db, 'company' => $company]
);
}

View File

@ -19,7 +19,10 @@ use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Services\ClientPortal\InstantPayment;
use App\Utils\CurlUtils;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
@ -65,11 +68,17 @@ class InvitationController extends Controller
private function genericRouter(string $entity, string $invitation_key)
{
if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
return response()->json(['message' => 'Invalid resource request']);
$key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
$invitation = $entity_obj::whereRaw('BINARY `key`= ?', [$invitation_key])
$invitation = $entity_obj::where('key', $invitation_key)
->whereHas($entity, function ($query) {
$query->where('is_deleted',0);
})
->with('contact.client')
->firstOrFail();
@ -133,6 +142,9 @@ class InvitationController extends Controller
private function returnRawPdf(string $entity, string $invitation_key)
{
if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
return response()->json(['message' => 'Invalid resource request']);
$key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
@ -181,4 +193,41 @@ class InvitationController extends Controller
return redirect()->route('client.payments.show', $payment->hashed_id);
}
public function payInvoice(Request $request, string $invitation_key)
{
$invitation = InvoiceInvitation::where('key', $invitation_key)
->with('contact.client')
->firstOrFail();
auth()->guard('contact')->login($invitation->contact, true);
$invoice = $invitation->invoice;
if($invoice->partial > 0)
$amount = round($invoice->partial, (int)$invoice->client->currency()->precision);
else
$amount = round($invoice->balance, (int)$invoice->client->currency()->precision);
$gateways = $invitation->contact->client->service()->getPaymentMethods($amount);
if(is_array($gateways))
{
$data = [
'company_gateway_id' => $gateways[0]['company_gateway_id'],
'payment_method_id' => $gateways[0]['gateway_type_id'],
'payable_invoices' => [
['invoice_id' => $invitation->invoice->hashed_id, 'amount' => $amount],
],
'signature' => false
];
$request->replace($data);
return (new InstantPayment($request))->run();
}
abort(404, "Invoice not found");
}
}

View File

@ -23,6 +23,7 @@ use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\Services\ClientPortal\InstantPayment;
use App\Services\Subscription\SubscriptionService;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
@ -79,235 +80,7 @@ class PaymentController extends Controller
*/
public function process(Request $request)
{
$is_credit_payment = false;
$tokens = [];
if ($request->input('company_gateway_id') == CompanyGateway::GATEWAY_CREDIT) {
$is_credit_payment = true;
}
$gateway = CompanyGateway::find($request->input('company_gateway_id'));
/**
* find invoices
*
* ['invoice_id' => xxx, 'amount' => 22.00]
*/
$payable_invoices = collect($request->payable_invoices);
$invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function($invoice){
$invoice->service()->removeUnpaidGatewayFees()->save();
});
/* pop non payable invoice from the $payable_invoices array */
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable();
});
/*return early if no invoices*/
if ($payable_invoices->count() == 0) {
return redirect()
->route('client.invoices.index')
->with(['message' => 'No payable invoices selected.']);
}
$settings = auth()->user()->client->getMergedSettings();
// nlog($settings);
/* This loop checks for under / over payments and returns the user if a check fails */
foreach ($payable_invoices as $payable_invoice) {
/*Match the payable invoice to the Model Invoice*/
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
/*
* Check if company supports over & under payments.
* Determine the payable amount and the max payable. ie either partial or invoice balance
*/
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
/*If we don't allow under/over payments force the payable amount - prevents inspect element adjustments in JS*/
if ($settings->client_portal_allow_under_payment == false && $settings->client_portal_allow_over_payment == false) {
$payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
}
if (!$settings->client_portal_allow_under_payment && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($settings->client_portal_allow_under_payment) {
if ($invoice_balance < $settings->client_portal_under_payment_minimum && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($invoice_balance < $settings->client_portal_under_payment_minimum) {
// Skip the under payment rule.
}
if ($invoice_balance >= $settings->client_portal_under_payment_minimum && $payable_amount < $settings->client_portal_under_payment_minimum) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]));
}
}
/* If we don't allow over payments and the amount exceeds the balance */
if (!$settings->client_portal_allow_over_payment && $payable_amount > $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.over_payments_disabled'));
}
}
/*Iterate through invoices and add gateway fees and other payment metadata*/
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice_collection = collect();
foreach ($payable_invoices as $payable_invoice) {
// nlog($payable_invoice);
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']);
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue($invoice->balance, auth()->user()->client->currency()->precision);
$payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
$payable_invoice['invoice_number'] = $invoice->number;
if (isset($invoice->po_number)) {
$additional_info = $invoice->po_number;
} elseif (isset($invoice->public_notes)) {
$additional_info = $invoice->public_notes;
} else {
$additional_info = $invoice->date;
}
$payable_invoice['additional_info'] = $additional_info;
$payable_invoice_collection->push($payable_invoice);
}
//});
if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) {
$invoices->each(function ($invoice) use ($request) {
InjectSignature::dispatch($invoice, $request->signature);
});
}
$payable_invoices = $payable_invoice_collection;
$payment_method_id = $request->input('payment_method_id');
$invoice_totals = $payable_invoices->sum('amount');
$first_invoice = $invoices->first();
$credit_totals = $first_invoice->client->getSetting('use_credits_payment') == 'always' ? $first_invoice->client->service()->getCreditBalance() : 0;
$starting_invoice_amount = $first_invoice->balance;
if ($gateway) {
$first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save();
}
/**
* Gateway fee is calculated
* by adding it as a line item, and then subtract
* the starting and finishing amounts of the invoice.
*/
$fee_totals = $first_invoice->balance - $starting_invoice_amount;
if ($gateway) {
$tokens = auth()->user()->client->gateway_tokens()
->whereCompanyGatewayId($gateway->id)
->whereGatewayTypeId($payment_method_id)
->get();
}
if(!$is_credit_payment){
$credit_totals = 0;
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
if ($request->query('hash')) {
$hash_data['billing_context'] = Cache::get($request->query('hash'));
}
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(32);
$payment_hash->data = $hash_data;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
if($is_credit_payment){
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
}
else{
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [
'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => $amount_with_fee,
];
$data = [
'payment_hash' => $payment_hash->hash,
'total' => $totals,
'invoices' => $payable_invoices,
'tokens' => $tokens,
'payment_method_id' => $payment_method_id,
'amount_with_fee' => $invoice_totals + $fee_totals,
];
if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($request, $data);
}
try {
return $gateway
->driver(auth()->user()->client)
->setPaymentMethod($payment_method_id)
->setPaymentHash($payment_hash)
->checkRequirements()
->processPaymentView($data);
} catch (\Exception $e) {
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_FAILURE,
auth('contact')->user()->client,
auth('contact')->user()->client->company
);
throw new PaymentFailed($e->getMessage());
}
return (new InstantPayment($request))->run();
}
public function response(PaymentResponseRequest $request)

View File

@ -127,7 +127,7 @@ class EmailController extends BaseController
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
$entity_obj->service()->markSent()->save();

View File

@ -234,21 +234,24 @@ class MigrationController extends BaseController
public function startMigration(Request $request)
{
// v4 Laravel 6
// $companies = [];
// foreach($request->all() as $input){
// if($input instanceof UploadedFile)
// nlog('is file');
// else
// $companies[] = json_decode($input);
// }
nlog("Starting Migration");
$companies = json_decode($request->companies,1);
if($request->companies){
//handle Laravel 5.5 UniHTTP
$companies = json_decode($request->companies,1);
}
else {
//handle Laravel 6 Guzzle
$companies = [];
foreach($request->all() as $input){
if($input instanceof UploadedFile)
nlog('is file');
else
$companies[] = json_decode($input,1);
}
}
if (app()->environment() === 'local') {
nlog($request->all());
@ -267,22 +270,11 @@ class MigrationController extends BaseController
foreach($companies as $company)
{
if(!is_array($company))
continue;
$company = (array)$company;
// v4 Laravel 6
// $input = $request->all();
// foreach ($input as $company) {
// if($company instanceof UploadedFile)
// continue;
// else
// $company = json_decode($company,1);
// if (!$company || !is_int($company['company_index'] || !$request->file($company['company_index'])->isValid())) {
// continue;
// }
$user = auth()->user();
$company_count = $user->account->companies()->count();

View File

@ -216,7 +216,7 @@ class PreviewController extends BaseController
if(!$request->has('entity_id'))
$entity_obj->service()->fillDefaults()->save();
$entity_obj->load('client');
$entity_obj->load('client.contacts','company');
App::forgetInstance('translator');
$t = app('translator');
@ -345,7 +345,7 @@ class PreviewController extends BaseController
$invoice->setRelation('invitations', $invitation);
$invoice->setRelation('client', $client);
$invoice->setRelation('company', auth()->user()->company());
$invoice->load('client');
$invoice->load('client.company');
// nlog(print_r($invoice->toArray(),1));

View File

@ -41,8 +41,9 @@ class CreditsTable extends Component
->where('status_id', '<>', Credit::STATUS_DRAFT)
->where('is_deleted', 0)
->where(function ($query){
$query->whereDate('due_date', '<=', now())
->orWhereNull('due_date');
$query->whereDate('due_date', '>=', now())
->orWhereNull('due_date')
->orWhere('due_date', '=', '');
})
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()

View File

@ -52,7 +52,7 @@ class QueryLogging
$timeEnd = microtime(true);
$time = $timeEnd - $timeStart;
// nlog("Query count = {$count}");
// info("Query count = {$count}");
if($count > 175){
nlog("Query count = {$count}");

View File

@ -42,6 +42,9 @@ class SetInviteDb
$entity = $request->route('entity');
}
if($entity == "pay")
$entity = "invoice";
if ($request->getSchemeAndHttpHost() && config('ninja.db.multi_db_enabled') && ! MultiDB::findAndSetDbByInvitation($entity, $request->route('invitation_key'))) {
if (request()->json) {
return response()->json($error, 403);

View File

@ -1331,6 +1331,12 @@ class CompanyImport implements ShouldQueue
$new_obj->save(['timestamps' => false]);
$new_obj->number = $this->getNextQuoteNumber($client = Client::find($obj_array['client_id']), $new_obj);
}
elseif($class == 'App\Models\ClientContact'){
$new_obj = new ClientContact();
$new_obj->company_id = $this->company->id;
$new_obj->fill($obj_array);
$new_obj->save(['timestamps' => false]);
}
else{
$new_obj = $class::firstOrNew(
[$match_key => $obj->{$match_key}, 'company_id' => $this->company->id],

View File

@ -50,6 +50,8 @@ class AutoBill
MultiDB::setDb($this->db);
try{
nlog("autobill {$this->invoice->id}");
$this->invoice->service()->autoBill()->save();

View File

@ -25,6 +25,8 @@ class AutoBillCron
public $tries = 1;
private $counter = 1;
/**
* Create a new job instance.
*
@ -56,6 +58,7 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill");
@ -72,6 +75,7 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill");
@ -95,6 +99,7 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}");
@ -111,19 +116,23 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}");
$auto_bill_invoices->cursor()->each(function ($invoice) use($db){
nlog($this->counter);
AutoBill::dispatch($invoice, $db);
$this->counter++;
});
}
nlog("fine");
nlog("Auto Bill - fine");
}
}

View File

@ -59,6 +59,8 @@ class CreateEntityPdf implements ShouldQueue
public $entity_string = '';
public $client;
/**
* Create a new job instance.
*
@ -69,15 +71,19 @@ class CreateEntityPdf implements ShouldQueue
$this->invitation = $invitation;
if ($invitation instanceof InvoiceInvitation) {
// $invitation->load('contact.client.company','invoice.client','invoice.user.account');
$this->entity = $invitation->invoice;
$this->entity_string = 'invoice';
} elseif ($invitation instanceof QuoteInvitation) {
// $invitation->load('contact.client.company','quote.client','quote.user.account');
$this->entity = $invitation->quote;
$this->entity_string = 'quote';
} elseif ($invitation instanceof CreditInvitation) {
// $invitation->load('contact.client.company','credit.client','credit.user.account');
$this->entity = $invitation->credit;
$this->entity_string = 'credit';
} elseif ($invitation instanceof RecurringInvoiceInvitation) {
// $invitation->load('contact.client.company','recurring_invoice');
$this->entity = $invitation->recurring_invoice;
$this->entity_string = 'recurring_invoice';
}
@ -86,6 +92,8 @@ class CreateEntityPdf implements ShouldQueue
$this->contact = $invitation->contact;
$this->client = $invitation->contact->client;
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk;
}
@ -102,7 +110,7 @@ class CreateEntityPdf implements ShouldQueue
App::setLocale($this->contact->preferredLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->entity->client->getMergedSettings()));
$t->replace(Ninja::transformTranslations($this->client->getMergedSettings()));
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->generate($this->invitation);
@ -111,22 +119,22 @@ class CreateEntityPdf implements ShouldQueue
$entity_design_id = '';
if ($this->entity instanceof Invoice) {
$path = $this->entity->client->invoice_filepath($this->invitation);
$path = $this->client->invoice_filepath($this->invitation);
$entity_design_id = 'invoice_design_id';
} elseif ($this->entity instanceof Quote) {
$path = $this->entity->client->quote_filepath($this->invitation);
$path = $this->client->quote_filepath($this->invitation);
$entity_design_id = 'quote_design_id';
} elseif ($this->entity instanceof Credit) {
$path = $this->entity->client->credit_filepath($this->invitation);
$path = $this->client->credit_filepath($this->invitation);
$entity_design_id = 'credit_design_id';
} elseif ($this->entity instanceof RecurringInvoice) {
$path = $this->entity->client->recurring_invoice_filepath($this->invitation);
$path = $this->client->recurring_invoice_filepath($this->invitation);
$entity_design_id = 'invoice_design_id';
}
$file_path = $path.$this->entity->numberFormatter().'.pdf';
$entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey($this->entity->client->getSetting($entity_design_id));
$entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey($this->client->getSetting($entity_design_id));
// if(!$this->company->account->hasFeature(Account::FEATURE_DIFFERENT_DESIGNS))
// $entity_design_id = 2;
@ -152,18 +160,18 @@ class CreateEntityPdf implements ShouldQueue
$state = [
'template' => $template->elements([
'client' => $this->entity->client,
'client' => $this->client,
'entity' => $this->entity,
'pdf_variables' => (array) $this->entity->company->settings->pdf_variables,
'pdf_variables' => (array) $this->company->settings->pdf_variables,
'$product' => $design->design->product,
'variables' => $variables,
]),
'variables' => $variables,
'options' => [
'all_pages_header' => $this->entity->client->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'),
'all_pages_header' => $this->client->getSetting('all_pages_header'),
'all_pages_footer' => $this->client->getSetting('all_pages_footer'),
],
'process_markdown' => $this->entity->client->company->markdown_enabled,
'process_markdown' => $this->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);

View File

@ -74,12 +74,15 @@ class EmailPayment implements ShouldQueue
MultiDB::setDb($this->company->db);
$this->payment->load('invoices');
$this->contact->load('client');
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build();
$invitation = null;
if($this->payment->invoices()->exists())
$invitation = $this->payment->invoices()->first()->invitations()->first();
if($this->payment->invoices && $this->payment->invoices->count() >=1)
$invitation = $this->payment->invoices->first()->invitations()->first();
$nmo = new NinjaMailerObject;
$nmo->mailable = new TemplateEmail($email_builder, $this->contact, $invitation);

View File

@ -59,15 +59,15 @@ class SendRecurring implements ShouldQueue
public function handle() : void
{
//reset all contacts here
$this->recurring_invoice->client->contacts()->update(['send_email' => false]);
// $this->recurring_invoice->client->contacts()->update(['send_email' => false]);
$this->recurring_invoice->invitations->each(function ($invitation){
// $this->recurring_invoice->invitations->each(function ($invitation){
$contact = $invitation->contact;
$contact->send_email = true;
$contact->save();
// $contact = $invitation->contact;
// $contact->send_email = true;
// $contact->save();
});
// });
// Generate Standard Invoice
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
@ -153,7 +153,7 @@ class SendRecurring implements ShouldQueue
}
//important catch all here - we should never leave contacts send_email to false incase they are permanently set to false in the future.
$this->recurring_invoice->client->contacts()->update(['send_email' => true]);
// $this->recurring_invoice->client->contacts()->update(['send_email' => true]);
}

View File

@ -237,9 +237,12 @@ class Import implements ShouldQueue
//company size check
if ($this->company->invoices()->count() > 500 || $this->company->products()->count() > 500 || $this->company->clients()->count() > 500) {
$this->company->is_large = true;
$this->company->save();
}
$this->company->client_registration_fields = \App\DataMapper\ClientRegistrationFields::generate();
$this->company->save();
$this->setInitialCompanyLedgerBalances();
// $this->fixClientBalances();

View File

@ -38,8 +38,6 @@ class SchedulerCheck implements ShouldQueue
{
set_time_limit(0);
Account::whereNotNull('id')->update(['is_scheduler_running' => true]);
if(config('ninja.app_version') != base_path('VERSION.txt'))
{

View File

@ -47,6 +47,7 @@ class PaymentEmailEngine extends BaseEmailEngine
$this->company = $payment->company;
$this->client = $payment->client;
$this->contact = $contact ?: $this->client->primary_contact()->first();
$this->contact->load('client.company');
$this->settings = $this->client->getMergedSettings();
$this->template_data = $template_data;
$this->helpers = new Helpers();

View File

@ -119,6 +119,10 @@ class Activity extends StaticModel
'hashed_id',
];
protected $with = [
'backup',
];
public function getHashedIdAttribute()
{
return $this->encodePrimaryKey($this->id);

View File

@ -84,12 +84,12 @@ class Client extends BaseModel implements HasLocalePreference
];
protected $with = [
'gateway_tokens',
'documents',
'contacts.company',
// 'gateway_tokens',
// 'documents',
// 'contacts.company',
// 'currency',
// 'primary_contact',
'country',
// 'country',
// 'contacts',
// 'shipping_country',
// 'company',

View File

@ -93,6 +93,7 @@ class Gateway extends StaticModel
GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true],
GatewayType::KBC => ['refund' => false, 'token_billing' => false],
GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
GatewayType::IDEAL => ['refund' => false, 'token_billing' => false],
];
case 15:
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal

View File

@ -27,6 +27,7 @@ class GatewayType extends StaticModel
const CREDIT = 10;
const KBC = 11;
const BANCONTACT = 12;
const IDEAL = 13;
public function gateway()
{
@ -63,7 +64,8 @@ class GatewayType extends StaticModel
return ctrans('texts.kbc_cbc');
case self::BANCONTACT:
return ctrans('texts.bancontact');
case self::IDEAL:
return ctrans('texts.ideal');
default:
return 'Undefined.';
break;

View File

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

View File

@ -449,6 +449,10 @@ class RecurringInvoice extends BaseModel
public function calculateDueDate($date)
{
//if nothing is set, assume we are using terms.
if(!$this->due_date_days)
return $this->calculateDateFromTerms($date);
switch ($this->due_date_days) {
case 'terms':
return $this->calculateDateFromTerms($date);

View File

@ -64,6 +64,10 @@ class Subscription extends BaseModel
'deleted_at' => 'timestamp',
];
protected $with = [
'company',
];
public function service(): SubscriptionService
{
return new SubscriptionService($this);

View File

@ -34,9 +34,11 @@ class InvoiceObserver
->where('event_id', Webhook::EVENT_CREATE_INVOICE)
->exists();
$invoice->load('client');
if ($subscriptions) {
$invoice->load('client');
WebhookHandler::dispatch(Webhook::EVENT_CREATE_INVOICE, $invoice, $invoice->company);
}
}
@ -53,11 +55,14 @@ class InvoiceObserver
->where('event_id', Webhook::EVENT_UPDATE_INVOICE)
->exists();
$invoice->load('client');
if ($subscriptions) {
$invoice->load('client');
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company);
}
}
@ -75,6 +80,9 @@ class InvoiceObserver
->exists();
if ($subscriptions) {
$invoice->load('client');
WebhookHandler::dispatch(Webhook::EVENT_DELETE_INVOICE, $invoice, $invoice->company);
}
}

View File

@ -30,9 +30,9 @@ class QuoteObserver
->where('event_id', Webhook::EVENT_CREATE_QUOTE)
->exists();
$quote->load('client');
if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_CREATE_QUOTE, $quote, $quote->company);
}
}
@ -49,10 +49,10 @@ class QuoteObserver
->where('event_id', Webhook::EVENT_UPDATE_QUOTE)
->exists();
$quote->load('client');
if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_QUOTE, $quote, $quote->company);
}
@ -71,6 +71,7 @@ class QuoteObserver
->exists();
if ($subscriptions) {
$quote->load('client');
WebhookHandler::dispatch(Webhook::EVENT_DELETE_QUOTE, $quote, $quote->company);
}
}

View File

@ -395,7 +395,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->first()->invitations->each(function ($invitation) use ($nmo) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if ($invitation->contact->email) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);
@ -459,7 +459,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->first()->invitations->each(function ($invitation) use ($nmo){
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);
@ -492,84 +492,84 @@ class BaseDriver extends AbstractPaymentDriver
public function checkRequirements()
{
if ($this->company_gateway->require_billing_address) {
if ($this->checkRequiredResource(auth()->user('contact')->client->address1)) {
if ($this->checkRequiredResource($this->client->address1)) {
$this->required_fields[] = 'billing_address1';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->address2)) {
if ($this->checkRequiredResource($this->client->address2)) {
$this->required_fields[] = 'billing_address2';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->city)) {
if ($this->checkRequiredResource($this->client->city)) {
$this->required_fields[] = 'billing_city';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->state)) {
if ($this->checkRequiredResource($this->client->state)) {
$this->required_fields[] = 'billing_state';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) {
if ($this->checkRequiredResource($this->client->postal_code)) {
$this->required_fields[] = 'billing_postal_code';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->country_id)) {
if ($this->checkRequiredResource($this->client->country_id)) {
$this->required_fields[] = 'billing_country';
}
}
if ($this->company_gateway->require_shipping_address) {
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address1)) {
if ($this->checkRequiredResource($this->client->shipping_address1)) {
$this->required_fields[] = 'shipping_address1';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address2)) {
if ($this->checkRequiredResource($this->client->shipping_address2)) {
$this->required_fields[] = 'shipping_address2';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_city)) {
if ($this->checkRequiredResource($this->client->shipping_city)) {
$this->required_fields[] = 'shipping_city';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_state)) {
if ($this->checkRequiredResource($this->client->shipping_state)) {
$this->required_fields[] = 'shipping_state';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_postal_code)) {
if ($this->checkRequiredResource($this->client->shipping_postal_code)) {
$this->required_fields[] = 'shipping_postal_code';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_country_id)) {
if ($this->checkRequiredResource($this->client->shipping_country_id)) {
$this->required_fields[] = 'shipping_country';
}
}
if ($this->company_gateway->require_client_name) {
if ($this->checkRequiredResource(auth()->user('contact')->client->name)) {
if ($this->checkRequiredResource($this->client->name)) {
$this->required_fields[] = 'name';
}
}
if ($this->company_gateway->require_client_phone) {
if ($this->checkRequiredResource(auth()->user('contact')->client->phone)) {
if ($this->checkRequiredResource($this->client->phone)) {
$this->required_fields[] = 'phone';
}
}
if ($this->company_gateway->require_contact_email) {
if ($this->checkRequiredResource(auth()->user('contact')->email)) {
if ($this->checkRequiredResource($this->email)) {
$this->required_fields[] = 'contact_email';
}
}
if ($this->company_gateway->require_contact_name) {
if ($this->checkRequiredResource(auth()->user('contact')->first_name)) {
$this->required_fields[] = 'contact_first_name';
}
// if ($this->company_gateway->require_contact_name) {
// if ($this->checkRequiredResource($this->first_name)) {
// $this->required_fields[] = 'contact_first_name';
// }
if ($this->checkRequiredResource(auth()->user('contact')->last_name)) {
$this->required_fields[] = 'contact_last_name';
}
}
// if ($this->checkRequiredResource($this->last_name)) {
// $this->required_fields[] = 'contact_last_name';
// }
// }
if ($this->company_gateway->require_postal_code) {
// In case "require_postal_code" is true, we don't need billing address.
@ -580,7 +580,7 @@ class BaseDriver extends AbstractPaymentDriver
}
}
if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) {
if ($this->checkRequiredResource($this->client->postal_code)) {
$this->required_fields[] = 'postal_code';
}
}

View File

@ -0,0 +1,215 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\MolliePaymentDriver;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class IDEAL implements MethodInterface
{
protected MolliePaymentDriver $mollie;
public function __construct(MolliePaymentDriver $mollie)
{
$this->mollie = $mollie;
$this->mollie->init();
}
/**
* Show the authorization page for iDEAL.
*
* @param array $data
* @return View
*/
public function authorizeView(array $data): View
{
return render('gateways.mollie.ideal.authorize', $data);
}
/**
* Handle the authorization for iDEAL.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Show the payment page for iDEAL.
*
* @param array $data
* @return Redirector|RedirectResponse
*/
public function paymentView(array $data)
{
$this->mollie->payment_hash
->withData('gateway_type_id', GatewayType::IDEAL)
->withData('client_id', $this->mollie->client->id);
try {
$payment = $this->mollie->gateway->payments->create([
'method' => 'ideal',
'amount' => [
'currency' => $this->mollie->client->currency()->code,
'value' => $this->mollie->convertToMollieAmount((float) $this->mollie->payment_hash->data->amount_with_fee),
],
'description' => \sprintf('Invoices: %s', collect($data['invoices'])->pluck('invoice_number')),
'redirectUrl' => route('client.payments.response', [
'company_gateway_id' => $this->mollie->company_gateway->id,
'payment_hash' => $this->mollie->payment_hash->hash,
'payment_method_id' => GatewayType::IDEAL,
]),
'webhookUrl' => $this->mollie->company_gateway->webhookUrl(),
'metadata' => [
'client_id' => $this->mollie->client->hashed_id,
],
]);
$this->mollie->payment_hash->withData('payment_id', $payment->id);
return redirect(
$payment->getCheckoutUrl()
);
} catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) {
return $this->processUnsuccessfulPayment($exception);
}
}
/**
* Handle unsuccessful payment.
*
* @param Exception $exception
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulPayment(\Exception $exception): void
{
PaymentFailureMailer::dispatch(
$this->mollie->client,
$exception->getMessage(),
$this->mollie->client->company,
$this->mollie->payment_hash->data->amount_with_fee
);
SystemLogger::dispatch(
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_MOLLIE,
$this->mollie->client,
$this->mollie->client->company,
);
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
/**
* Handle the payments for the iDEAL.
*
* @param PaymentResponseRequest $request
* @return mixed
*/
public function paymentResponse(PaymentResponseRequest $request)
{
if (!\property_exists($this->mollie->payment_hash->data, 'payment_id')) {
return $this->processUnsuccessfulPayment(
new PaymentFailed('Whoops, something went wrong. Missing required [payment_id] parameter. Please contact administrator. Reference hash: ' . $this->mollie->payment_hash->hash)
);
}
try {
$payment = $this->mollie->gateway->payments->get(
$this->mollie->payment_hash->data->payment_id
);
if ($payment->status === 'paid') {
return $this->processSuccessfulPayment($payment);
}
if ($payment->status === 'open') {
return $this->processOpenPayment($payment);
}
if ($payment->status === 'failed') {
return $this->processUnsuccessfulPayment(
new PaymentFailed(ctrans('texts.status_failed'))
);
}
return $this->processUnsuccessfulPayment(
new PaymentFailed(ctrans('texts.status_voided'))
);
} catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) {
return $this->processUnsuccessfulPayment($exception);
}
}
/**
* Handle the successful payment for iDEAL.
*
* @param string $status
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment, string $status = 'paid'): RedirectResponse
{
$data = [
'gateway_type_id' => GatewayType::IDEAL,
'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total,
'payment_type' => PaymentType::IDEAL,
'transaction_reference' => $payment->id,
];
$payment_record = $this->mollie->createPayment(
$data,
$status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING
);
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)]);
}
/**
* Handle 'open' payment status for IDEAL.
*
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse
{
return $this->processSuccessfulPayment($payment, 'open');
}
}

View File

@ -27,6 +27,7 @@ use App\Models\SystemLog;
use App\PaymentDrivers\Mollie\Bancontact;
use App\PaymentDrivers\Mollie\BankTransfer;
use App\PaymentDrivers\Mollie\CreditCard;
use App\PaymentDrivers\Mollie\IDEAL;
use App\PaymentDrivers\Mollie\KBC;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator;
@ -70,6 +71,7 @@ class MolliePaymentDriver extends BaseDriver
GatewayType::BANCONTACT => Bancontact::class,
GatewayType::BANK_TRANSFER => BankTransfer::class,
GatewayType::KBC => KBC::class,
GatewayType::IDEAL => IDEAL::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
@ -93,6 +95,7 @@ class MolliePaymentDriver extends BaseDriver
$types[] = GatewayType::BANCONTACT;
$types[] = GatewayType::BANK_TRANSFER;
$types[] = GatewayType::KBC;
$types[] = GatewayType::IDEAL;
return $types;
}

View File

@ -70,13 +70,9 @@ class ActivityRepository extends BaseRepository
*/
public function createBackup($entity, $activity)
{
if($entity instanceof User){
}
else if ($entity->company->is_disabled) {
if ($entity instanceof User || $entity->company->is_disabled)
return;
}
$backup = new Backup();
@ -85,6 +81,7 @@ class ActivityRepository extends BaseRepository
|| get_class($entity) == Credit::class
|| get_class($entity) == RecurringInvoice::class
) {
$entity->load('company', 'client');
$contact = $entity->client->primary_contact()->first();
$backup->html_backup = $this->generateHtml($entity);
$backup->amount = $entity->amount;
@ -92,7 +89,6 @@ class ActivityRepository extends BaseRepository
$backup->activity_id = $activity->id;
$backup->json_backup = '';
//$backup->json_backup = $entity->toJson();
$backup->save();
}
@ -121,6 +117,8 @@ class ActivityRepository extends BaseRepository
$entity_design_id = 'credit_design_id';
}
$entity->load('client','client.company');
$entity_design_id = $entity->design_id ? $entity->design_id : $this->decodePrimaryKey($entity->client->getSetting($entity_design_id));
$design = Design::find($entity_design_id);

View File

@ -174,10 +174,6 @@ class BaseRepository
if(array_key_exists('client_id', $data))
$model->client_id = $data['client_id'];
//pickup changes here to recalculate reminders
//if($model instanceof Invoice && ($model->isDirty('date') || $model->isDirty('due_date')))
// $model->service()->setReminder()->save();
$client = Client::where('id', $model->client_id)->withTrashed()->first();
$state = [];
@ -210,7 +206,10 @@ class BaseRepository
$model->custom_surcharge_tax3 = $client->company->custom_surcharge_taxes3;
$model->custom_surcharge_tax4 = $client->company->custom_surcharge_taxes4;
$model->saveQuietly();
if(!$model->id)
$model->save();
else
$model->saveQuietly();
/* Model now persisted, now lets do some child tasks */
@ -309,10 +308,6 @@ class BaseRepository
/* Perform model specific tasks */
if ($model instanceof Invoice) {
nlog("Finished amount = " . $state['finished_amount']);
nlog("Starting amount = " . $state['starting_amount']);
nlog("Diff = " . ($state['finished_amount'] - $state['starting_amount']));
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {

View File

@ -0,0 +1,291 @@
<?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\ClientPortal;
use App\Exceptions\PaymentFailed;
use App\Factory\PaymentFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Invoice\InjectSignature;
use App\Jobs\Util\SystemLogger;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\Services\Subscription\SubscriptionService;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Illuminate\View\View;
class InstantPayment
{
use MakesHash;
use MakesDates;
public Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function run()
{
$is_credit_payment = false;
$tokens = [];
if ($this->request->input('company_gateway_id') == CompanyGateway::GATEWAY_CREDIT) {
$is_credit_payment = true;
}
$gateway = CompanyGateway::find($this->request->input('company_gateway_id'));
/**
* find invoices
*
* ['invoice_id' => xxx, 'amount' => 22.00]
*/
$payable_invoices = collect($this->request->payable_invoices);
$invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function($invoice){
$invoice->service()->removeUnpaidGatewayFees()->save();
});
/* pop non payable invoice from the $payable_invoices array */
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable();
});
/*return early if no invoices*/
if ($payable_invoices->count() == 0) {
return redirect()
->route('client.invoices.index')
->with(['message' => 'No payable invoices selected.']);
}
$client = $invoices->first()->client;
$settings = $client->getMergedSettings();
/* This loop checks for under / over payments and returns the user if a check fails */
foreach ($payable_invoices as $payable_invoice) {
/*Match the payable invoice to the Model Invoice*/
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
/*
* Check if company supports over & under payments.
* Determine the payable amount and the max payable. ie either partial or invoice balance
*/
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount'], $client->currency()->precision));
$invoice_balance = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), $client->currency()->precision);
/*If we don't allow under/over payments force the payable amount - prevents inspect element adjustments in JS*/
if ($settings->client_portal_allow_under_payment == false && $settings->client_portal_allow_over_payment == false) {
$payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), $client->currency()->precision);
}
if (!$settings->client_portal_allow_under_payment && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($settings->client_portal_allow_under_payment) {
if ($invoice_balance < $settings->client_portal_under_payment_minimum && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($invoice_balance < $settings->client_portal_under_payment_minimum) {
// Skip the under payment rule.
}
if ($invoice_balance >= $settings->client_portal_under_payment_minimum && $payable_amount < $settings->client_portal_under_payment_minimum) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]));
}
}
/* If we don't allow over payments and the amount exceeds the balance */
if (!$settings->client_portal_allow_over_payment && $payable_amount > $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.over_payments_disabled'));
}
}
/*Iterate through invoices and add gateway fees and other payment metadata*/
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice_collection = collect();
foreach ($payable_invoices as $payable_invoice) {
// nlog($payable_invoice);
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']);
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount'], $client->currency()->precision));
$invoice_balance = Number::roundValue($invoice->balance, $client->currency()->precision);
$payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
$payable_invoice['invoice_number'] = $invoice->number;
if (isset($invoice->po_number)) {
$additional_info = $invoice->po_number;
} elseif (isset($invoice->public_notes)) {
$additional_info = $invoice->public_notes;
} else {
$additional_info = $invoice->date;
}
$payable_invoice['additional_info'] = $additional_info;
$payable_invoice_collection->push($payable_invoice);
}
if ($this->request->has('signature') && !is_null($this->request->signature) && !empty($this->request->signature)) {
$invoices->each(function ($invoice){
InjectSignature::dispatch($invoice, $this->request->signature);
});
}
$payable_invoices = $payable_invoice_collection;
$payment_method_id = $this->request->input('payment_method_id');
$invoice_totals = $payable_invoices->sum('amount');
$first_invoice = $invoices->first();
$credit_totals = $first_invoice->client->getSetting('use_credits_payment') == 'always' ? $first_invoice->client->service()->getCreditBalance() : 0;
$starting_invoice_amount = $first_invoice->balance;
if ($gateway) {
$first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save();
}
/**
* Gateway fee is calculated
* by adding it as a line item, and then subtract
* the starting and finishing amounts of the invoice.
*/
$fee_totals = $first_invoice->balance - $starting_invoice_amount;
if ($gateway) {
$tokens = $client->gateway_tokens()
->whereCompanyGatewayId($gateway->id)
->whereGatewayTypeId($payment_method_id)
->get();
}
if(!$is_credit_payment){
$credit_totals = 0;
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
if ($this->request->query('hash')) {
$hash_data['billing_context'] = Cache::get($this->request->query('hash'));
}
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(32);
$payment_hash->data = $hash_data;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
if($is_credit_payment){
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
}
else{
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [
'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => $amount_with_fee,
];
$data = [
'payment_hash' => $payment_hash->hash,
'total' => $totals,
'invoices' => $payable_invoices,
'tokens' => $tokens,
'payment_method_id' => $payment_method_id,
'amount_with_fee' => $invoice_totals + $fee_totals,
];
if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($this->request, $data);
}
try {
return $gateway
->driver($client)
->setPaymentMethod($payment_method_id)
->setPaymentHash($payment_hash)
->checkRequirements()
->processPaymentView($data);
} catch (\Exception $e) {
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_FAILURE,
$client,
$client->company
);
throw new PaymentFailed($e->getMessage());
}
}
public function processCreditPayment(Request $request, array $data)
{
return render('gateways.credit.index', $data);
}
}

View File

@ -44,7 +44,7 @@ class SendEmail
}
$this->credit->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
$email_builder = (new CreditEmail())->build($invitation, $this->reminder_template);
// EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company);

View File

@ -304,7 +304,7 @@ class AutoBillInvoice extends AbstractService
{
//get all client gateway tokens and set the is_default one to the first record
$gateway_tokens = $this->client->gateway_tokens()->orderBy('is_default', 'DESC');
$gateway_tokens = $this->client->gateway_tokens()->orderBy('is_default', 'DESC')->get();
// $gateway_tokens = $this->client->gateway_tokens;
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use($amount) {
@ -312,7 +312,7 @@ class AutoBillInvoice extends AbstractService
$company_gateway = $gateway_token->gateway;
//check if fees and limits are set
if (isset($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id))
if (isset($company_gateway->fees_and_limits) && !is_array($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id))
{
//if valid we keep this gateway_token
if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount))

View File

@ -322,6 +322,8 @@ class InvoiceService
public function deletePdf()
{
$this->invoice->load('invitations');
$this->invoice->invitations->each(function ($invitation){
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
@ -452,13 +454,13 @@ class InvoiceService
if (! $this->invoice->design_id)
$this->invoice->design_id = $this->decodePrimaryKey($settings->invoice_design_id);
if (!isset($this->invoice->footer))
if (!isset($this->invoice->footer) || empty($this->invoice->footer))
$this->invoice->footer = $settings->invoice_footer;
if (!isset($this->invoice->terms))
if (!isset($this->invoice->terms) || empty($this->invoice->terms))
$this->invoice->terms = $settings->invoice_terms;
if (!isset($this->invoice->public_notes))
if (!isset($this->invoice->public_notes) || empty($this->invoice->public_notes))
$this->invoice->public_notes = $this->invoice->client->public_notes;
/* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/
@ -473,8 +475,10 @@ class InvoiceService
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;

View File

@ -44,7 +44,7 @@ class SendEmail extends AbstractService
}
$this->invoice->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
}
});

View File

@ -32,8 +32,10 @@ class SendEmail
*/
public function run()
{
$this->payment->load('company', 'client.contacts');
$this->payment->client->contacts->each(function ($contact) {
if ($contact->send_email && $contact->email) {
if ($contact->email) {
EmailPayment::dispatchNow($this->payment, $this->payment->company, $contact);
}
});

View File

@ -77,8 +77,6 @@ class PdfMaker
$this->updateVariables($this->data['variables']);
}
$this->processOptions();
return $this;
}

View File

@ -163,144 +163,6 @@ trait PdfMakerUtilities
return $element;
}
public function processOptions()
{
if (!isset($this->options['all_pages_header']) || $this->options['all_pages_header'] == false) {
return;
}
if (!isset($this->options['all_pages_footer']) || $this->options['all_pages_footer'] == false) {
return;
}
$this->insertPrintCSS();
$this->wrapIntoTable();
}
public function insertPrintCSS()
{
$css = <<<'EOT'
table.page-container {
page-break-after: always;
min-width: 100%;
}
thead.page-header {
display: table-header-group;
}
tfoot.page-footer {
display: table-footer-group;
}
EOT;
$css_node = $this->document->createTextNode($css);
$style = $this->document->getElementsByTagName('style')->item(0);
if ($style) {
return $style->appendChild($css_node);
}
$head = $this->document->getElementsByTagName('head')->item(0);
if ($head) {
$style_node = $this->document->createElement('style', $css);
return $head->appendChild($style_node);
}
return $this;
}
public function wrapIntoTable()
{
$markup = <<<'EOT'
<table class="page-container" id="page-container">
<thead class="page-report">
<tr>
<th class="page-report-cell" id="repeat-header">
<!-- Repeating header goes here.. -->
</th>
</tr>
</thead>
<tfoot class="report-footer">
<tr>
<td class="report-footer-cell" id="repeat-footer">
<!-- Repeating footer goes here -->
</td>
</tr>
</tfoot>
<tbody class="report-content">
<tr>
<td class="report-content-cell" id="repeat-content">
<!-- Rest of the content goes here -->
</td>
</tr>
</tbody>
</table>
EOT;
$document = new DOMDocument();
$document->loadHTML($markup);
$table = $document->getElementById('page-container');
$body = $this->document->getElementsByTagName('body')
->item(0);
$body->appendChild(
$this->document->importNode($table, true)
);
for ($i = 0; $i < $body->childNodes->length; $i++) {
$element = $body->childNodes->item($i);
if ($element->nodeType !== 1) {
continue;
}
if (
$element->getAttribute('id') == 'header' ||
$element->getAttribute('id') == 'footer' ||
$element->getAttribute('id') === 'page-container'
) {
continue;
}
$clone = $element->cloneNode(true);
$element->parentNode->removeChild($element);
$this->document->getElementById('repeat-content')->appendChild($clone);
}
// info($this->data['options']);
if (
$header = $this->document->getElementById('header') &&
isset($this->data['options']['all_pages_header']) &&
$this->data['options']['all_pages_header']
) {
$header = $this->document->getElementById('header');
$clone = $header->cloneNode(true);
$header->parentNode->removeChild($header);
$this->document->getElementById('repeat-header')->appendChild($clone);
}
if (
$footer = $this->document->getElementById('footer') &&
isset($this->data['options']['all_pages_footer']) &&
$this->data['options']['all_pages_footer']
) {
$footer = $this->document->getElementById('footer');
$clone = $footer->cloneNode(true);
$footer->parentNode->removeChild($footer);
$this->document->getElementById('repeat-footer')->appendChild($clone);
}
}
public function getEmptyElements(array &$elements, array $variables)
{
foreach ($elements as &$element) {

View File

@ -39,7 +39,7 @@ class ConvertQuote
{
$invoice = CloneQuoteToInvoiceFactory::create($quote, $quote->user_id);
$invoice->design_id = $this->decodePrimaryKey($this->client->getSetting('invoice_design_id'));
$invoice = $this->invoice_repo->save([], $invoice);
$invoice = $this->invoice_repo->save($invoice->toArray(), $invoice);
$invoice->fresh();

View File

@ -42,7 +42,7 @@ class SendEmail
}
$this->quote->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
}
});

View File

@ -165,6 +165,7 @@ class CompanyTransformer extends EntityTransformer
'markdown_enabled' => (bool) $company->markdown_enabled,
'use_comma_as_decimal_place' => (bool) $company->use_comma_as_decimal_place,
'report_include_drafts' => (bool) $company->report_include_drafts,
'client_registration_fields' => (array) $company->client_registration_fields,
];
}

View File

@ -49,6 +49,7 @@ class HtmlEngine
public function __construct($invitation)
{
$this->invitation = $invitation;
// $invitation->load('contact.client.company', 'company');
$this->entity_string = $this->resolveEntityString();
@ -58,7 +59,9 @@ class HtmlEngine
$this->contact = $invitation->contact;
$this->client = $this->entity->client;
$this->client = $invitation->contact->client;
$this->client->load('country','company');
$this->entity->load('client');
$this->settings = $this->client->getMergedSettings();
@ -113,22 +116,25 @@ class HtmlEngine
$data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$invoice.date'] = &$data['$date'];
$data['$invoiceDate'] = &$data['$date'];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$dueDate'] = &$data['$due_date'];
$data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];
$data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];
$data['$invoice.due_date'] = &$data['$due_date'];
$data['$invoice.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: '&nbsp;', 'label' => ctrans('texts.po_number')];
$data['$poNumber'] = &$data['$invoice.po_number'];
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->entity->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.date')];
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.date')];
$data['$invoice.datetime'] = &$data['$entity.datetime'];
$data['$quote.datetime'] = &$data['$entity.datetime'];
$data['$credit.datetime'] = &$data['$entity.datetime'];
$data['$payment_button'] = ['value' => '<a class="button" href="'.$this->invitation->getPaymentLink().'">'.ctrans('texts.pay_now').'</a>', 'label' => ctrans('texts.pay_now')];
$data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')];
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')];
@ -140,11 +146,11 @@ class HtmlEngine
$data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link'];
$data['$view_button'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$payment_button'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')];
if($this->entity->project()->exists()) {
if($this->entity->project) {
$data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')];
}
}
@ -161,7 +167,7 @@ class HtmlEngine
$data['$view_button'] = &$data['$view_link'];
$data['$approveButton'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.approve')];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
}
if ($this->entity_string == 'credit') {
@ -176,7 +182,7 @@ class HtmlEngine
$data['$viewLink'] = &$data['$view_link'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')];
// $data['$view_link'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.credit_date')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.credit_date')];
}
$data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>''];
@ -197,7 +203,7 @@ class HtmlEngine
if ($this->entity->partial > 0) {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
} else {
@ -230,7 +236,7 @@ class HtmlEngine
$data['$credit.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')];
$data['$credit.total'] = &$data['$credit.total'];
$data['$credit.po_number'] = &$data['$invoice.po_number'];
$data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.credit_date')];
$data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.credit_date')];
$data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance')];
$data['$credit.balance'] = &$data['$balance'];
@ -257,13 +263,13 @@ class HtmlEngine
$data['$entity_issued_to'] = ['value' => '', 'label' => ctrans("texts.{$this->entity_string}_issued_to")];
$data['$your_entity'] = ['value' => '', 'label' => ctrans("texts.your_{$this->entity_string}")];
$data['$quote.date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$quote.date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$quote.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')];
$data['$quote.po_number'] = &$data['$invoice.po_number'];
$data['$quote.quote_number'] = &$data['$quote.number'];
$data['$quote_no'] = &$data['$quote.number'];
$data['$quote.quote_no'] = &$data['$quote.number'];
$data['$quote.valid_until'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.valid_until')];
$data['$quote.valid_until'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.valid_until')];
$data['$valid_until'] = &$data['$quote.valid_until'];
$data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')];
$data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')];
@ -457,7 +463,8 @@ class HtmlEngine
$data['$auto_bill'] = &$data['$autoBill'];
/*Payment Aliases*/
$data['$paymentLink'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')];
$data['$paymentLink'] = &$data['$payment_link'];
$data['$payment_url'] = &$data['$payment_link'];
$data['$portalButton'] = &$data['$paymentLink'];
$data['$dir'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];

View File

@ -82,11 +82,20 @@ class Phantom
$url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret');
info($url);
$key = config('ninja.phantomjs_key');
$secret = config('ninja.phantomjs_key');
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D";
$pdf = CurlUtils::get($phantom_url);
$key = config( 'ninja.phantomjs_key' );
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
$pdf = CurlUtils::post( $phantom_url, json_encode( [
'url' => $url,
'renderType' => 'pdf',
'outputAsJson' => false,
'renderSettings' => [
'emulateMedia' => 'print',
'pdfOptions' => [
'preferCSSPageSize' => true,
'printBackground' => true,
],
],
] ) );
$this->checkMime($pdf, $invitation, $entity);
@ -100,14 +109,20 @@ class Phantom
public function convertHtmlToPdf($html)
{
$hash = Str::random(32);
Cache::put($hash, $html, 300);
$url = route('tmp_pdf', ['hash' => $hash]);
info($url);
$key = config('ninja.phantomjs_key');
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D";
$pdf = CurlUtils::get($phantom_url);
$key = config( 'ninja.phantomjs_key' );
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
$pdf = CurlUtils::post( $phantom_url, json_encode( [
'content' => $html,
'renderType' => 'pdf',
'outputAsJson' => false,
'renderSettings' => [
'emulateMedia' => 'print',
'pdfOptions' => [
'preferCSSPageSize' => true,
'printBackground' => true,
],
],
] ) );
$response = Response::make($pdf, 200);
$response->header('Content-Type', 'application/pdf');

View File

@ -43,6 +43,17 @@ trait Inviteable
return $status;
}
public function getPaymentLink()
{
if(Ninja::isHosted()){
$domain = $this->company->domain();
}
else
$domain = config('ninja.app_url');
return $domain.'/client/pay/'.$this->key;
}
public function getLink() :string
{
$entity_type = Str::snake(class_basename($this->entityType()));

View File

@ -270,7 +270,7 @@ trait MakesInvoiceValues
if (! is_array($items)) {
$data;
}
$locale_info = localeconv();
foreach ($items as $key => $item) {
@ -296,11 +296,7 @@ trait MakesInvoiceValues
$data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $this->client);
$data[$key][$table_type.'.description'] = Helpers::processReservedKeywords($item->notes, $this->client);
/* need to test here as this is new - 18/09/2021*/
if(!array_key_exists($table_type.'.gross_line_total', $data[$key]))
$data[$key][$table_type.'.gross_line_total'] = 0;
$data[$key][$table_type . ".{$_table_type}1"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}1", $item->custom_value1, $this->client);
$data[$key][$table_type . ".{$_table_type}2"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}2", $item->custom_value2, $this->client);
$data[$key][$table_type . ".{$_table_type}3"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}3", $item->custom_value3, $this->client);
@ -314,8 +310,12 @@ trait MakesInvoiceValues
$data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client);
$data[$key][$table_type.'.line_total'] = Number::formatMoney($item->line_total, $this->client);
if(property_exists($item, 'gross_line_total'))
$data[$key][$table_type.'.gross_line_total'] = Number::formatMoney($item->gross_line_total, $this->client);
else
$data[$key][$table_type.'.gross_line_total'] = 0;
if (isset($item->discount) && $item->discount > 0) {
if ($item->is_amount_discount) {
$data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client);

406
composer.lock generated

File diff suppressed because it is too large Load Diff

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.3.19',
'app_tag' => '5.3.19',
'app_version' => '5.3.22',
'app_tag' => '5.3.22',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,28 @@
<?php
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIdealToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('payment_types', function (Blueprint $table) {
$type = new PaymentType();
$type->id = 37;
$type->name = 'iDEAL';
$type->gateway_type_id = GatewayType::IDEAL;
$type->save();
});
}
}

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdatedBoldAndModernDesigns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
\Illuminate\Support\Facades\Artisan::call('ninja:design-update');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -3,38 +3,38 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"/": "0d9690c2b925c794b94e0778817e5c19",
"version.json": "9ec5e3813adc4bfd8713556c5059e97d",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1",
"assets/NOTICES": "9eb7e2eb2888ea5bae5f536720db37cd",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/google_logo.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/google_logo.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/logo_dark.png": "a233ed1d4d0f7414bf97a9a10f11fb0a",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1",
"assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5",
"version.json": "9ec5e3813adc4bfd8713556c5059e97d",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"main.dart.js": "9542568225b6ad9e1ffbc87c3e6f74a2"
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"main.dart.js": "7905763d1c2f393f06c8c5b95449e54b",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"/": "2b102108c1e1cc925a193f954e3a2b68"
};
// The application shell files that are downloaded before a service worker can

204444
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

210264
public/main.foss.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

216217
public/main.last.dart.js vendored

File diff suppressed because one or more lines are too long

216052
public/main.next.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

201726
public/main.wasm.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -4316,8 +4316,8 @@ $LANG = array(
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
'kbc_cbc' => 'KBC/CBC',
'bancontact' => 'Bancontact',
'sepa_mandat' => 'By providing your IBAN and confirming this payment, you are authorizing :company and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.',
'bank_account_holder' => 'Bank Account Holder',
'sepa_mandat' => 'By providing your IBAN and confirming this payment, you are authorizing Rocketship Inc. and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.',
'ideal' => 'iDEAL',
);
return $LANG;

View File

@ -13,31 +13,51 @@
zoom: 80%;
}
body, html {
margin: 0;
padding: 0;
}
@page {
margin: -0.25cm !important;
margin: 0 !important;
size: $page_size $page_layout;
}
p {
margin: 0;
padding: 0;
/* page-break-after: always; */
}
.header-wrapper {
#spacer-table > * > tr > td {
padding: 0;
}
#spacer-table{
width: 100%;
}
#header {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr;
gap: 20px;
background-color: #2d2c2a;
padding: 3rem;
color: white;
min-width: 100%;
line-height: var(--line-height);
position: fixed;
top: 0;
}
#header, #header-spacer {
height: 160px;
padding: 3rem;
margin-bottom: 3rem;
}
.company-logo {
height: 100%;
padding-right: 120px;
max-width: 100%;
object-fit: contain;
object-position: left center;
}
#company-details,
@ -46,6 +66,12 @@
flex-direction: column;
}
#company-details,
#company-address,
.logo-container {
max-height: 160px;
}
#client-details {
margin: 2rem;
display: flex;
@ -61,7 +87,6 @@
display: grid;
grid-template-columns: 1.5fr 1fr;
padding-left: 1rem;
padding-top: 3rem;
}
.entity-details-wrapper {
@ -86,7 +111,11 @@
table-layout: fixed;
overflow-wrap: break-word;
margin-top: 3rem;
/* margin-bottom: 200px; */
margin-bottom: 200px;
}
[data-ref="table"]:last-child{
margin-bottom:0;
}
.task-time-details {
@ -138,23 +167,23 @@
gap: 80px;
}
#table-totals .totals-table-right-side>* {
#table-totals .totals-table-right-side > * {
display: grid;
grid-template-columns: 1fr 1fr;
}
#table-totals>.totals-table-right-side>*> :nth-child(1) {
#table-totals > .totals-table-right-side > * > :nth-child(1) {
text-align: "$dir_text_align";
margin-top: .75rem;
}
#table-totals>.totals-table-right-side> * > :not([hidden]) ~ :not([hidden]) {
#table-totals > .totals-table-right-side > * > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(.75rem * var(--tw-space-y-reverse));
}
#table-totals>.totals-table-right-side>*> :nth-child(2) {
#table-totals > .totals-table-right-side > * > :nth-child(2) {
text-align: right;
}
@ -186,51 +215,40 @@
font-size: 1.5rem;
}
.footer-wrapper {
#footer {
margin-top: 1rem;
background-color: #2d2c2a;
height: 160px;
min-width: 100%;
position: fixed;
bottom: 0;
padding: 1rem 3rem;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 15px;
color: white;
}
#footer, #footer-spacer {
height: 160px;
padding: 1rem 3rem;
margin-top: 3rem;
}
[data-ref="total_table-footer"] {
padding-top: 0.5rem
}
/** Repeating header & footer styling. */
table {
width: 100%;
}
table.print-content {}
table.print-content th,
table.print-content td {
table[data-ref="table"] th,
table[data-ref="table"] td {
padding: .2rem .4rem;
text-align: left;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
@media print {
.print-footer {
position: fixed;
bottom: 0;
left: 0;
}
.no-print {
display: none
}
}
/** Markdown-specific styles. **/
#product-table h3,
#task-table h3,
@ -239,9 +257,9 @@
margin-bottom: 0;
}
[data-ref="product_table-product.description-th"] {
width: 23%;
}
[data-ref="product_table-product.description-th"] {
width: 23%;
}
[data-ref="statement-totals"] {
margin-top: 1rem;
@ -253,10 +271,6 @@
white-space: nowrap;
}
.logo-container {
max-height: 160px;
}
#statement-invoice-table-totals > p {
margin-right: 2rem;
margin-top: 1rem;
@ -264,101 +278,94 @@
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/
/* .company-logo { display: none } */
/** Hide company logo **/
/* .company-logo { display: none } */
/* Hide company details */
/* #company-details > * { display: none } */
/* Hide company details */
/* #company-details > * { display: none } */
/* Hide company address */
/* #company-address > * { display: none } */
/* Hide company address */
/* #company-address > * { display: none } */
/* Hide public notes */
/* [data-ref="total_table-public_notes"] { display: none } */
/* Hide public notes */
/* [data-ref="total_table-public_notes"] { display: none } */
/* Hide terms label */
/* [data-ref="total_table-terms-label"] { display: none } */
/* Hide terms label */
/* [data-ref="total_table-terms-label"] { display: none } */
/* Hide totals table */
/* #table-totals { display: none } */
/* Hide totals table */
/* #table-totals { display: none } */
/* Hide totals table left side */
/* #table-totals div:first-child > * { display: none !important } */
/* Hide totals table left side */
/* #table-totals div:first-child > * { display: none !important } */
/* Hide totals table right side */
/* .totals-table-right-side { display: none } */
/* Hide totals table right side */
/* .totals-table-right-side { display: none } */
/** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
/** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style>
<table>
<!-- Start Header -->
<thead>
<tr>
<td>
<div class="header-wrapper" id="header">
<div class="logo-container">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
</div>
<div id="company-details"></div>
<div id="company-address"></div>
</div>
</td>
</tr>
</thead>
<!-- End Header -->
<tr>
<td id="body">
<div class="client-entity-wrapper">
<div class="client-wrapper-left-side">
<h4 class="entity-label">$entity_label</h4>
<div id="client-details" cellspacing="0"></div>
</div>
<div id="header">
<div class="logo-container">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
</div>
<div id="company-details"></div>
<div id="company-address"></div>
</div>
<div id="body">
<table id="spacer-table" cellspacing="0" >
<thead>
<tr>
<td>
<div id="header-spacer"></div>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="client-entity-wrapper">
<div class="client-wrapper-left-side">
<h4 class="entity-label">$entity_label</h4>
<div id="client-details" cellspacing="0"></div>
</div>
<div class="entity-details-wrapper-right-side">
<div class="entity-details-wrapper">
<table id="entity-details" dir="$dir"></table>
<div class="entity-details-wrapper-right-side">
<div class="entity-details-wrapper">
<table id="entity-details" dir="$dir"></table>
</div>
</div>
</div>
</div>
<!-- Start Print Content -->
<table id="product-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="product-table" cellspacing="0" data-ref="table"></table>
<table id="task-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="task-table" cellspacing="0" data-ref="table"></table>
<table id="delivery-note-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
<table id="statement-invoice-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-aging-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
<!-- End Print Content -->
</td>
</tr>
<tr>
<td>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
<!-- Start Space For Footer -->
<tfoot>
<tr>
<td style="height: 180px">
<!-- Leave this empty and don't remove it. This space is where footer placed on print. -->
</td>
</tr>
</tfoot>
<!-- End Space For Footer -->
</table>
<!-- Start Footer -->
<div class="footer-wrapper print-footer" id="footer">
<table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div id="footer-spacer"></div>
</td>
</tr>
</tfoot>
</table>
</div>
<div id="footer">
<div>
<p data-ref="total_table-footer">$entity_footer</p>
@ -366,14 +373,13 @@
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => {
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table-totals', 'statement-payment-table-totals','statement-invoice-table-totals',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
];
tables.forEach((tableIdentifier) => {
document.getElementById(tableIdentifier).childElementCount === 0
? document.getElementById(tableIdentifier).style.display = 'none'
: '';
const el =document.getElementById(tableIdentifier);
if(el && el.childElementCount === 0)el.remove()
});
});
</script>
@ -381,4 +387,3 @@
<div> <!-- #2 column --> </div>
<div> <!-- #3 column --> </div>
</div>
<!-- End Footer -->

View File

@ -9,38 +9,54 @@
-moz-osx-font-smoothing: grayscale;
font-family: Arial, Helvetica, sans-serif;
font-size: "7px";
margin: 0;
padding: 0;
zoom: 80%;
}
body, html {
margin: 0;
padding: 0;
}
@page {
margin: -0.22cm;
size: A4 portrait;
margin: 0 !important;
size: $page_size $page_layout;
}
p {
margin: 0;
padding: 0;
/* page-break-after: always; */
}
.header-container {
background-color: var(--primary-color);
color: white;
display: grid;
grid-template-columns: 1.5fr 1fr;
padding: 3rem;
min-width: 100%;
height: 160px;
#spacer-table > * > tr > td {
padding: 0;
}
#spacer-table{
width: 100%;
}
#header {
background-color: var(--primary-color);
color: white;
display: grid;
grid-template-columns: 1.5fr 1fr;
position:fixed;
top: 0;
width: 100%;
}
#header, #header-spacer {
height: 160px;
padding: 3rem;
margin-bottom: 3rem;
}
.company-name {
text-align: left;
}
.header-container .company-name {
font-size: 2rem;
#header .company-name {
font-size: 2rem;
}
#entity-details {
@ -55,7 +71,7 @@
}
.logo-client-wrapper {
margin: 3rem 2rem;
margin: 0 2rem 3rem;
display: grid;
grid-template-columns: 1.5fr 1fr;
}
@ -121,15 +137,18 @@
text-align: right;
}
.footer-wrapper {
margin-top: 1rem;
background-color: var(--primary-color);
padding-left: 3rem;
padding-right: 3rem;
height: 220px;
width: 100%;
position: fixed;
bottom: 0;
#footer {
background-color: var(--primary-color);
width: 100%;
position: fixed;
bottom: 0;
}
#footer, #footer-spacer {
height: 220px;
padding: 1rem 3rem;
margin-top: 1rem;
}
.footer-content {
@ -204,54 +223,21 @@
font-size: 1.3rem;
}
table.page-container {
page-break-after: always;
min-width: 100%;
}
thead.page-header {
display: table-header-group;
}
tfoot.page-footer {
display: table-footer-group;
}
.page-content-cell {
padding: 1rem;
}
[data-ref="total_table-footer"] {
margin-top: 2rem;
margin-bottom: 2rem;
margin-top: 2rem;
margin-bottom: 2rem;
}
/** Repeating header & footer styling. */
table {
width: 100%;
width: 100%;
}
table.print-content {
}
table.print-content th,
table.print-content td {
padding: 0.2rem 0.4rem;
text-align: left;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
@media print {
.print-footer {
position: fixed;
bottom: 0;
left: 0;
}
.no-print {
display: none;
}
table[data-ref="table"] th,
table[data-ref="table"] td {
padding: 0.2rem 0.4rem;
text-align: left;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
/** Markdown-specific styles. **/
@ -306,70 +292,57 @@
/** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style>
<div id="header">
<h1 class="company-name">$company.name</h1>
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
<div id="body">
<table id="spacer-table" cellspacing="0">
<thead>
<tr>
<td>
<div id="header-spacer"></div>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="logo-client-wrapper">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
<div id="client-details"></div>
</div>
<div class="table-wrapper">
<table id="product-table" cellspacing="0" data-ref="table"></table>
<table>
<!-- Start Header -->
<thead>
<tr>
<td>
<div class="header-container" id="header">
<h1 class="company-name">$company.name</h1>
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
</td>
</tr>
</thead>
<!-- End Header -->
<tr>
<td id="body">
<div class="logo-client-wrapper">
<img
class="company-logo"
src="$company.logo"
alt="$company.name logo"
/>
<table id="task-table" cellspacing="0" data-ref="table"></table>
<div id="client-details"></div>
</div>
<table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
<!-- Start Print Content -->
<div class="table-wrapper">
<table id="product-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="task-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="delivery-note-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
</div>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div id="footer-spacer"></div>
</td>
</tr>
</tfoot>
</table>
</div>
<table id="statement-invoice-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-aging-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
</div>
<!-- End Print Content -->
</td>
</tr>
<tr>
<td>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
<!-- Start Space For Footer -->
<tfoot>
<tr>
<td style="height: 230px">
<!-- Leave this empty and don't remove it. This space is where footer placed on print -->
</td>
</tr>
</tfoot>
<!-- End Space For Footer -->
</table>
<!-- Start Footer -->
<div class="footer-wrapper print-footer" id="footer">
<div id="footer">
<div class="footer-content">
<div>
<p data-ref="total_table-footer">$entity_footer</p>
@ -378,14 +351,14 @@
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => {
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table-totals', 'statement-payment-table-totals','statement-invoice-table-totals',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
];
tables.forEach((tableIdentifier) => {
document.getElementById(tableIdentifier).childElementCount === 0
? document.getElementById(tableIdentifier).style.display = 'none'
: '';
? document.getElementById(tableIdentifier).remove()
: '';
});
});
</script>
@ -396,4 +369,3 @@
</div>
</div>
</div>
<!-- End Footer -->

View File

@ -13,9 +13,13 @@
<div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
@if($account && !$account->isPaid())
<div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div>
<div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div>
@elseif(isset($company) && !is_null($company))
<div>
<img src="{{ asset($company->present()->logo()) }}" class="h-14 mb-10" alt="{{ $company->present()->name() }} logo">
</div>
@endif
<div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1>

View File

@ -13,11 +13,14 @@
<div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
@if($account && !$account->isPaid())
<div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div>
<div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div>
@elseif(isset($company) && !is_null($company))
<div>
<img src="{{ asset($company->present()->logo()) }}" class="h-14 mb-10" alt="{{ $company->present()->name() }} logo">
</div>
@endif
<div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1>

View File

@ -14,6 +14,7 @@
@csrf
<div class="grid grid-cols-12 gap-4 mt-10">
@if($company->client_registration_fields)
@foreach($company->client_registration_fields as $field)
@if($field['required'])
<div class="col-span-12 md:col-span-6">
@ -96,6 +97,7 @@
@endif
@endif
@endforeach
@endif
</div>
<div class="flex justify-between items-center mt-8">

View File

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

View File

@ -67,7 +67,7 @@
{{ ctrans('texts.invoice_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->date }}
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
</dd>
@endif
</div>

View File

@ -39,7 +39,7 @@
<input type="checkbox" class="form-checkbox mr-1"
name="send_logs" {{ old('send_logs' ? 'checked': '') }}>
<span>{{ ctrans('texts.send_fail_logs_to_our_server') }}</span>
<a class="button-link mt-1 block" href="https://www.invoiceninja.com/privacy-policy/">Read more
<a class="button-link mt-1 block" target="_blank" href="https://www.invoiceninja.com/privacy-policy/">Read more
about how we use this.</a>
</dd>
</div>

View File

@ -10,7 +10,12 @@
<div class="col-span-12 md:col-start-4 md:col-span-6 mt-4 md:mt-10">
<h1 class="text-center text-2xl font-semibold">Invoice Ninja Setup</h1>
<p class="text-sm text-center">{{ ctrans('texts.if_you_need_help') }}
<a href="https://www.invoiceninja.com/forums/forum/support/" class="button-link">{{ ctrans('texts.support_forum') }}</a>
<a
target="_blank"
href="https://www.invoiceninja.com/forums/forum/support/"
class="button-link underline">
{{ ctrans('texts.support_forum') }}
</a>
</p>
@if($errors->any())

View File

@ -103,6 +103,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf')->name('quote.download_invitation_key');
Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key');
Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
Route::get('pay/{invitation_key}', 'ClientPortal\InvitationController@payInvoice')->name('pay.invoice');
// Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this
});

View File

@ -0,0 +1,106 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Browser\ClientPortal\Gateways\Mollie;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\ClientPortal\Login;
class IDEALTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', '1bd651fb213ca0c9d66ae3c336dc77e8')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testSuccessfulPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'paid')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Completed');
});
}
public function testOpenPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'open')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Pending');
});
}
public function testFailedPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'failed')
->press('Continue')
->waitForText('Failed.');
});
}
public function testCancelledPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'canceled')
->press('Continue')
->waitForText('Cancelled.');
});
}
}

View File

@ -93,9 +93,9 @@ class CreditsTest extends TestCase
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01')
->assertDontSee('testing-number-01')
->assertSee('testing-number-02')
->assertDontSee('testing-number-03');
->assertSee('testing-number-03');
}
public function testShowingCreditsWithNullDueDate()
@ -122,6 +122,7 @@ class CreditsTest extends TestCase
'client_id' => $client->id,
'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT,
'due_date' => null,
]);
Credit::factory()->create([
@ -142,12 +143,21 @@ class CreditsTest extends TestCase
'status_id' => Credit::STATUS_SENT,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-04',
'due_date' => '',
'status_id' => Credit::STATUS_SENT,
]);
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01')
->assertSee('testing-number-02')
->assertDontSee('testing-number-03');
->assertSee('testing-number-03');
}
}