Merge pull request #6754 from turbo124/v5-stable

v5.3.18
This commit is contained in:
David Bomba 2021-10-01 18:27:19 +10:00 committed by GitHub
commit e5ddd4e2e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 625142 additions and 620560 deletions

3
.gitignore vendored
View File

@ -17,7 +17,6 @@ local_version.txt
/resources/assets/bower
/public/logo
/storage/*
.env.dusk.local
/public/vendors/*
*.log
@ -29,4 +28,4 @@ nbproject
.php_cs.cache
public/test.pdf
public/storage/test.pdf
/Modules
/Modules

View File

@ -1 +1 @@
5.3.17
5.3.18

View File

@ -56,7 +56,7 @@ class PostUpdate extends Command
exec('vendor/bin/composer install --no-dev -o', $output);
info(print_r($output,1));
info(print_r($output,1));
info("finished running composer install ");
try {
@ -75,9 +75,17 @@ class PostUpdate extends Command
info("view cleared");
try {
Artisan::call('queue:restart');
} catch (\Exception $e) {
info("I wasn't able to restart the queue.");
}
info("queue restarted");
VersionCheck::dispatch();
info("Sent for version check");
}
}

View File

@ -0,0 +1,101 @@
<?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\DataMapper;
class ClientRegistrationFields
{
public static function generate()
{
$data =
[
[
'key' => 'first_name',
'required' => true
],
[
'key' => 'last_name',
'required' => true
],
[
'key' => 'email',
'required' => true
],
[
'key' => 'phone',
'required' => true
],
[
'key' => 'password',
'required' => true
],
[
'key' => 'name',
'required' => false
],
[
'key' => 'website',
'required' => false
],
[
'key' => 'address1',
'required' => false
],
[
'key' => 'address2',
'required' => false
],
[
'key' => 'city',
'required' => false
],
[
'key' => 'state',
'required' => false
],
[
'key' => 'postal_code',
'required' => false
],
[
'key' => 'country_id',
'required' => false
],
[
'key' => 'custom_value1',
'required' => false
],
[
'key' => 'custom_value2',
'required' => false
],
[
'key' => 'custom_value3',
'required' => false
],
[
'key' => 'custom_value4',
'required' => false
],
[
'key' => 'public_notes',
'required' => false
],
[
'key' => 'vat_number',
'required' => false
],
];
return $data;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Factory;
use App\DataMapper\ClientRegistrationFields;
use App\DataMapper\CompanySettings;
use App\Libraries\MultiDB;
use App\Models\Company;
@ -35,7 +36,8 @@ class CompanyFactory
$company->db = config('database.default');
//$company->custom_fields = (object) ['invoice1' => '1', 'invoice2' => '2', 'client1'=>'3'];
$company->custom_fields = (object) [];
$company->client_registration_fields = ClientRegistrationFields::generate();
if(Ninja::isHosted())
$company->subdomain = MultiDB::randomSubdomainGenerator();
else

View File

@ -20,10 +20,10 @@ use Illuminate\Database\Eloquent\Builder;
class DocumentFilters extends QueryFilters
{
public function client_id(int $client_id) :Builder
{
return $this->builder->where('client_id', $client_id);
}
// public function client_id(int $client_id) :Builder
// {
// return $this->builder->where('client_id', $client_id);
// }
/**
* Filter based on search text.

View File

@ -746,6 +746,8 @@ class BaseController extends Controller
//pass referral code to front end
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$data['build'] = request()->has('build') ? request()->input('build') : '';
$data['login'] = request()->has('login') ? request()->input('login') : "false";
$data['user_agent'] = request()->server('HTTP_USER_AGENT');
$data['path'] = $this->setBuild();
@ -765,6 +767,9 @@ class BaseController extends Controller
if(request()->has('build')) {
$build = request()->input('build');
}
elseif(Ninja::isHosted()){
return 'main.dart.js';
}
switch ($build) {
case 'wasm':
@ -776,13 +781,10 @@ class BaseController extends Controller
case 'next':
return 'main.next.dart.js';
case 'profile':
return 'main.profile.dart.js';
return 'main.profile.dart.js';
default:
return 'main.foss.dart.js';
if(Ninja::isSelfHost())
return 'main.foss.dart.js';
return 'main.dart.js';
}
}

View File

@ -57,7 +57,12 @@ class DocumentController extends Controller
{
$document = Document::where('hash', $document_hash)->firstOrFail();
return Storage::disk($document->disk)->download($document->url, $document->name);
$headers = [];
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
return Storage::disk($document->disk)->download($document->url, $document->name, $headers);
}
public function downloadMultiple(DownloadMultipleDocumentsRequest $request)

View File

@ -16,6 +16,7 @@ use App\Events\Invoice\InvoiceWasViewed;
use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Payment;
@ -106,15 +107,12 @@ class InvitationController extends Controller
{
switch ($entity_string) {
case 'invoice':
$invitation->invoice->service()->markSent()->save();
event(new InvoiceWasViewed($invitation, $invitation->company, Ninja::eventVars()));
break;
case 'quote':
$invitation->quote->service()->markSent()->save();
event(new QuoteWasViewed($invitation, $invitation->company, Ninja::eventVars()));
break;
case 'credit':
$invitation->credit->service()->markSent()->save();
event(new CreditWasViewed($invitation, $invitation->company, Ninja::eventVars()));
break;
default:
@ -125,9 +123,43 @@ class InvitationController extends Controller
public function routerForDownload(string $entity, string $invitation_key)
{
if(Ninja::isHosted())
return $this->returnRawPdf($entity, $invitation_key);
return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf');
}
private function returnRawPdf(string $entity, string $invitation_key)
{
$key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
$invitation = $entity_obj::whereRaw('BINARY `key`= ?', [$invitation_key])
->with('contact.client')
->firstOrFail();
if(!$invitation)
return response()->json(["message" => "no record found"], 400);
$file_name = $invitation->{$entity}->numberFormatter().'.pdf';
nlog($file_name);
$file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
$headers = ['Content-Type' => 'application/pdf'];
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
return response()->streamDownload(function () use($file) {
echo $file;
}, $file_name, $headers);
}
public function routerForIframe(string $entity, string $client_hash, string $invitation_key)
{
}

View File

@ -500,7 +500,7 @@ class CompanyController extends BaseController
$account->delete();
if(Ninja::isHosted())
\Modules\Admin\Jobs\Account\NinjaDeletedAccount::dispatch($account_key);
\Modules\Admin\Jobs\Account\NinjaDeletedAccount::dispatch($account_key, $request->all());
LightLogs::create(new AccountDeleted())
->increment()

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Filters\DocumentFilters;
use App\Http\Requests\Document\DestroyDocumentRequest;
use App\Http\Requests\Document\EditDocumentRequest;
use App\Http\Requests\Document\ShowDocumentRequest;
use App\Http\Requests\Document\StoreDocumentRequest;
use App\Http\Requests\Document\UpdateDocumentRequest;
@ -114,9 +115,16 @@ class DocumentController extends BaseController
public function download(ShowDocumentRequest $request, Document $document)
{
$headers = [];
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
return response()->streamDownload(function () use ($document) {
echo file_get_contents($document->generateUrl());
}, basename($document->generateUrl()));
}, basename($document->generateUrl()), $headers);
}
/**
@ -126,7 +134,7 @@ class DocumentController extends BaseController
* @param Document $document
* @return Response
*/
public function edit(EditDocumentRegquest $request, Document $document)
public function edit(EditDocumentRequest $request, Document $document)
{
return $this->itemResponse($document);
}

