1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-27 19:57:10 +02:00

Merge branch 'v5-develop' into v5-stable

This commit is contained in:
David Bomba 2021-07-10 07:50:45 +10:00
commit 44f4c4312a
80 changed files with 123979 additions and 122970 deletions

View File

@ -4,14 +4,19 @@
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&utm_medium=referral&utm_content=turbo124/invoiceninja&utm_campaign=Badge_Grade)
# Invoice Ninja version 5!
## Quick Start
## Preamble
Currently the client portal and API are of alpha quality, to get started:
Version 5 of Invoice Ninja is here! We've taken the best parts of version 4 and bolted on all of the most requested features to produce a invoicing application like no other.
The new interface has a lot more functionality so it isn't a carbon copy of v4, but once you get used to the new layout and functionality we are sure you will love it!
If you have any questions, please join us on our [forum](https://forum.invoiceninja.com) or on [slack](https://invoiceninja.slack.com)
## Quick Start
```bash
git clone https://github.com/invoiceninja/invoiceninja.git
@ -69,6 +74,8 @@ To improve chances of PRs being merged please include tests to ensure your code
API documentation is hosted using Swagger and can be found [HERE](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
Installation, Configuration and Troubleshooting documentation can be found [HERE] (https://invoiceninja.github.io)
## Credits
* [Hillel Coren](https://hillelcoren.com/)
* [David Bomba](https://github.com/turbo124)
@ -85,10 +92,18 @@ API documentation is hosted using Swagger and can be found [HERE](https://app.sw
## Current work in progress
Invoice Ninja is currently being written in a combination of Laravel for the API and Client Portal and Flutter for the front end management console. This will allow an immersive and consistent experience across any device: mobile, tablet or desktop.
Invoice Ninja is written in a combination of technologies:
To manage our workflow we will be creating separate branches for the client (Flutter) and server (Laravel API / Client Portal) and merge these into a release branch for deployments.
API - Laravel
Client Portal - Laravel + Tailwind
Admin Portal - Flutter
This allows an immersive and consistent experience across any device: mobile, tablet or desktop.
## Security
If you find a security issue with this application please send an email to contact@invoiceninja.com Please follow responsible disclosure procedures if you detect an issue. For further information on responsible disclosure please read [here](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html)
## License
Invoice Ninja is released under the Attribution Assurance License.
Invoice Ninja is released under the Elastic License.
See [LICENSE](LICENSE) for details.

View File

@ -1 +1 @@
5.2.10
5.2.11

View File

@ -365,7 +365,7 @@ class CheckData extends Command
/* Due to accounting differences we need to perform a second loop here to ensure there actually is an issue */
$clients->each(function ($client_record) use ($credit_total_applied) {
$client = Client::find($client_record->id);
$client = Client::withTrashed()->find($client_record->id);
$total_invoice_payments = 0;
@ -594,6 +594,7 @@ class CheckData extends Command
'client',
'client_contact',
'payment',
'recurring_invoice',
],
'invoices' => [
'client',

View File

@ -64,7 +64,7 @@ class Kernel extends ConsoleKernel
$schedule->job(new AutoBillCron)->dailyAt('00:30')->withoutOverlapping();
$schedule->job(new SchedulerCheck)->everyFiveMinutes();
$schedule->job(new SchedulerCheck)->daily()->withoutOverlapping();
/* Run hosted specific jobs */
if (Ninja::isHosted()) {

View File

@ -243,7 +243,7 @@ class CompanySettings extends BaseSettings
public $font_size = 7; //@implemented
public $primary_font = 'Roboto';
public $secondary_font = 'Roboto';
public $primary_color = '#142cb5';
public $primary_color = '#298AAB';
public $secondary_color = '#7081e0';
public $hide_paid_to_date = false; //@TODO where?

View File

@ -19,6 +19,8 @@ class InvoiceItem
public $product_key = '';
public $product_cost = 0;
public $notes = '';
public $discount = 0;
@ -57,6 +59,7 @@ class InvoiceItem
'type_id' => 'string',
'quantity' => 'float',
'cost' => 'float',
'product_cost' => 'float',
'product_key' => 'string',
'notes' => 'string',
'discount' => 'float',

View File

@ -81,17 +81,23 @@ class Handler extends ExceptionHandler
app('sentry')->configureScope(function (Scope $scope): void {
if(auth()->guard('contact') && auth()->guard('contact')->user())
$name = 'hosted@invoiceninja.com';
if(auth()->guard('contact') && auth()->guard('contact')->user()){
$name = "Contact = ".auth()->guard('contact')->user()->email;
$key = auth()->guard('contact')->user()->company->account->key;
elseif (auth()->guard('user') && auth()->guard('user')->user())
}
elseif (auth()->guard('user') && auth()->guard('user')->user()){
$name = "Admin = ".auth()->guard('user')->user()->email;
$key = auth()->user()->account->key;
}
else
$key = 'Anonymous';
$scope->setUser([
'id' => 'Hosted_User',
'id' => $key,
'email' => 'hosted@invoiceninja.com',
'name' => $key,
'name' => $name,
]);
});
@ -120,8 +126,7 @@ class Handler extends ExceptionHandler
}
}
// if(config('ninja.expanded_logging'))
parent::report($exception);
parent::report($exception);
}
@ -191,7 +196,7 @@ class Handler extends ExceptionHandler
} elseif ($exception instanceof GenericPaymentDriverFailure && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure) {
$data['message'] = $exception->getMessage();
return response()->json(['message' => $exception->getMessage()], 400);
}
return parent::render($request, $exception);

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Contact\ContactPasswordResetRequest;
use App\Libraries\MultiDB;
use App\Models\Account;
use Illuminate\Contracts\View\Factory;
@ -73,9 +74,8 @@ class ContactForgotPasswordController extends Controller
return Password::broker('contacts');
}
public function sendResetLinkEmail(Request $request)
public function sendResetLinkEmail(ContactPasswordResetRequest $request)
{
//MultiDB::userFindAndSetDb($request->input('email'));
$user = MultiDB::hasContact($request->input('email'));

View File

@ -35,10 +35,16 @@ class ContactLoginController extends Controller
public function showLoginForm(Request $request)
{
if (strpos($request->getHost(), 'invoicing.co') !== false) {
//if we are on the root domain invoicing.co do not show any company logos
if(Ninja::isHosted() && count(explode('.', request()->getHost())) == 2){
$company = null;
}elseif (strpos($request->getHost(), 'invoicing.co') !== false) {
$subdomain = explode('.', $request->getHost())[0];
$company = Company::where('subdomain', $subdomain)->first();
} elseif (Ninja::isSelfHost()) {
} elseif(Ninja::isHosted() && $company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first()){
}
elseif (Ninja::isSelfHost()) {
$company = Account::first()->default_company;
} else {
$company = null;

View File

@ -488,6 +488,8 @@ class LoginController extends BaseController
auth()->user()->email_verified_at = now();
auth()->user()->save();
auth()->user()->setCompany(auth()->user()->account->default_company);
$this->setLoginCache(auth()->user());
$cu = CompanyUser::whereUserId(auth()->user()->id);

View File

@ -24,6 +24,7 @@ use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class InvoiceController extends Controller
{
@ -170,8 +171,10 @@ class InvoiceController extends Controller
$invitation = $invoice->invitations->first();
//$file = $invoice->pdf_file_path($invitation);
$file = $invoice->service()->getInvoicePdf(auth()->user());
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}
// enable output of HTTP headers

View File

@ -24,8 +24,10 @@ use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class QuoteController extends Controller
{
@ -46,7 +48,7 @@ class QuoteController extends Controller
*
* @param ShowQuoteRequest $request
* @param Quote $quote
* @return Factory|View|\Symfony\Component\HttpFoundation\BinaryFileResponse
* @return Factory|View|BinaryFileResponse
*/
public function show(ShowQuoteRequest $request, Quote $quote)
{
@ -88,8 +90,11 @@ class QuoteController extends Controller
if ($quotes->count() == 1) {
$file = $quotes->first()->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $quotes->first()->service()->getQuotePdf();
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}
// enable output of HTTP headers
@ -110,16 +115,18 @@ class QuoteController extends Controller
protected function approve(array $ids, $process = false)
{
$quotes = Quote::whereIn('id', $ids)
->whereClientId(auth()->user()->client->id)
->where('client_id', auth('contact')->user()->client->id)
->where('company_id', auth('contact')->user()->client->company_id)
->where('status_id', Quote::STATUS_SENT)
->get();
if (! $quotes || $quotes->count() == 0) {
if (!$quotes || $quotes->count() == 0) {
return redirect()->route('client.quotes.index');
}
if ($process) {
foreach ($quotes as $quote) {
$quote->service()->approve(auth('contact')->user())->save();
$quote->service()->approve(auth()->user())->save();
event(new QuoteWasApproved(auth('contact')->user(), $quote, $quote->company, Ninja::eventVars()));
if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) {

View File

@ -37,6 +37,7 @@ use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class CreditController.
@ -536,8 +537,14 @@ class CreditController extends BaseController
}
break;
case 'download':
$file = $credit->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// $file = $credit->pdf_file_path();
$file = $credit->service()->getCreditPdf($credit->invitations->first());
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
break;
case 'archive':
$this->credit_repository->archive($credit);
@ -585,9 +592,12 @@ class CreditController extends BaseController
// $contact = $invitation->contact;
$credit = $invitation->credit;
$file_path = $credit->service()->getCreditPdf($invitation);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $credit->service()->getCreditPdf($invitation);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
// return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -672,8 +672,17 @@ class InvoiceController extends BaseController
break;
case 'download':
$file = $invoice->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// $file = $invoice->pdf_file_path();
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $invoice->service()->getInvoicePdf();
// return response()->download(Storage::get($file), basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
break;
case 'restore':
@ -722,10 +731,11 @@ class InvoiceController extends BaseController
}
//touch reminder1,2,3_sent + last_sent here if the email is a reminder.
$invoice->service()->touchReminder($this->reminder_template)->deletePdf()->save();
//$invoice->service()->touchReminder($this->reminder_template)->deletePdf()->save();
$invoice->service()->touchReminder($this->reminder_template)->markSent()->save();
$invoice->invitations->load('contact.client.country', 'invoice.client.country', 'invoice.company')->each(function ($invitation) use ($invoice) {
EmailEntity::dispatch($invitation, $invoice->company, $this->reminder_template);
EmailEntity::dispatch($invitation, $invoice->company, $this->reminder_template)->delay(now()->addSeconds(30));
});
if ($invoice->invitations->count() >= 1) {
@ -795,8 +805,11 @@ class InvoiceController extends BaseController
$file = $invoice->service()->getInvoicePdf($contact);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// return response()->download(Storage::get($file), basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}
/**
@ -848,7 +861,10 @@ class InvoiceController extends BaseController
$file = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}

View File

@ -39,6 +39,7 @@ use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class QuoteController.
@ -676,8 +677,14 @@ class QuoteController extends BaseController
break;
case 'download':
$file = $quote->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
//$file = $quote->pdf_file_path();
$file = $quote->service()->getQuotePdf();
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
//return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
break;
case 'restore':
@ -728,9 +735,14 @@ class QuoteController extends BaseController
$contact = $invitation->contact;
$quote = $invitation->quote;
$file_path = $quote->service()->getQuotePdf($contact);
$file = $quote->service()->getQuotePdf($contact);
nlog($file);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
// return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -33,6 +33,7 @@ use App\Utils\Traits\SavesDocuments;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class RecurringInvoiceController.
@ -500,9 +501,12 @@ class RecurringInvoiceController extends BaseController
$contact = $invitation->contact;
$recurring_invoice = $invitation->recurring_invoice;
$file_path = $recurring_invoice->service()->getInvoicePdf($contact);
$file = $recurring_invoice->service()->getInvoicePdf($contact);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -241,6 +241,11 @@ class SetupController extends Controller
$pdf->setChromiumPath(config('ninja.snappdf_chromium_path'));
}
if (config('ninja.snappdf_chromium_arguments')) {
$pdf->clearChromiumArguments();
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$pdf = $pdf
->setHtml('GENERATING PDFs WORKS! Thank you for using Invoice Ninja!')
->generate();

View File

@ -60,12 +60,6 @@ class StripeConnectController extends BaseController
$redirect_uri = 'https://invoicing.co/stripe/completed';
$endpoint = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={$stripe_client_id}&redirect_uri={$redirect_uri}&scope=read_write&state={$token}";
// if($email = $request->getContact()->email)
// $endpoint .= "&stripe_user[email]={$email}";
// $company_name = str_replace(" ", "_", $company->present()->name());
// $endpoint .= "&stripe_user[business_name]={$company_name}";
return redirect($endpoint);
}
@ -87,18 +81,24 @@ class StripeConnectController extends BaseController
}
// nlog($response);
$company = Company::where('company_key', $request->getTokenContent()['company_key'])->first();
$company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id);
$fees_and_limits = new \stdClass;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits;
$company_gateway->gateway_key = 'd14dd26a47cecc30fdd65700bfb67b34';
$company_gateway->fees_and_limits = $fees_and_limits;
$company_gateway->setConfig([]);
$company_gateway->token_billing = 'always';
// $company_gateway->save();
$company_gateway = CompanyGateway::query()
->where('gateway_key', 'd14dd26a47cecc30fdd65700bfb67b34')
->where('company_id', $company->id)
->first();
if(!$company_gateway)
{
$company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id);
$fees_and_limits = new \stdClass;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits;
$company_gateway->gateway_key = 'd14dd26a47cecc30fdd65700bfb67b34';
$company_gateway->fees_and_limits = $fees_and_limits;
$company_gateway->setConfig([]);
$company_gateway->token_billing = 'always';
// $company_gateway->save();
}
$payload = [
'account_id' => $response->stripe_user_id,
@ -111,18 +111,6 @@ class StripeConnectController extends BaseController
"access_token" => $response->access_token
];
/* Link account if existing account exists */
// if($account_id = $this->checkAccountAlreadyLinkToEmail($company_gateway, $request->getContact()->email)) {
// $payload['account_id'] = $account_id;
// $payload['stripe_user_id'] = $account_id;
// $company_gateway->setConfig($payload);
// $company_gateway->save();
// return view('auth.connect.existing');
// }
$company_gateway->setConfig($payload);
$company_gateway->save();

View File

@ -35,6 +35,9 @@ class StripeController extends BaseController
public function import()
{
return response()->json(['message' => 'Processing'], 200);
if(auth()->user()->isAdmin())
{

View File

@ -51,10 +51,10 @@ class QueryLogging
$count = count($queries);
$timeEnd = microtime(true);
$time = $timeEnd - $timeStart;
//nlog($request->method().' - '.urldecode($request->url()).": $count queries - ".$time);
// if($count > 50)
//nlog($queries);
if($count > 150)
nlog($queries);
$ip = '';
if(request()->header('Cf-Connecting-Ip'))

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\ClientPortal\Contact;
use Illuminate\Foundation\Http\FormRequest;
class ContactPasswordResetRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required',
];
}
}

View File

@ -67,8 +67,8 @@ class UpdateCompanyRequest extends Request
{
$input = $this->all();
// if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1)
// $input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']);
if(Ninja::isHosted() && array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1)
$input['portal_domain'] = $this->addScheme($input['portal_domain']);
if (array_key_exists('settings', $input)) {
$input['settings'] = $this->filterSaveableSettings($input['settings']);
@ -105,4 +105,15 @@ class UpdateCompanyRequest extends Request
return $settings;
}
private function addScheme($url, $scheme = 'https://')
{
$url = str_replace("http://", "", $url);
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme . $url : $url;
return rtrim($url, '/');
}
}

View File

@ -28,12 +28,7 @@ class ImportJsonRequest extends Request
public function rules()
{
return [
// 'import_type' => 'required',
// 'files' => 'required_without:hash|array|min:1|max:6',
// 'hash' => 'nullable|string',
// 'column_map' => 'required_with:hash|array',
// 'skip_header' => 'required_with:hash|boolean',
// 'files.*' => 'file|mimes:csv,txt',
'files' => 'file|mimes:zip',
];
}
}

View File

@ -28,6 +28,7 @@ use App\Models\Account;
use App\Models\Timezone;
use App\Notifications\Ninja\NewAccountCreated;
use App\Utils\Ninja;
use App\Utils\Traits\User\LoginCache;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -39,6 +40,7 @@ use Turbo124\Beacon\Facades\LightLogs;
class CreateAccount
{
use Dispatchable;
use LoginCache;
protected $request;
@ -77,9 +79,6 @@ class CreateAccount
$sp794f3f->save();
if(Ninja::isHosted())
$sp794f3f->startTrial('pro');
$sp035a66 = CreateCompany::dispatchNow($this->request, $sp794f3f);
$sp035a66->load('account');
$sp794f3f->default_company_id = $sp035a66->id;
@ -95,6 +94,8 @@ class CreateAccount
}
$spaa9f78->setCompany($sp035a66);
$this->setLoginCache($spaa9f78);
$spafe62e = isset($this->request['token_name']) ? $this->request['token_name'] : request()->server('HTTP_USER_AGENT');
$sp2d97e8 = CreateCompanyToken::dispatchNow($sp035a66, $spaa9f78, $spafe62e);

View File

@ -221,8 +221,8 @@ class CompanyImport implements ShouldQueue
private function unzipFile()
{
if(mime_content_type(Storage::path($this->file_location)) == 'text/plain')
return Storage::path($this->file_location);
// if(mime_content_type(Storage::path($this->file_location)) == 'text/plain')
// return Storage::path($this->file_location);
$path = TempFile::filePath(Storage::get($this->file_location), basename($this->file_location));

View File

@ -86,9 +86,8 @@ class CreateEntityPdf implements ShouldQueue
$this->contact = $invitation->contact;
$this->disk = $disk;
// $this->disk = $disk ?? config('filesystems.default');
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk;
}
public function handle()
@ -201,11 +200,9 @@ class CreateEntityPdf implements ShouldQueue
if(!Storage::disk($this->disk)->exists($path))
Storage::disk($this->disk)->makeDirectory($path, 0775);
Storage::disk($this->disk)->put($file_path, $pdf);
nlog($file_path);
Storage::disk($this->disk)->put($file_path, $pdf);
}
catch(\Exception $e)
{

View File

@ -190,6 +190,9 @@ class Import implements ShouldQueue
{
set_time_limit(0);
nlog("Starting Migration");
nlog($this->user->email);
auth()->login($this->user, false);
auth()->user()->setCompany($this->company);
@ -333,7 +336,7 @@ class Import implements ShouldQueue
$data = $this->transformCompanyData($data);
if(Ninja::isHosted() && strlen($data['subdomain']) > 1) {
if(Ninja::isHosted()) {
if(!MultiDB::checkDomainAvailable($data['subdomain']))
$data['subdomain'] = MultiDB::randomSubdomainGenerator();
@ -364,6 +367,10 @@ class Import implements ShouldQueue
unset($data['referral_code']);
}
if (isset($data['custom_fields']) && is_array($data['custom_fields'])) {
$data['custom_fields'] = $this->parseCustomFields($data['custom_fields']);
}
$company_repository = new CompanyRepository();
$company_repository->save($data, $this->company);
@ -385,6 +392,34 @@ class Import implements ShouldQueue
$company_repository = null;
}
private function parseCustomFields($fields) :array
{
if(array_key_exists('account1', $fields))
$fields['company1'] = $fields['account1'];
if(array_key_exists('account2', $fields))
$fields['company2'] = $fields['account2'];
if(array_key_exists('invoice1', $fields))
$fields['surcharge1'] = $fields['invoice1'];
if(array_key_exists('invoice2', $fields))
$fields['surcharge2'] = $fields['invoice2'];
if(array_key_exists('invoice_text1', $fields))
$fields['invoice1'] = $fields['invoice_text1'];
if(array_key_exists('invoice_text2', $fields))
$fields['invoice2'] = $fields['invoice_text2'];
foreach ($fields as &$value) {
$value = (string) $value;
}
return $fields;
}
private function transformCompanyData(array $data): array
{
$company_settings = CompanySettings::defaults();
@ -1321,7 +1356,7 @@ class Import implements ShouldQueue
$modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']);
}
else if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
$modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34';
$modified['fees_and_limits'] = [];
}

View File

@ -50,7 +50,7 @@ $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars[
$fields->subscription_id = $subscription->id;
$fields->user_id = $user_id;
$fields->company_id = $subscription->company_id;
$fields->activity_type_id = Activity::ARCHIVE_SUBSCRIPTIOn;
$fields->activity_type_id = Activity::ARCHIVE_SUBSCRIPTION;
$this->activity_repo->save($fields, $subscription, $event->event_vars);
}

View File

@ -11,6 +11,7 @@
namespace App\Mail\Engine;
use App\Models\Account;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number;

View File

@ -12,6 +12,7 @@
namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Jobs\Entity\CreateEntityPdf;
use App\Models\Account;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
@ -116,8 +117,6 @@ class InvoiceEmailEngine extends BaseEmailEngine
else
$this->setAttachments([$this->invoice->pdf_file_path($this->invitation)]);
// $this->setAttachments(['path' => $this->invoice->pdf_file_path(), 'name' => basename($this->invoice->pdf_file_path())]);
}
//attach third party documents

View File

@ -52,7 +52,7 @@ class SupportMessageSent extends Mailable
$account = auth()->user()->account;
$plan = $account->plan ?: 'Free Self Hosted';
$plan = $account->plan ?: 'Forever Free';
$company = auth()->user()->company();
$user = auth()->user();

View File

@ -13,6 +13,8 @@ namespace App\Models;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits;
use App\Models\CompanyGateway;
use App\Models\Presenters\ClientPresenter;
use App\Services\Client\ClientService;
use App\Utils\Traits\AppSetup;
@ -396,61 +398,131 @@ class Client extends BaseModel implements HasLocalePreference
*/
public function getCreditCardGateway() :?CompanyGateway
{
$company_gateways = $this->getSetting('company_gateway_ids');
// $company_gateways = $this->getSetting('company_gateway_ids');
/* It is very important to respect the order of the company_gateway_ids as they are ordered by priority*/
if (strlen($company_gateways) >= 1) {
$transformed_ids = $this->transformKeys(explode(',', $company_gateways));
$gateways = $this->company
->company_gateways
->whereIn('id', $transformed_ids)
->sortby(function ($model) use ($transformed_ids) {
return array_search($model->id, $transformed_ids);
});
} else {
$gateways = $this->company->company_gateways;
}
// /* It is very important to respect the order of the company_gateway_ids as they are ordered by priority*/
// if (strlen($company_gateways) >= 1) {
// $transformed_ids = $this->transformKeys(explode(',', $company_gateways));
// $gateways = $this->company
// ->company_gateways
// ->whereIn('id', $transformed_ids)
// ->sortby(function ($model) use ($transformed_ids) {
// return array_search($model->id, $transformed_ids);
// });
// } else {
// $gateways = $this->company->company_gateways;
// }
foreach ($gateways as $gateway) {
if (in_array(GatewayType::CREDIT_CARD, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::CREDIT_CARD))) {
return $gateway;
// foreach ($gateways as $gateway) {
// if (in_array(GatewayType::CREDIT_CARD, $gateway->driver($this)->gatewayTypeEnabled($gateway, GatewayType::CREDIT_CARD))) {
// return $gateway;
// }
// }
// return null;
//
$pms = $this->service()->getPaymentMethods(0);
foreach($pms as $pm)
{
if($pm['gateway_type_id'] == GatewayType::CREDIT_CARD)
{
$cg = CompanyGateway::find($pm['company_gateway_id']);
if($cg && !property_exists($cg->fees_and_limits, GatewayType::CREDIT_CARD)){
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if($cg && $cg->fees_and_limits->{GatewayType::CREDIT_CARD}->is_enabled)
return $cg;
}
}
}
return null;
return null;
}
//todo refactor this - it is only searching for existing tokens
public function getBankTransferGateway() :?CompanyGateway
{
$company_gateways = $this->getSetting('company_gateway_ids');
$pms = $this->service()->getPaymentMethods(0);
if($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, array_column($pms, 'gateway_type_id'))){
foreach($pms as $pm){
if($pm['gateway_type_id'] == GatewayType::BANK_TRANSFER)
{
$cg = CompanyGateway::find($pm['company_gateway_id']);
if($cg && !property_exists($cg->fees_and_limits, GatewayType::BANK_TRANSFER)){
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if($cg && $cg->fees_and_limits->{GatewayType::BANK_TRANSFER}->is_enabled)
return $cg;
}
}
if (strlen($company_gateways) >= 1) {
$transformed_ids = $this->transformKeys(explode(',', $company_gateways));
$gateways = $this->company
->company_gateways
->whereIn('id', $transformed_ids)
->sortby(function ($model) use ($transformed_ids) {
return array_search($model->id, $transformed_ids);
});
} else {
$gateways = $this->company->company_gateways;
}
foreach ($gateways as $gateway) {
if ($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::BANK_TRANSFER))) {
return $gateway;
if($this->currency()->code == 'EUR' && in_array(GatewayType::BANK_TRANSFER, array_column($pms, 'gateway_type_id'))){
foreach($pms as $pm){
if($pm['gateway_type_id'] == GatewayType::SEPA)
{
$cg = CompanyGateway::find($pm['company_gateway_id']);
if($cg && $cg->fees_and_limits->{GatewayType::SEPA}->is_enabled)
return $cg;
}
}
if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::SEPA))) {
return $gateway;
}
}
return null;
// $company_gateways = $this->getSetting('company_gateway_ids');
// if (strlen($company_gateways) >= 1) {
// $transformed_ids = $this->transformKeys(explode(',', $company_gateways));
// $gateways = $this->company
// ->company_gateways
// ->whereIn('id', $transformed_ids)
// ->sortby(function ($model) use ($transformed_ids) {
// return array_search($model->id, $transformed_ids);
// });
// } else {
// $gateways = $this->company->company_gateways;
// }
// foreach ($gateways as $gateway) {
// if ($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::BANK_TRANSFER))) {
// return $gateway;
// }
// if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::SEPA))) {
// return $gateway;
// }
// }
// return null;
}
public function getBankTransferMethodType()
{
if ($this->currency()->code == 'USD') {
return GatewayType::BANK_TRANSFER;
}

View File

@ -59,6 +59,17 @@ class CompanyGateway extends BaseModel
16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'],
];
// const TYPE_PAYPAL = 300;
// const TYPE_STRIPE = 301;
// const TYPE_LEDGER = 302;
// const TYPE_FAILURE = 303;
// const TYPE_CHECKOUT = 304;
// const TYPE_AUTHORIZE = 305;
// const TYPE_CUSTOM = 306;
// const TYPE_BRAINTREE = 307;
// const TYPE_WEPAY = 309;
public $gateway_consts = [
'38f2c48af60c7dd69e04248cbb24c36e' => 300,
'd14dd26a37cecc30fdd65700bfb55b23' => 301,
@ -66,6 +77,8 @@ class CompanyGateway extends BaseModel
'3b6621f970ab18887c4f6dca78d3f8bb' => 305,
'54faab2ab6e3223dbe848b1686490baa' => 306,
'd14dd26a47cecc30fdd65700bfb67b34' => 301,
'8fdeed552015b3c7b44ed6c8ebd9e992' => 309,
'f7ec488676d310683fb51802d076d713' => 307,
];
protected $touches = [];

View File

@ -161,6 +161,8 @@ class User extends Authenticatable implements MustVerifyEmail
public function setCompany($company)
{
$this->company = $company;
return $this;
}
/**

View File

@ -202,6 +202,7 @@ class AuthorizeCreditCard
private function processFailedResponse($data, $request)
{
$response = $data['response'];
$amount = array_key_exists('amount_with_fee', $data) ? $data['amount_with_fee'] : 0;
PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransactionResponse()->getTransId(), $this->authorize->client->company, $data['amount_with_fee']);

View File

@ -114,7 +114,7 @@ class StripePaymentDriver extends BaseDriver
public function gatewayTypes(): array
{
$types = [
GatewayType::CRYPTO,
// GatewayType::CRYPTO,
GatewayType::CREDIT_CARD
];

View File

@ -49,28 +49,11 @@ class CompanyRepository extends BaseRepository
private function parseCustomFields($fields) :array
{
if(array_key_exists('account1', $fields))
$fields['company1'] = $fields['account1'];
if(array_key_exists('account2', $fields))
$fields['company2'] = $fields['account2'];
if(array_key_exists('invoice1', $fields))
$fields['surcharge1'] = $fields['invoice1'];
if(array_key_exists('invoice2', $fields))
$fields['surcharge2'] = $fields['invoice2'];
if(array_key_exists('invoice_text1', $fields))
$fields['invoice1'] = $fields['invoice_text1'];
if(array_key_exists('invoice_text2', $fields))
$fields['invoice2'] = $fields['invoice_text2'];
foreach ($fields as &$value) {
$value = (string) $value;
}
return $fields;
}
}

View File

@ -60,14 +60,16 @@ class ClientService
return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision);
}
public function getCredits() :Collection
public function getCredits()
{
return $this->client->credits()
->where('is_deleted', false)
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhere('due_date', NULL)
->orderBy('created_at','ASC');
->where(function ($query){
$query->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhereNull('due_date');
})
->orderBy('created_at','ASC')->get();
}
public function getPaymentMethods(float $amount)

View File

@ -41,10 +41,13 @@ class GetCreditPdf extends AbstractService
$file_path = $path.$this->credit->numberFormatter().'.pdf';
$disk = 'public';
// $disk = 'public';
$disk = config('filesystems.default');
$file_path = CreateEntityPdf::dispatchNow($this->invitation);
return Storage::disk($disk)->path($file_path);
nlog($file_path);
return $file_path;
// return Storage::disk($disk)->path($file_path);
}
}

View File

@ -49,9 +49,9 @@ class GenerateDeliveryNote
$this->contact = $contact;
$this->disk = 'public';
// $this->disk = 'public';
// $this->disk = $disk ?? config('filesystems.default');
$this->disk = $disk ?? config('filesystems.default');
}
public function run()
@ -111,7 +111,8 @@ class GenerateDeliveryNote
Storage::disk($this->disk)->put($file_path, $pdf);
return Storage::disk($this->disk)->path($file_path);
//return Storage::disk($this->disk)->path($file_path);
return $file_path;
}
}

View File

@ -39,7 +39,8 @@ class GetInvoicePdf extends AbstractService
$file_path = $path.$this->invoice->numberFormatter().'.pdf';
$disk = 'public';
// $disk = 'public';
$disk = config('filesystems.default');
$file = Storage::disk($disk)->exists($file_path);
@ -47,6 +48,8 @@ class GetInvoicePdf extends AbstractService
$file_path = CreateEntityPdf::dispatchNow($invitation);
}
return Storage::disk($disk)->path($file_path);
// return Storage::disk($disk)->path($file_path);
//
return $file_path;
}
}

View File

@ -119,6 +119,7 @@ class PaymentService
->service()
->getCredits();
foreach ($credits as $credit) {
//starting invoice balance
$invoice_balance = $invoice->balance;

View File

@ -39,10 +39,12 @@ class GetQuotePdf extends AbstractService
$file_path = $path.$this->quote->numberFormatter().'.pdf';
$disk = 'public';
// $disk = 'public';
$disk = config('filesystems.default');
$file_path = CreateEntityPdf::dispatchNow($invitation);
return Storage::disk($disk)->path($file_path);
return $file_path;
//return Storage::disk($disk)->path($file_path);
}
}

View File

@ -51,6 +51,11 @@ class QuoteService
$this->quote->fresh();
if ($this->quote->client->getSetting('auto_archive_quote')) {
$quote_repo = new QuoteRepository();
$quote_repo->archive($this->quote);
}
return $this;
}
@ -129,10 +134,6 @@ class QuoteService
public function convertToInvoice()
{
//to prevent circular references we need to explicit call this here.
// $mark_approved = new MarkApproved($this->quote->client);
// $this->quote = $mark_approved->run($this->quote);
$this->convert();
$this->invoice->service()->createInvitations();

View File

@ -41,14 +41,14 @@ class GetInvoicePdf extends AbstractService
$file_path = $path.$this->entity->hashed_id.'.pdf';
$disk = 'public';
$disk = config('filesystems.default');
$file = Storage::disk($disk)->exists($file_path);
if (! $file) {
$file_path = CreateEntityPdf::dispatchNow($invitation);
}
return Storage::disk($disk)->path($file_path);
return $file_path;
}
}

View File

@ -34,6 +34,11 @@ trait PdfMaker
$pdf->setChromiumPath(config('ninja.snappdf_chromium_path'));
}
if (config('ninja.snappdf_chromium_arguments')) {
$pdf->clearChromiumArguments();
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$generated = $pdf
->setHtml($html)
->generate();

View File

@ -37,9 +37,13 @@ trait SubscriptionHooker
nlog("method name must be a string");
nlog($subscription->webhook_configuration['post_purchase_rest_method']);
nlog($subscription->webhook_configuration['post_purchase_url']);
$post_purchase_rest_method = (string)$subscription->webhook_configuration['post_purchase_rest_method'];
$post_purchase_url = (string)$subscription->webhook_configuration['post_purchase_url'];
try {
$response = $client->{$subscription->webhook_configuration['post_purchase_rest_method']}($subscription->webhook_configuration['post_purchase_url'],[
$response = $client->{$post_purchase_rest_method}($post_purchase_url,[
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false
]);

View File

@ -33,7 +33,7 @@
"asm/php-ansible": "dev-main",
"authorizenet/authorizenet": "^2.0",
"bacon/bacon-qr-code": "^2.0",
"beganovich/snappdf": "^1.0",
"beganovich/snappdf": "^1.7",
"braintree/braintree_php": "^6.0",
"checkout/checkout-sdk-php": "^1.0",
"cleverit/ubl_invoice": "^1.3",
@ -47,6 +47,7 @@
"google/apiclient": "^2.7",
"guzzlehttp/guzzle": "^7.0.1",
"hashids/hashids": "^4.0",
"hedii/laravel-gelf-logger": "^6.0",
"intervention/image": "^2.5",
"laracasts/presenter": "^0.2.1",
"laravel/framework": "^8.0",

705
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -102,8 +102,72 @@ return [
'invoiceninja' => [
'driver' => 'single',
'level' => 'debug',
'path' => storage_path('logs/invoiceninja.log'),
],
'gelf' => [
'driver' => 'custom',
'via' => \Hedii\LaravelGelfLogger\GelfLoggerFactory::class,
// This optional option determines the processors that should be
// pushed to the handler. This option is useful to modify a field
// in the log context (see NullStringProcessor), or to add extra
// data. Each processor must be a callable or an object with an
// __invoke method: see monolog documentation about processors.
// Default is an empty array.
'processors' => [
\Hedii\LaravelGelfLogger\Processors\NullStringProcessor::class,
// another processor...
],
// This optional option determines the minimum "level" a message
// must be in order to be logged by the channel. Default is 'debug'
'level' => 'debug',
// This optional option determines the channel name sent with the
// message in the 'facility' field. Default is equal to app.env
// configuration value
'name' => 'my-custom-name',
// This optional option determines the system name sent with the
// message in the 'source' field. When forgotten or set to null,
// the current hostname is used.
'system_name' => null,
// This optional option determines if you want the UDP, TCP or HTTP
// transport for the gelf log messages. Default is UDP
'transport' => 'udp',
// This optional option determines the host that will receive the
// gelf log messages. Default is 127.0.0.1
'host' => env('GRAYLOG_SERVER', '127.0.0.1'),
// This optional option determines the port on which the gelf
// receiver host is listening. Default is 12201
'port' => 12201,
// This optional option determines the path used for the HTTP
// transport. When forgotten or set to null, default path '/gelf'
// is used.
'path' => null,
// This optional option determines the maximum length per message
// field. When forgotten or set to null, the default value of
// \Monolog\Formatter\GelfMessageFormatter::DEFAULT_MAX_LENGTH is
// used (currently this value is 32766)
'max_length' => null,
// This optional option determines the prefix for 'context' fields
// from the Monolog record. Default is null (no context prefix)
'context_prefix' => null,
// This optional option determines the prefix for 'extra' fields
// from the Monolog record. Default is null (no extra prefix)
'extra_prefix' => null,
],
],
];

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.2.10',
'app_tag' => '5.2.10',
'app_version' => '5.2.11',
'app_tag' => '5.2.11',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -142,6 +142,7 @@ return [
'log_pdf_html' => env('LOG_PDF_HTML', false),
'expanded_logging' => env('EXPANDED_LOGGING', false),
'snappdf_chromium_path' => env('SNAPPDF_CHROMIUM_PATH', false),
'snappdf_chromium_arguments' => env('SNAPPDF_CHROMIUM_ARGUMENTS', false),
'v4_migration_version' => '4.5.35',
'flutter_renderer' => env('FLUTTER_RENDERER', 'selfhosted-html'),
'webcron_secret' => env('WEBCRON_SECRET', false),

View File

@ -1,8 +1,6 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateDesigns extends Migration
{

File diff suppressed because it is too large Load Diff

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"main.dart.js": "ec0d23274ffa0ea4b6743fe88c16ccaa",
"main.dart.js": "bb665dece53f7ac166766cfaaecff64e",
"/": "23224b5e03519aaa87594403d54412cf",
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
@ -29,7 +29,7 @@ const RESOURCES = {
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/AssetManifest.json": "7e49562f32e24a9e2557fe4178a84b79",
"version.json": "4d10e2258012cbb88b24009334a24f24",
"version.json": "037c648413a92d1fa58410a4da834fbf",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.ico": "51636d3a390451561744c42188ccd628"

124639
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

120183
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

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=9d6698418e6cdd571d49",
"/css/app.css": "/css/app.css?id=b1d2337c232fcb67a548",
"/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",
@ -21,7 +21,5 @@
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=5c35d28cf0a3286e7c45",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=fc3055d6a099f523ea98",
"/js/setup/setup.js": "/js/setup/setup.js?id=8d454e7090f119552a6c",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad",
"/js/admin.js": "/js/admin.js?id=003930085af69b13a86a",
"/css/admin.css": "/css/admin.css?id=0ca9742f0da64aa54214"
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad"
}

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.52","build_number":"52"}
{"app_name":"invoiceninja_flutter","version":"5.0.53","build_number":"53"}

View File

@ -4273,6 +4273,11 @@ $LANG = array(
'already_default_payment_method' => 'This is your preferred way of paying.',
'auto_bill_disabled' => 'Auto Bill Disabled',
'select_payment_method' => 'Select a payment method:',
'login_without_password' => 'Log in without password',
'email_sent' => 'E-mail sent, please check your inbox.',
'one_time_purchases' => 'One time purchases',
'recurring_purchases' => 'Recurring purchases',
'you_might_be_interested_in_following' => 'You might be interested in following',
);
return $LANG;

View File

@ -34,7 +34,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
#company-details,

View File

@ -29,7 +29,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
.header-container > span {

View File

@ -29,7 +29,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
#company-details {

View File

@ -29,7 +29,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
.header-wrapper #client-details,

View File

@ -23,7 +23,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
.company-logo-wrapper {

View File

@ -71,7 +71,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
.entity-label {

View File

@ -61,7 +61,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
#client-details {

View File

@ -28,7 +28,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
.header-wrapper #company-address {

View File

@ -47,7 +47,7 @@
}
.company-logo {
zoom: 50%; /** Adapt the zoom size, if you think it's necessary. **/
max-width: 65%;
}
.contacts-wrapper {

View File

@ -54,7 +54,7 @@
}
.company-logo {
zoom: 50%;
max-width: 65%;
}
.header-invoice-number {

View File

@ -32,7 +32,8 @@
<input type="email" name="email" id="email"
class="input"
value="{{ request()->query('email') ?? old('email') }}"
autofocus>
autofocus
required>
@error('email')
<div class="validation validation-fail">
{{ $message }}

View File

@ -1,5 +1,5 @@
<div class="grid grid-cols-12">
<div class="col-span-12 lg:col-span-6 bg-gray-50 flex flex-col items-center">
<div class="col-span-12 xl:col-span-8 bg-gray-50 flex flex-col items-center">
<div class="w-full p-10 lg:mt-24 md:max-w-3xl">
<img class="h-8" src="{{ $subscription->company->present()->logo }}"
alt="{{ $subscription->company->present()->name }}">
@ -14,7 +14,7 @@
<div class="flex flex-col mt-8">
<p
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
One-time purchases:
{{ ctrans('texts.one_time_purchases') }}
</p>
@foreach($subscription->service()->products() as $product)
@ -25,7 +25,6 @@
<div data-ref="price-and-quantity-container">
<span
data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
{{-- <span data-ref="quantity" class="text-sm">(1x)</span>--}}
</div>
</div>
@endforeach
@ -36,7 +35,7 @@
<div class="flex flex-col mt-8">
<p
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
Recurring purchases:
{{ ctrans('texts.recurring_purchases') }}
</p>
@foreach($subscription->service()->recurring_products() as $product)
@ -82,12 +81,26 @@
<span>{{ ctrans('texts.client_portal') }}</span>
</a>
@endif
@if($subscription->service()->getPlans()->count() - 1 > 1)
<div class="flex flex-col mt-10">
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
{{ ctrans('texts.you_might_be_interested_in_following') }}:
</p>
<div class="mt-4 space-x-2">
@foreach($subscription->service()->getPlans() as $_subscription)
<a class="border mt-4 bg-white rounded py-2 px-4 hover:bg-gray-100 text-sm" target="_blank" href="{{ route('client.subscription.purchase', $_subscription->hashed_id) }}">{{ $_subscription->name }}</a>
@endforeach
</div>
</div>
@endif
</div>
</div>
<div class="col-span-12 lg:col-span-6 bg-white lg:h-screen">
<div class="grid grid-cols-12 flex flex-col p-10 lg:mt-48 lg:ml-16">
<div class="col-span-12 w-full lg:col-span-6">
<div class="col-span-12 xl:col-span-4 bg-white flex flex-col items-center lg:h-screen">
<div class="w-full p-10 md:p-24 xl:mt-32 md:max-w-3xl">
<div class="col-span-12 w-full xl:col-span-9">
<h2 class="text-2xl font-bold tracking-wide">{{ $heading_text ?? ctrans('texts.login') }}</h2>
@if (session()->has('message'))
@component('portal.ninja2020.components.message')
@ -179,12 +192,12 @@
<button wire:loading.attr="disabled" type="button" wire:click="passwordlessLogin"
class="mt-4 text-sm active:outline-none focus:outline-none">
Log in without password
{{ ctrans('texts.login_without_password') }}
</button>
@if($steps['passwordless_login_sent'])
<span
class="block mt-2 text-sm text-green-600">E-mail sent. Please check your inbox!</span>
class="block mt-2 text-sm text-green-600">{{ ctrans('texts.email_sent') }}</span>
@endif
@endif
@ -200,7 +213,7 @@
</div>
<div class="relative flex justify-center text-sm leading-5">
<span class="px-2 text-gray-700 bg-white">Have a coupon code?</span>
<span class="px-2 text-gray-700 bg-white">{{ ctrans('texts.promo_code') }}</span>
</div>
</div>
@ -211,7 +224,7 @@
<input type="text" wire:model.lazy="coupon" class="input w-full m-0"/>
</label>
<button class="button button-primary bg-primary">Apply</button>
<button class="button button-primary bg-primary">{{ ctrans('texts.apply') }}</button>
</form>
@endif

View File

@ -1,4 +1,4 @@
@extends('portal.ninja2020.layout.clean')
@extends('themes.ninja2020.clean')
@section('meta_title', ctrans('texts.set_password'))
@section('body')

View File

@ -1,4 +1,4 @@
@extends('portal.ninja2020.layout.clean')
@extends('themes.ninja2020.clean')
@section('meta_title', ctrans('texts.confirmation'))
@section('body')

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- Error: {{ session('error') }} -->
@if (config('services.analytics.tracking_id'))
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-122229484-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '{{ config('services.analytics.tracking_id') }}', {'anonymize_ip': true});
function trackEvent(category, action) {
ga('send', 'event', category, action, this.src);
}
</script>
<script>
Vue.config.devtools = true;
</script>
@else
<script>
function gtag() {
}
</script>
@endif
<!-- Title -->
@auth()
<title>@yield('meta_title', '') {{ auth('contact')->user()->user->account->isPaid() ? auth('contact')->user()->company->present()->name() : 'Invoice Ninja' }}</title>
@endauth
@guest
<title>@yield('meta_title', '') {{ config('app.name') }}</title>
@endguest
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="@yield('meta_description')"/>
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.x/dist/alpine.min.js" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet" type="text/css">
<!-- Styles -->
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
{{-- <link href="{{ mix('favicon.png') }}" rel="shortcut icon" type="image/png"> --}}
<link rel="canonical" href="{{ config('ninja.app_url') }}/{{ request()->path() }}"/>
{{-- Feel free to push anything to header using @push('header') --}}
@stack('head')
@livewireStyles
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.css" />
</head>
<body class="antialiased">
@if(session()->has('message'))
<div class="py-1 text-sm text-center text-white bg-primary disposable-alert">
{{ session('message') }}
</div>
@endif
@yield('body')
@livewireScripts
<script src="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.js" data-cfasync="false"></script>
<script>
window.addEventListener("load", function(){
if (! window.cookieconsent) {
return;
}
window.cookieconsent.initialise({
"palette": {
"popup": {
"background": "#000"
},
"button": {
"background": "#f1d600"
},
},
"content": {
"href": "https://www.invoiceninja.com/privacy-policy/",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",
}
})}
);
</script>
</body>
<footer>
@yield('footer')
@stack('footer')
</footer>
</html>

View File

@ -125,9 +125,9 @@ class TaskStatusApiTest extends TestCase
public function testTaskStatusDeletedFromDELETEROute()
{
$data = [
'ids' => [$this->encodePrimaryKey($this->task_status->id)],
];
// $data = [
// 'ids' => [$this->encodePrimaryKey($this->task_status->id)],
// ];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
@ -135,7 +135,8 @@ class TaskStatusApiTest extends TestCase
])->delete('/api/v1/task_statuses/'.$this->encodePrimaryKey($this->task_status->id));
$arr = $response->json();
nlog($arr);
// nlog($arr);
$this->assertTrue($arr['data']['is_deleted']);
}

View File

@ -34,6 +34,11 @@ class PdfGenerationTest extends TestCase
$pdf->setChromiumPath(config('ninja.snappdf_chromium_path'));
}
if (config('ninja.snappdf_chromium_arguments')) {
$pdf->clearChromiumArguments();
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$pdf = $pdf
->setHtml('<h1>Invoice Ninja</h1>')
->generate();

59
tests/Unit/UrlTest.php Normal file
View File

@ -0,0 +1,59 @@
<?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 Tests\Unit;
use Tests\TestCase;
/**
* @test
*/
class UrlTest extends TestCase
{
public function setUp() :void
{
parent::setUp();
}
public function testNoScheme()
{
$url = 'google.com';
$this->assertEquals("https://google.com", $this->addScheme($url));
}
public function testNoSchemeAndTrailingSlash()
{
$url = 'google.com/';
$this->assertEquals("https://google.com", $this->addScheme($url));
}
public function testNoSchemeAndTrailingSlashAndHttp()
{
$url = 'http://google.com/';
$this->assertEquals("https://google.com", $this->addScheme($url));
}
private function addScheme($url, $scheme = 'https://')
{
$url = str_replace("http://", "", $url);
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme . $url : $url;
return rtrim($url, '/');
}
}