View File

@ -114,7 +114,7 @@ class EmailController extends BaseController
public function send(SendEmailRequest $request)
{
$entity = $request->input('entity');
$entity_obj = $entity::find($request->input('entity_id'));
$entity_obj = $entity::withTrashed()->with('invitations')->find($request->input('entity_id'));
$subject = $request->has('subject') ? $request->input('subject') : '';
$body = $request->has('body') ? $request->input('body') : '';
$entity_string = strtolower(class_basename($entity_obj));

View File

@ -397,7 +397,7 @@ class InvoiceController extends BaseController
$invoice = $this->invoice_repo->save($request->all(), $invoice);
$invoice->service()->deletePdf();
$invoice->service()->triggeredActions($request)->deletePdf();
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));

View File

@ -24,6 +24,7 @@ use App\Models\CompanyToken;
use App\Utils\Ninja;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\App;
@ -231,10 +232,23 @@ class MigrationController extends BaseController
* @return \Illuminate\Http\JsonResponse|void
*/
public function startMigration(Request $request)
{
nlog("Starting Migration");
{
$companies = json_decode($request->companies);
// 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 (app()->environment() === 'local') {
nlog($request->all());
@ -250,19 +264,31 @@ class MigrationController extends BaseController
} finally {
// Controller logic here
foreach ($companies as $company) {
$is_valid = $request->file($company->company_index)->isValid();
foreach($companies as $company)
{
if (!$is_valid) {
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();
// Look for possible existing company (based on company keys).
$existing_company = Company::whereRaw('BINARY `company_key` = ?', [$company->company_key])->first();
$existing_company = Company::whereRaw('BINARY `company_key` = ?', [$company['company_key']])->first();
App::forgetInstance('translator');
$t = app('translator');
@ -291,7 +317,7 @@ class MigrationController extends BaseController
$checks = [
'existing_company' => $existing_company ? (bool)1 : false,
'force' => property_exists($company, 'force') ? (bool) $company->force : false,
'force' => array_key_exists('force', $company) ? (bool) $company['force'] : false,
];
// If there's existing company and ** no ** force is provided - skip migration.
@ -378,10 +404,10 @@ class MigrationController extends BaseController
]);
}
$migration_file = $request->file($company->company_index)
$migration_file = $request->file($company['company_index'])
->storeAs(
'migrations',
$request->file($company->company_index)->getClientOriginalName(),
$request->file($company['company_index'])->getClientOriginalName(),
'public'
);

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers;
use App\DataMapper\Analytics\EmailBounce;
use App\DataMapper\Analytics\Mail\EmailBounce;
use App\DataMapper\Analytics\Mail\EmailSpam;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
@ -74,7 +75,6 @@ class PostMarkController extends BaseController
if($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('postmark.secret'))
{
// nlog($request->all());
MultiDB::findAndSetDbByCompanyKey($request->input('Tag'));
@ -165,13 +165,13 @@ class PostMarkController extends BaseController
$this->invitation->email_status = 'bounced';
$this->invitation->save();
// $bounce = new EmailBounce(
// $request->input('Tag'),
// $request->input('From'),
// $request->input('MessageID')
// );
$bounce = new EmailBounce(
$request->input('Tag'),
$request->input('From'),
$request->input('MessageID')
);
// LightLogs::create($bounce)->batch();
LightLogs::create($bounce)->batch();
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company);
}

View File

@ -212,7 +212,10 @@ class PreviewController extends BaseController
}
$entity_obj = $repo->save($request->all(), $entity_obj);
$entity_obj->service()->fillDefaults()->save();
if(!$request->has('entity_id'))
$entity_obj->service()->fillDefaults()->save();
$entity_obj->load('client');
App::forgetInstance('translator');

View File

@ -391,7 +391,9 @@ class QuoteController extends BaseController
$quote = $this->quote_repo->save($request->all(), $quote);
$quote->service()->deletePdf();
$quote->service()
->triggeredActions($request)
->deletePdf();
event(new QuoteWasUpdated($quote, $quote->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));

View File

@ -30,6 +30,7 @@ class SubdomainController extends BaseController
'invoiceninja',
'cname',
'sandbox',
'stage',
];
public function __construct()

View File

@ -76,7 +76,7 @@ class SendingController extends Controller
}
Mail::to(config('ninja.contact.ninja_official_contact'))
->send(new SupportMessageSent($request->input('message'), $send_logs));
->send(new SupportMessageSent($request->all(), $send_logs));
return response()->json([
'success' => true,

View File

@ -47,6 +47,7 @@ class RecurringInvoicesTable extends Component
->with('client')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->where('is_deleted', false)
->paginate($this->per_page);
return render('components.livewire.recurring-invoices-table', [

View File

@ -44,6 +44,7 @@ class UpdateCompanyRequest extends Request
$rules['size_id'] = 'integer|nullable';
$rules['country_id'] = 'integer|nullable';
$rules['work_email'] = 'email|nullable';
// $rules['client_registration_fields'] = 'array';
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
$rules['portal_domain'] = 'sometimes|url';

View File

@ -82,7 +82,7 @@ class SendEmailRequest extends Request
$entity = $input['entity'];
/* Harvest the entity*/
$entity_obj = $entity::whereId($input['entity_id'])->company()->first();
$entity_obj = $entity::whereId($input['entity_id'])->withTrashed()->company()->first();
/* Check object, check user and company id is same as users, and check user can edit the object */
if ($entity_obj && ($company->id == $entity_obj->company_id) && auth()->user()->can('edit', $entity_obj)) {

View File

@ -794,6 +794,7 @@ class CompanyImport implements ShouldQueue
['clients' => 'client_id'],
['projects' => 'project_id'],
['vendors' => 'vendor_id'],
['invoices' => 'invoice_id'],
],
'expenses',
'number');

View File

@ -11,6 +11,7 @@
namespace App\Jobs\Company;
use App\DataMapper\ClientRegistrationFields;
use App\DataMapper\CompanySettings;
use App\Libraries\MultiDB;
use App\Models\Company;
@ -62,7 +63,8 @@ class CreateCompany
$company->subdomain = isset($this->request['subdomain']) ? $this->request['subdomain'] : '';
$company->custom_fields = new \stdClass;
$company->default_password_timeout = 1800000;
$company->client_registration_fields = ClientRegistrationFields::generate();
if(Ninja::isHosted())
$company->subdomain = MultiDB::randomSubdomainGenerator();
else

View File

@ -104,9 +104,6 @@ class CreateEntityPdf implements ShouldQueue
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->entity->client->getMergedSettings()));
/*This line of code hurts... it deletes ALL $entity PDFs... this causes a race condition when trying to send an email*/
// $this->entity->service()->deletePdf();
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->generate($this->invitation);
}

View File

@ -0,0 +1,207 @@
<?php
/**
* Entity Ninja (https://entityninja.com).
*
* @link https://github.com/entityninja/entityninja source repository
*
* @copyright Copyright (c) 2021. Entity Ninja LLC (https://entityninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Entity;
use App\Exceptions\FilePermissionsFailure;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Design;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoice;
use App\Models\RecurringInvoiceInvitation;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker as PdfMakerService;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter;
use App\Utils\Traits\Pdf\PdfMaker;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Storage;
class CreateRawPdf implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, MakesInvoiceHtml, PdfMaker, MakesHash;
public $entity;
public $company;
public $contact;
public $invitation;
public $entity_string = '';
/**
* Create a new job instance.
*
* @param $invitation
*/
public function __construct($invitation, $db)
{
MultiDB::setDb($db);
$this->invitation = $invitation;
if ($invitation instanceof InvoiceInvitation) {
$this->entity = $invitation->invoice;
$this->entity_string = 'invoice';
} elseif ($invitation instanceof QuoteInvitation) {
$this->entity = $invitation->quote;
$this->entity_string = 'quote';
} elseif ($invitation instanceof CreditInvitation) {
$this->entity = $invitation->credit;
$this->entity_string = 'credit';
} elseif ($invitation instanceof RecurringInvoiceInvitation) {
$this->entity = $invitation->recurring_invoice;
$this->entity_string = 'recurring_invoice';
}
$this->company = $invitation->company;
$this->contact = $invitation->contact;
}
public function handle()
{
/* Forget the singleton*/
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->contact->preferredLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->entity->client->getMergedSettings()));
$entity_design_id = '';
if ($this->entity instanceof Invoice) {
$path = $this->entity->client->invoice_filepath($this->invitation);
$entity_design_id = 'invoice_design_id';
} elseif ($this->entity instanceof Quote) {
$path = $this->entity->client->quote_filepath($this->invitation);
$entity_design_id = 'quote_design_id';
} elseif ($this->entity instanceof Credit) {
$path = $this->entity->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);
$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));
$design = Design::find($entity_design_id);
/* Catch all in case migration doesn't pass back a valid design */
if(!$design)
$design = Design::find(2);
$html = new HtmlEngine($this->invitation);
if ($design->is_custom) {
$options = [
'custom_partials' => json_decode(json_encode($design->design), true)
];
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else {
$template = new PdfMakerDesign(strtolower($design->name));
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => $this->entity->client,
'entity' => $this->entity,
'pdf_variables' => (array) $this->entity->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'),
],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);
$maker
->design($template)
->build();
$pdf = null;
try {
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$finfo = new \finfo(FILEINFO_MIME);
//fallback in case hosted PDF fails.
if($finfo->buffer($pdf) != 'application/pdf; charset=binary')
{
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
}
else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
if (config('ninja.log_pdf_html')) {
info($maker->getCompiledHTML());
}
if ($pdf)
return $pdf;
throw new FilePermissionsFailure("Unable to generate the raw PDF");
}
public function failed($e)
{
}
}

View File

@ -132,7 +132,6 @@ class SendRecurring implements ShouldQueue
});
if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $invoice->auto_bill_enabled) {
nlog("attempting to autobill {$invoice->number}");
$invoice->service()->autoBill()->save();

View File

@ -13,6 +13,7 @@ namespace App\Jobs\Util;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Models\Client as ClientModel;
use App\Models\SystemLog;
use App\Models\Webhook;
use App\Transformers\ArraySerializer;
@ -147,11 +148,12 @@ class WebhookHandler implements ShouldQueue
private function resolveClient()
{
if($this->entity->client()->exists()){
//make sure it isn't an instance of the Client Model
if((!$this->entity instanceof ClientModel) && $this->entity->client()->exists()){
return $this->entity->client;
}
return $this->company->clients->first();
return $this->company->clients()->first();
}
public function failed($exception)

View File

@ -41,6 +41,8 @@ class CreditViewedActivity implements ShouldQueue
{
MultiDB::setDb($event->company->db);
$event->invitation->credit->service()->markSent()->save();
$fields = new stdClass;
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->invitation->user_id;

View File

@ -45,6 +45,8 @@ class InvoiceViewedActivity implements ShouldQueue
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->invitation->invoice->user_id;
$event->invitation->invoice->service()->markSent()->save();
$fields->user_id = $user_id;
$fields->company_id = $event->invitation->company_id;
$fields->activity_type_id = Activity::VIEW_INVOICE;

View File

@ -41,6 +41,8 @@ class QuoteViewedActivity implements ShouldQueue
{
MultiDB::setDb($event->company->db);
$event->invitation->quote->service()->markSent()->save();
$fields = new stdClass;
$fields->user_id = $event->invitation->quote->user_id;

View File

@ -13,13 +13,13 @@ class SupportMessageSent extends Mailable
{
// use Queueable, SerializesModels;
public $support_message;
public $data;
public $send_logs;
public function __construct($support_message, $send_logs)
public function __construct(array $data, $send_logs)
{
$this->support_message = $support_message;
$this->data = $data;
$this->send_logs = $send_logs;
}
@ -63,17 +63,19 @@ class SupportMessageSent extends Mailable
$user = auth()->user();
$db = str_replace("db-ninja-", "", $company->db);
$is_large = $company->is_large ? "L" : "S";
$platform = array_key_exists('platform', $this->data) ? $this->data['platform'] : "U";
$migrated = strlen($company->company_key) == 32 ? "M" : "";
if(Ninja::isHosted())
$subject = "{$priority}Hosted-{$db}-{$is_large} :: {$plan} :: ".date('M jS, g:ia');
$subject = "{$priority}Hosted-{$db}-{$is_large}{$platform}{$migrated} :: {$plan} :: ".date('M jS, g:ia');
else
$subject = "{$priority}Self Hosted :: {$plan} :: ".date('M jS, g:ia');
$subject = "{$priority}Self Hosted :: {$plan}{$platform} :: ".date('M jS, g:ia');
return $this->from(config('mail.from.address'), $user->present()->name())
->replyTo($user->email, $user->present()->name())
->subject($subject)
->view('email.support.message', [
'support_message' => $this->support_message,
'support_message' => $this->data['support_message'],
'system_info' => $system_info,
'laravel_log' => $log_lines,
'logo' => $company->present()->logo(),

View File

@ -407,7 +407,7 @@ class Account extends BaseModel
}
}
catch(\Exception $e){
\Sentry\captureMessage("I encountered an error with email quotas - defaulting to SEND");
\Sentry\captureMessage("I encountered an error with email quotas for account {$this->key} - defaulting to SEND");
}
return false;

View File

@ -96,6 +96,7 @@ class Company extends BaseModel
'show_task_end_date',
'use_comma_as_decimal_place',
'report_include_drafts',
'client_registration_fields',
];
protected $hidden = [
@ -111,6 +112,7 @@ class Company extends BaseModel
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'client_registration_fields' => 'array',
];
protected $with = [
@ -485,7 +487,7 @@ class Company extends BaseModel
{
if (Ninja::isHosted()) {
if($this->portal_mode == 'domain')
if($this->portal_mode == 'domain' && strlen($this->portal_domain) > 3)
return $this->portal_domain;
return "https://{$this->subdomain}." . config('ninja.app_domain');

View File

@ -85,12 +85,14 @@ class Gateway extends StaticModel
return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]];//eWay
break;
case 11:
return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => false]];//Payfast
return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]];//Payfast
break;
case 7:
return [
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], // Mollie
GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true],
GatewayType::KBC => ['refund' => false, 'token_billing' => false],
GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
];
case 15:
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal

View File

@ -25,6 +25,8 @@ class GatewayType extends StaticModel
const APPLE_PAY = 8;
const SEPA = 9;
const CREDIT = 10;
const KBC = 11;
const BANCONTACT = 12;
public function gateway()
{
@ -41,32 +43,27 @@ class GatewayType extends StaticModel
switch ($type) {
case self::CREDIT_CARD:
return ctrans('texts.credit_card');
break;
case self::BANK_TRANSFER:
return ctrans('texts.bank_transfer');
break;
case self::PAYPAL:
return ctrans('texts.paypal');
break;
case self::CRYPTO:
return ctrans('texts.crypto');
break;
case self::CUSTOM:
return ctrans('texts.custom');
break;
case self::ALIPAY:
return ctrans('texts.alipay');
break;
case self::SOFORT:
return ctrans('texts.sofort');
break;
case self::APPLE_PAY:
return ctrans('texts.apple_pay');
break;
case self::SEPA:
return ctrans('texts.sepa');
break;
case self::KBC:
return ctrans('texts.kbc_cbc');
case self::BANCONTACT:
return ctrans('texts.bancontact');
default:
return 'Undefined.';
break;

View File

@ -43,6 +43,8 @@ class PaymentType extends StaticModel
const GOCARDLESS = 30;
const CRYPTO = 31;
const MOLLIE_BANK_TRANSFER = 34;
const KBC = 35;
const BANCONTACT = 36;
public static function parseCardType($cardName)
{

View File

@ -164,4 +164,77 @@ class RecurringExpense extends BaseModel
}
}
public function recurringDates()
{
/* Return early if nothing to send back! */
if ($this->status_id == RecurringInvoice::STATUS_COMPLETED ||
$this->remaining_cycles == 0 ||
!$this->next_send_date) {
return [];
}
/* Endless - lets send 10 back*/
$iterations = $this->remaining_cycles;
if ($this->remaining_cycles == -1) {
$iterations = 10;
}
$data = [];
if (!Carbon::parse($this->next_send_date)) {
return $data;
}
$next_send_date = Carbon::parse($this->next_send_date)->copy();
for ($x=0; $x<$iterations; $x++) {
// we don't add the days... we calc the day of the month!!
$this->nextDateByFrequency($next_send_date);
$data[] = [
'send_date' => $next_send_date->format('Y-m-d'),
];
}
return $data;
}
public function nextDateByFrequency($date)
{
$offset = $this->client->timezone_offset();
switch ($this->frequency_id) {
case RecurringInvoice::FREQUENCY_DAILY:
return $date->startOfDay()->addDay()->addSeconds($offset);
case RecurringInvoice::FREQUENCY_WEEKLY:
return $date->startOfDay()->addWeek()->addSeconds($offset);
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
return $date->startOfDay()->addWeeks(2)->addSeconds($offset);
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
return $date->startOfDay()->addWeeks(4)->addSeconds($offset);
case RecurringInvoice::FREQUENCY_MONTHLY:
return $date->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
return $date->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset);
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
return $date->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
return $date->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset);
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
return $date->addMonthsNoOverflow(6)->addSeconds($offset);
case RecurringInvoice::FREQUENCY_ANNUALLY:
return $date->startOfDay()->addYear()->addSeconds($offset);
case RecurringInvoice::FREQUENCY_TWO_YEARS:
return $date->startOfDay()->addYears(2)->addSeconds($offset);
case RecurringInvoice::FREQUENCY_THREE_YEARS:
return $date->startOfDay()->addYears(3)->addSeconds($offset);
default:
return null;
}
}
}

View File

@ -287,7 +287,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->each(function ($invoice) use ($fee_total) {
if (collect($invoice->line_items)->contains('type_id', '3')) {
$invoice->service()->toggleFeesPaid()->deletePdf()->save();
$invoice->service()->toggleFeesPaid()->save();
$invoice->client->service()->updateBalance($fee_total)->save();
$invoice->ledger()->updateInvoiceBalance($fee_total, "Gateway fee adjustment for invoice {$invoice->number}");
}

View File

@ -119,6 +119,15 @@ class BraintreePaymentDriver extends BaseDriver
]);
if ($result->success) {
$address = $this->gateway->address()->create([
'customerId' => $result->customer->id,
'firstName' => $this->client->present()->name,
'streetAddress' => $this->client->address1,
'postalCode' => $this->client->postal_code,
'countryCodeAlpha2' => $this->client->country ? $this->client->country->iso_3166_2 : '',
]);
return $result->customer;
}
}

View File

@ -0,0 +1,216 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\Request;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
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 Bancontact implements MethodInterface
{
protected MolliePaymentDriver $mollie;
public function __construct(MolliePaymentDriver $mollie)
{
$this->mollie = $mollie;
$this->mollie->init();
}
/**
* Show the authorization page for Bancontact.
*
* @param array $data
* @return View
*/
public function authorizeView(array $data): View
{
return render('gateways.mollie.bancontact.authorize', $data);
}
/**
* Handle the authorization for Bancontact.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Show the payment page for Bancontact.
*
* @param array $data
* @return Redirector|RedirectResponse
*/
public function paymentView(array $data)
{
$this->mollie->payment_hash
->withData('gateway_type_id', GatewayType::BANCONTACT)
->withData('client_id', $this->mollie->client->id);
try {
$payment = $this->mollie->gateway->payments->create([
'method' => 'bancontact',
'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::BANCONTACT,
]),
'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 KBC.
*
* @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 Bancontact.
*
* @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::BANCONTACT,
'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total,
'payment_type' => PaymentType::BANCONTACT,
'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 Bancontact.
*
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse
{
return $this->processSuccessfulPayment($payment, 'open');
}
}

View File

@ -0,0 +1,199 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\Request;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
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 KBC implements MethodInterface
{
protected MolliePaymentDriver $mollie;
public function __construct(MolliePaymentDriver $mollie)
{
$this->mollie = $mollie;
$this->mollie->init();
}
/**
* Show the authorization page for KBC.
*
* @param array $data
* @return View
*/
public function authorizeView(array $data): View
{
return render('gateways.mollie.kbc.authorize', $data);
}
/**
* Handle the authorization for KBC.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Show the payment page for KBC.
*
* @param array $data
* @return Redirector|RedirectResponse
*/
public function paymentView(array $data)
{
$this->mollie->payment_hash
->withData('gateway_type_id', GatewayType::KBC)
->withData('client_id', $this->mollie->client->id);
try {
$payment = $this->mollie->gateway->payments->create([
'method' => 'kbc',
'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::KBC,
]),
'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 KBC.
*
* @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 === '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 KBC.
*
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse
{
$data = [
'gateway_type_id' => GatewayType::KBC,
'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total,
'payment_type' => PaymentType::KBC,
'transaction_reference' => $payment->id,
];
$payment_record = $this->mollie->createPayment(
$data,
$payment->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)]);
}
}

View File

@ -24,8 +24,10 @@ use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Mollie\Bancontact;
use App\PaymentDrivers\Mollie\BankTransfer;
use App\PaymentDrivers\Mollie\CreditCard;
use App\PaymentDrivers\Mollie\KBC;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator;
use Mollie\Api\Exceptions\ApiException;
@ -65,7 +67,9 @@ class MolliePaymentDriver extends BaseDriver
*/
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
GatewayType::BANCONTACT => Bancontact::class,
GatewayType::BANK_TRANSFER => BankTransfer::class,
GatewayType::KBC => KBC::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
@ -86,7 +90,9 @@ class MolliePaymentDriver extends BaseDriver
$types = [];
$types[] = GatewayType::CREDIT_CARD;
$types[] = GatewayType::BANCONTACT;
$types[] = GatewayType::BANK_TRANSFER;
$types[] = GatewayType::KBC;
return $types;
}

View File

@ -77,36 +77,47 @@ class Token
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$amount = round(($amount * pow(10, $this->payfast->client->currency()->precision)),0);
// $header =[
// 'merchant-id' => $this->payfast->company_gateway->getConfigField('merchantId'),
// 'timestamp' => now()->format('c'),
// 'version' => 'v1',
// ];
$header =[
'merchant-id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'version' => 'v1',
'timestamp' => now()->format('c'),
];
// $body = [
// 'amount' => $amount,
// 'item_name' => 'purchase',
// 'item_description' => ctrans('texts.invoices') . ': ' . collect($payment_hash->invoices())->pluck('invoice_number'),
// // 'm_payment_id' => $payment_hash->hash,
// ];
$body = [
'amount' => $amount,
'item_name' => 'purchase',
'item_description' => ctrans('texts.invoices') . ': ' . collect($payment_hash->invoices())->pluck('invoice_number'),
'm_payment_id' => $payment_hash->hash,
];
nlog(array_merge($body, $header));
// $header['signature'] = md5( $this->generate_parameter_string(array_merge($header, $body), false) );
// $result = $this->send($header, $body, $cgt->token);
$api = new \PayFast\PayFastApi(
[
'merchantId' => $this->payfast->company_gateway->getConfigField('merchantId'),
'passPhrase' => $this->payfast->company_gateway->getConfigField('passPhrase'),
'testMode' => $this->payfast->company_gateway->getConfigField('testMode')
]
);
$header['signature'] = $this->payfast->generateTokenSignature(array_merge($body, $header));
$adhocArray = $api
->subscriptions
->adhoc($cgt->token, ['amount' => $amount, 'item_name' => 'purchase']);
nlog($header['signature']);
$result = $this->send($header, $body, $cgt->token);
nlog($result);
// $api = new \PayFast\PayFastApi(
// [
// 'merchantId' => $this->payfast->company_gateway->getConfigField('merchantId'),
// 'passPhrase' => $this->payfast->company_gateway->getConfigField('passPhrase'),
// 'testMode' => $this->payfast->company_gateway->getConfigField('testMode')
// ]
// );
// $adhocArray = $api
// ->subscriptions
// ->adhoc($cgt->token, ['amount' => $amount, 'item_name' => 'purchase']);
// nlog($adhocArray);
nlog($adhocArray);
// /*Refactor and push to BaseDriver*/
// if ($data['response'] != null && $data['response']->getMessages()->getResultCode() == 'Ok') {
@ -151,8 +162,8 @@ class Token
protected function generate_parameter_string( $api_data, $sort_data_before_merge = true, $skip_empty_values = true ) {
// if sorting is required the passphrase should be added in before sort.
if ( ! empty( $this->payfast->company_gateway->getConfigField('passPhrase') ) && $sort_data_before_merge )
$api_data['passphrase'] = $this->payfast->company_gateway->getConfigField('passPhrase');
if ( ! empty( $this->payfast->company_gateway->getConfigField('passphrase') ) && $sort_data_before_merge )
$api_data['passphrase'] = $this->payfast->company_gateway->getConfigField('passphrase');
if ( $sort_data_before_merge ) {
ksort( $api_data );
@ -175,7 +186,7 @@ class Token
if ( $sort_data_before_merge ) {
$parameter_string = rtrim( $parameter_string, '&' );
} elseif ( ! empty( $this->pass_phrase ) ) {
$parameter_string .= 'passphrase=' . urlencode( $this->payfast->company_gateway->getConfigField('passPhrase') );
$parameter_string .= 'passphrase=' . urlencode( $this->payfast->company_gateway->getConfigField('passphrase') );
} else {
$parameter_string = rtrim( $parameter_string, '&' );
}
@ -199,6 +210,8 @@ class Token
}
}
nlog(http_build_query($fields));
return md5(http_build_query($fields));
}

View File

@ -28,7 +28,7 @@ class PayFastPaymentDriver extends BaseDriver
public $refundable = false; //does this gateway support refunds?
public $token_billing = false; //does this gateway support token billing?
public $token_billing = true; //does this gateway support token billing?
public $can_authorise_credit_card = true; //does this gateway support authorizations?
@ -72,7 +72,7 @@ class PayFastPaymentDriver extends BaseDriver
[
'merchantId' => $this->company_gateway->getConfigField('merchantId'),
'merchantKey' => $this->company_gateway->getConfigField('merchantKey'),
'passPhrase' => $this->company_gateway->getConfigField('passPhrase'),
'passPhrase' => $this->company_gateway->getConfigField('passphrase'),
'testMode' => $this->company_gateway->getConfigField('testMode')
]
);
@ -123,7 +123,39 @@ class PayFastPaymentDriver extends BaseDriver
return (new Token($this))->tokenBilling($cgt, $payment_hash);
}
public function generateSignature($data)
public function generateTokenSignature($data)
{
$fields = [];
$keys = [
'merchant-id',
'version',
'timestamp',
'amount',
'item_name',
'item_description',
'itn',
'm_payment_id',
'cc_css',
'split_payment'
];
foreach($keys as $key)
{
if (!empty($data[$key])) {
$fields[$key] = $data[$key];
}
}
if($this->company_gateway->getConfigField('passphrase'))
$fields['passphrase'] = $this->company_gateway->getConfigField('passphrase');
nlog(http_build_query($fields));
return md5(http_build_query($fields));
}
public function generateSignature($data)
{
$fields = array();

View File

@ -22,7 +22,9 @@ use App\Jobs\Util\SystemLogger;
use App\Mail\Gateways\ACHVerificationNotification;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
@ -145,6 +147,62 @@ class ACH
return render('gateways.stripe.ach.pay', $data);
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))
->withTrashed()
->first();
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}";
} else {
$description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
}
$this->stripe->init();
$response = null;
try {
$state = [
'gateway_type_id' => GatewayType::BANK_TRANSFER,
'amount' => $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'currency' => $this->stripe->client->getCurrencyCode(),
'customer' => $cgt->gateway_customer_reference,
'source' => $cgt->token,
];
$state['charge'] = \Stripe\Charge::create([
'amount' => $state['amount'],
'currency' => $state['currency'],
'customer' => $state['customer'],
'source' => $state['source'],
'description' => $description,
], $this->stripe->stripe_connect_auth);
$payment_hash->data = array_merge((array)$payment_hash->data, $state);
$payment_hash->save();
if ($state['charge']->status === 'pending' && is_null($state['charge']->failure_message)) {
return $this->processPendingPayment($state, false);
}
return $this->processUnsuccessfulPayment($state);
} catch (Exception $e) {
if ($e instanceof CardException) {
return redirect()->route('client.payment_methods.verification', ['payment_method' => $source->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
}
throw new PaymentFailed($e->getMessage(), $e->getCode());
}
}
public function paymentResponse($request)
{
@ -201,7 +259,7 @@ class ACH
}
}
public function processPendingPayment($state)
public function processPendingPayment($state, $client_present = true)
{
$this->stripe->init();
@ -224,6 +282,9 @@ class ACH
$this->stripe->client->company,
);
if(!$client_present)
return $payment;
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
}

View File

@ -15,11 +15,13 @@ namespace App\PaymentDrivers\Stripe;
use App\Events\Payment\PaymentWasCreated;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use App\PaymentDrivers\Stripe\ACH;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Stripe\Exception\ApiConnectionException;
@ -51,6 +53,9 @@ class Charge
*/
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
if($cgt->gateway_type_id == GatewayType::BANK_TRANSFER)
return (new ACH($this->stripe))->tokenBilling($cgt, $payment_hash);
nlog(" DB = ".$this->stripe->client->company->db);
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();

View File

@ -314,7 +314,9 @@ class StripePaymentDriver extends BaseDriver
$this->init();
$client_gateway_token = ClientGatewayToken::whereClientId($this->client->id)->whereCompanyGatewayId($this->company_gateway->id)->first();
$client_gateway_token = ClientGatewayToken::whereClientId($this->client->id)
->whereCompanyGatewayId($this->company_gateway->id)
->first();
//Search by customer reference
if ($client_gateway_token && $client_gateway_token->gateway_customer_reference) {

View File

@ -80,10 +80,10 @@ class ClientContactRepository extends BaseRepository
});
//need to reload here to shake off stale contacts
$client->load('contacts');
$client->fresh();
//always made sure we have one blank contact to maintain state
if ($client->contacts->count() == 0) {
if ($client->contacts()->count() == 0) {
$new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
$new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40);

View File

@ -24,6 +24,7 @@ use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait;
@ -110,6 +111,7 @@ class Statement
DB::rollBack();
}
return $pdf;
}

View File

@ -47,6 +47,8 @@ class InvoiceService
*/
public function markPaid()
{
$this->removeUnpaidGatewayFees();
$this->invoice = (new MarkPaid($this->invoice))->run();
return $this;

View File

@ -605,6 +605,16 @@ class Design extends BaseDesign
public function tableTotals(): array
{
if ($this->type === self::STATEMENT) {
return [
['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [
['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: flex; align-items: flex-start;'], 'elements' => [
['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']],
]],
]],
];
}
$_variables = array_key_exists('variables', $this->context)
? $this->context['variables']
: ['values' => ['$entity.public_notes' => $this->entity->public_notes, '$entity.terms' => $this->entity->terms, '$entity_footer' => $this->entity->footer], 'labels' => []];
@ -626,7 +636,7 @@ class Design extends BaseDesign
['element' => 'div', 'properties' => ['class' => 'totals-table-right-side', 'dir' => '$dir'], 'elements' => []],
];
if ($this->type == self::DELIVERY_NOTE || $this->type == self::STATEMENT) {
if ($this->type == self::DELIVERY_NOTE) {
return $elements;
}

View File

@ -217,6 +217,13 @@ trait DesignHelpers
public function sharedFooterElements()
{
// We want to show headers for statements, no exceptions.
$statements = "
document.querySelectorAll('#statement-invoice-table > thead > tr > th, #statement-payment-table > thead > tr > th, #statement-aging-table > thead > tr > th').forEach(t => {
t.hidden = false;
});
";
// Unminified version, just for the reference.
// By default all table headers are hidden with HTML `hidden` property.
// This will check for table data values & if they're not empty it will remove hidden from the column itself.
@ -259,6 +266,7 @@ document.addEventListener('DOMContentLoaded', function() {
$html_decode = 'document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll(`[data-state="encoded-html"]`).forEach(e=>e.innerHTML=e.innerText)},!1);';
return ['element' => 'div', 'elements' => [
['element' => 'script', 'content' => $statements],
['element' => 'script', 'content' => $javascript],
['element' => 'script', 'content' => $html_decode],
]];

View File

@ -45,6 +45,10 @@ class TriggeredActions extends AbstractService
$this->quote = $this->quote->service()->markSent()->save();
}
if ($this->request->has('convert') && $this->request->input('convert') == 'true') {
$this->quote = $this->quote->service()->convert()->save();
}
return $this->quote;
}

View File

@ -27,13 +27,27 @@ class InvoiceHistoryTransformer extends EntityTransformer
'activity',
];
public function transform(Backup $backup)
public function transform(?Backup $backup)
{
if(!$backup){
return [
'id' => '',
'activity_id' => '',
'json_backup' => (string) '',
'html_backup' => (string) '',
'amount' => (float) 0,
'created_at' => (int) 0,
'updated_at' => (int) 0,
];
}
return [
'id' => $this->encodePrimaryKey($backup->id),
'activity_id' => $this->encodePrimaryKey($backup->activity_id),
'json_backup' => (string) $backup->json_backup ?: '',
'html_backup' => (string) $backup->html_backup ?: '',
'json_backup' => (string) '',
'html_backup' => (string) '',
'amount' => (float) $backup->amount,
'created_at' => (int) $backup->created_at,
'updated_at' => (int) $backup->updated_at,

View File

@ -49,7 +49,7 @@ class RecurringExpenseTransformer extends EntityTransformer
*/
public function transform(RecurringExpense $recurring_expense)
{
return [
$data = [
'id' => $this->encodePrimaryKey($recurring_expense->id),
'user_id' => $this->encodePrimaryKey($recurring_expense->user_id),
'assigned_user_id' => $this->encodePrimaryKey($recurring_expense->assigned_user_id),
@ -101,6 +101,12 @@ class RecurringExpenseTransformer extends EntityTransformer
'remaining_cycles' => (int) $recurring_expense->remaining_cycles,
'last_sent_date' => $recurring_expense->last_sent_date ?: '',
'next_send_date' => $recurring_expense->next_send_date ?: '',
'recurring_dates' => (array) [],
];
if(request()->has('show_dates') && request()->query('show_dates') == 'true')
$data['recurring_dates'] = (array) $recurring_expense->recurringDates();
return $data;
}
}

View File

@ -127,7 +127,7 @@ class RecurringInvoiceTransformer extends EntityTransformer
'due_date_days' => (string) $invoice->due_date_days ?: '',
'paid_to_date' => (float) $invoice->paid_to_date,
'subscription_id' => (string)$this->encodePrimaryKey($invoice->subscription_id),
'recurring_dates' => (array) [],
];

View File

@ -158,6 +158,7 @@ class HtmlEngine
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.view_quote')];
$data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link'];
$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')];
@ -171,6 +172,7 @@ class HtmlEngine
$data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_credit').'</a>', 'label' => ctrans('texts.view_credit')];
$data['$viewButton'] = &$data['$view_link'];
$data['$view_button'] = &$data['$view_link'];
$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')];

View File

@ -48,7 +48,7 @@ trait Inviteable
$entity_type = Str::snake(class_basename($this->entityType()));
if(Ninja::isHosted()){
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
$domain = $this->company->domain();
}
else
$domain = config('ninja.app_url');
@ -75,7 +75,7 @@ trait Inviteable
{
if(Ninja::isHosted())
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
$domain = $this->company->domain();
else
$domain = config('ninja.app_url');

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.17',
'app_tag' => '5.3.17',
'app_version' => '5.3.18',
'app_tag' => '5.3.18',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,26 @@
<?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 AddKbcToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$type = new PaymentType();
$type->id = 35;
$type->name = 'KBC/CBC';
$type->gateway_type_id = GatewayType::KBC;
$type->save();
}
}

View File

@ -0,0 +1,26 @@
<?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 AddBancontactToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$type = new PaymentType();
$type->id = 36;
$type->name = 'Bancontact';
$type->gateway_type_id = GatewayType::BANCONTACT;
$type->save();
}
}

View File

@ -0,0 +1,66 @@
<?php
use App\DataMapper\ClientRegistrationFields;
use App\Models\Company;
use App\Models\Currency;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddRequiredClientRegistrationFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Blueprint $table) {
$table->mediumText('client_registration_fields')->nullable();
});
Company::all()->each(function ($company){
$company->update(['client_registration_fields' => ClientRegistrationFields::generate()]);
});
Model::unguard();
$currencies = [
['id' => 111, 'name' => 'Cuban Peso','code' => 'CUP', 'symbol' => '₱', 'precision' => '2','thousand_separator' => ',','decimal_separator' => '.'],
];
foreach ($currencies as $currency) {
$record = Currency::whereCode($currency['code'])->first();
if ($record) {
$record->name = $currency['name'];
$record->symbol = $currency['symbol'];
$record->precision = $currency['precision'];
$record->thousand_separator = $currency['thousand_separator'];
$record->decimal_separator = $currency['decimal_separator'];
if (isset($currency['swap_currency_symbol'])) {
$record->swap_currency_symbol = $currency['swap_currency_symbol'];
}
$record->save();
} else {
Currency::create($currency);
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"/": "d54a2d8f5df9a52b1936136260e327b3",
"/": "046e0fcccb3cc2bf087c3ca7100aab14",
"assets/NOTICES": "9eb7e2eb2888ea5bae5f536720db37cd",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
@ -34,7 +34,7 @@ const RESOURCES = {
"favicon.ico": "51636d3a390451561744c42188ccd628",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"main.dart.js": "d0bf26e6fb4226713654b97cdfef6a53"
"main.dart.js": "cad794ed29ee69bb27294af52f923f50"
};
// The application shell files that are downloaded before a service worker can

244612
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

241535
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

246369
public/main.last.dart.js vendored

File diff suppressed because one or more lines are too long

251530
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

239614
public/main.wasm.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=08bae341ed680d6ba544",
"/css/app.css": "/css/app.css?id=ad5ff55c9ef56ede1726",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",

View File

@ -4314,6 +4314,8 @@ $LANG = array(
'generic_gateway_error' => 'Gateway configuration error. Please check your credentials.',
'my_documents' => 'My documents',
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
'kbc_cbc' => 'KBC/CBC',
'bancontact' => 'Bancontact',
);
return $LANG;

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}" data-user-agent="{{ $user_agent }}">
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}" data-user-agent="{{ $user_agent }}" data-login="{{ $login }}">
<head>
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Version: {{ config('ninja.app_version') }} -->

View File

@ -73,6 +73,7 @@
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.state')])
<select class="input w-full" id="billing-region">
<option disabled selected></option>
@foreach(\App\DataProviders\USStates::get() as $code => $state)
<option value="{{ $code }}">{{ $state }} ({{ $code }})</option>
@endforeach

View File

@ -10,15 +10,8 @@
<style>*, *::after, *::before {
box-sizing: border-box
}
html {
background-color: #FFF;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
}
#payment-form {
width: 31.5rem;
margin: 0 auto
width: 100%;
}
iframe {
@ -164,7 +157,7 @@
@component('portal.ninja2020.components.general.card-element-single')
<div id="checkout--container">
<form id="payment-form" method="POST" action="#">
<form class="xl:flex xl:justify-center" id="payment-form" method="POST" action="#">
<div class="one-liner">
<div class="card-frame">
<!-- form will be added here -->

View File

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

View File

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

View File

@ -43,7 +43,8 @@
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.country')])
<select name="countries" id="country" class="form-select input w-full" required>
<select name="countries" id="country" class="form-select input w-full">
<option disabled selected></option>
@foreach($countries as $country)
<option value="{{ $country->iso_3166_2 }}">{{ $country->iso_3166_2 }} ({{ $country->name }})</option>
@endforeach
@ -52,6 +53,7 @@
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.currency')])
<select name="currencies" id="currency" class="form-select input w-full">
<option disabled selected></option>
@foreach($currencies as $currency)
<option value="{{ $currency->code }}">{{ $currency->code }} ({{ $currency->name }})</option>
@endforeach

View File

@ -14,7 +14,7 @@
{{ ctrans('texts.first_name') }}
</dt>
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input type="text" class="input w-full" name="first_name" value="{{ old('first_name') }}">
<input type="text" class="input w-full" name="first_name" value="{{ old('first_name') }}" required>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:flex sm:items-center">
@ -22,7 +22,7 @@
{{ ctrans('texts.last_name') }}
</dt>
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input type="text" class="input w-full" name="last_name" value="{{ old('last_name') }}">
<input type="text" class="input w-full" name="last_name" value="{{ old('last_name') }}"required>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:flex sm:items-center">
@ -30,7 +30,7 @@
{{ ctrans('texts.email') }}
</dt>
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input type="email" class="input w-full" name="email" value="{{ old('email') }}">
<input type="email" class="input w-full" name="email" value="{{ old('email') }}"required>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:flex sm:items-center">
@ -38,7 +38,7 @@
{{ ctrans('texts.password') }}
</dt>
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input type="password" class="input w-full" name="password">
<input type="password" class="input w-full" name="password"required>
</dd>
</div>
</dl>

View File

@ -53,7 +53,7 @@
<div class="alert py-2 bg-white" id="test-pdf-response"></div>
</dd>
<a target="_blank" class="block text-sm text-gray-900 leading-5 underline"
href="https://invoiceninja.github.io/selfhost.html#phantom-js">
href="https://invoiceninja.github.io/docs/self-host-troubleshooting/#pdf-conversion-issues">
{{ ctrans('texts.setup_phantomjs_note') }}
</a>
</div>

2
storage/app/public/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,102 @@
<?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 BancontactTest 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('Bancontact')
->waitForText('Test profile')
->radio('final_state', 'paid')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Completed');
});
}
public function testOpenPayments(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Bancontact')
->waitForText('Test profile')
->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('Bancontact')
->waitForText('Test profile')
->radio('final_state', 'failed')
->press('Continue')
->waitForText('Failed.');
});
}
public function testCancelledTest(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Bancontact')
->waitForText('Test profile')
->radio('final_state', 'canceled')
->press('Continue')
->waitForText('Cancelled.');
});
}
}

View File

@ -0,0 +1,89 @@
<?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 KBCTest 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('Undefined.')
->waitForText('Test profile')
->press('CBC')
->radio('final_state', 'paid')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Completed');
});
}
public function testFailedPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Undefined.')
->waitForText('Test profile')
->press('CBC')
->radio('final_state', 'failed')
->press('Continue')
->waitForText('Failed.');
});
}
public function testCancelledTest(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Undefined.')
->waitForText('Test profile')
->press('CBC')
->radio('final_state', 'canceled')
->press('Continue')
->waitForText('Cancelled.');
});
}
}