Merge branch 'v5-develop' of https://github.com/turbo124/invoiceninja into v5-develop

This commit is contained in:
David Bomba 2021-03-21 13:51:45 +11:00
commit 8d490200b6
108 changed files with 107791 additions and 106281 deletions

View File

@ -7,15 +7,7 @@
[![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)
# Invoice Ninja version 5.1 RC2!
Invoice Ninja version 5.1 has now reached Release Candidate 2!
What does this mean exactly? We consider this version _almost_ stable. There may be some remaining small issues which we would love to get feedback on. We would really appreciate the community booting up this version and attempting the migration from their Invoice Ninja V4 application and inspect the migrated data.
We'd also like feedback on any issues that you can see, and help us nail down the few remaining issues before Version 5 graduates to Stable Gold Release.
Please note we do not consider this version ready for production use, please stick with your V4 installation for your production clients!
# Invoice Ninja version 5!
## Quick Start

View File

@ -1 +1 @@
5.1.24
5.1.29

View File

@ -165,6 +165,7 @@ class DemoMode extends Command
'account_id' => $account->id,
'email' => 'small@example.com',
'confirmation_code' => $this->createDbHash(config('database.default')),
'email_verified_at' => now(),
]);
}
@ -199,6 +200,7 @@ class DemoMode extends Command
'password' => Hash::make('Password0'),
'account_id' => $account->id,
'confirmation_code' => $this->createDbHash(config('database.default')),
'email_verified_at' => now(),
]);
$company_token = new CompanyToken;

View File

@ -57,24 +57,17 @@ class Kernel extends ConsoleKernel
$schedule->job(new BillingSubscriptionCron)->daily()->withoutOverlapping();
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping();
$schedule->job(new SchedulerCheck)->everyFiveMinutes();
/* Run hosted specific jobs */
if (Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota())->daily()->withoutOverlapping();
$schedule->job(new SendFailedEmails())->daily()->withoutOverlapping();
$schedule->job(new AdjustEmailQuota)->daily()->withoutOverlapping();
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
}
/* Run queue's with this*/
if (Ninja::isSelfHost()) {
$schedule->command('queue:work')->everyMinute()->withoutOverlapping();
//we need to add this as we are seeing cached queues mess up the system on first load.
$schedule->command('queue:restart')->everyFiveMinutes()->withoutOverlapping();
$schedule->job(new SchedulerCheck)->everyFiveMinutes()->withoutOverlapping();
}
}
/**

View File

@ -0,0 +1,47 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\DataMapper\Billing;
class WebhookConfiguration
{
/**
* @var string
*/
public $return_url = '';
/**
* @var string
*/
public $post_purchase_url = '';
/**
* @var array
*/
public $post_purchase_headers = [];
/**
* @var string
*/
public $post_purchase_body = '';
/**
* @var array
*/
public static $casts = [
'return_url' => 'string',
'post_purchase_url' => 'string',
'post_purchase_headers' => 'array',
'post_purchase_body' => 'object',
];
}

View File

@ -108,6 +108,7 @@ class CompanySettings extends BaseSettings
public $project_number_counter = 1; //@implemented
public $shared_invoice_quote_counter = false; //@implemented
public $shared_invoice_credit_counter = false; //@implemented
public $recurring_number_prefix = 'R'; //@implemented
public $reset_counter_frequency_id = '0'; //@implemented
public $reset_counter_date = ''; //@implemented
@ -262,6 +263,7 @@ class CompanySettings extends BaseSettings
public $hide_empty_columns_on_pdf = false;
public static $casts = [
'shared_invoice_credit_counter' => 'bool',
'reply_to_name' => 'string',
'hide_empty_columns_on_pdf' => 'bool',
'enable_reminder_endless' => 'bool',

View File

@ -51,7 +51,7 @@ class InvoiceItem
public $custom_value4 = '';
public $type_id = '1'; //1 = product, 2 = service, 3 unpaid gateway fee, 4 paid gateway fee, 5 late fee
public $type_id = '1'; //1 = product, 2 = service, 3 unpaid gateway fee, 4 paid gateway fee, 5 late fee, 6 promo code
public static $casts = [
'type_id' => 'string',

View File

@ -35,7 +35,7 @@ class CompanyFactory
$company->custom_fields = (object) [];
$company->subdomain = '';
$company->enabled_modules = config('ninja.enabled_modules'); //32767;//8191; //4095
$company->default_password_timeout = 30;
$company->default_password_timeout = 30 * 60000;
return $company;
}

View File

@ -142,11 +142,11 @@ class ActivityController extends BaseController
$pdf = $this->makePdf(null, null, $backup->html_backup);
if (isset($activity->invoice_id)) {
$filename = $activity->invoice->number.'.pdf';
$filename = $activity->invoice->numberFormatter().'.pdf';
} elseif (isset($activity->quote_id)) {
$filename = $activity->quote->number.'.pdf';
$filename = $activity->quote->numberFormatter().'.pdf';
} elseif (isset($activity->credit_id)) {
$filename = $activity->credit->number.'.pdf';
$filename = $activity->credit->numberFormatter().'.pdf';
} else {
$filename = 'backup.pdf';
}

View File

@ -28,6 +28,9 @@ use Google_Client;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use PragmaRX\Google2FA\Google2FA;
use Turbo124\Beacon\Facades\LightLogs;
class LoginController extends BaseController
@ -159,19 +162,48 @@ class LoginController extends BaseController
}
if ($this->attemptLogin($request)) {
LightLogs::create(new LoginSuccess())
->increment()
->batch();
$user = $this->guard()->user();
//if user has 2fa enabled - lets check this now:
if($user->google_2fa_secret && $request->has('one_time_password'))
{
$google2fa = new Google2FA();
if(strlen($request->input('one_time_password')) == 0 || !$google2fa->verifyKey(decrypt($user->google_2fa_secret), $request->input('one_time_password')))
{
return response()
->json(['message' => ctrans('texts.invalid_one_time_password')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
}
elseif($user->google_2fa_secret && !$request->has('one_time_password')) {
return response()
->json(['message' => ctrans('texts.invalid_one_time_password')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
$user->setCompany($user->account->default_company);
$timeout = auth()->user()->company()->default_password_timeout;
Cache::put(auth()->user()->hashed_id.'_logged_in', Str::random(64), $timeout);
$cu = CompanyUser::query()
->where('user_id', auth()->user()->id);
return $this->listResponse($cu);
} else {
LightLogs::create(new LoginFailure())
->increment()
->batch();
@ -182,6 +214,7 @@ class LoginController extends BaseController
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
}
@ -269,12 +302,14 @@ class LoginController extends BaseController
$user = $google->getTokenResponse(request()->input('id_token'));
if (is_array($user)) {
$query = [
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google',
];
if ($existing_user = MultiDB::hasUser($query)) {
Auth::login($existing_user, true);
$existing_user->setCompany($existing_user->account->default_company);
@ -282,38 +317,40 @@ class LoginController extends BaseController
->where('user_id', auth()->user()->id);
return $this->listResponse($cu);
}
}
if ($user) {
$client = new Google_Client();
$client->setClientId(config('ninja.auth.google.client_id'));
$client->setClientSecret(config('ninja.auth.google.client_secret'));
$client->setRedirectUri(config('ninja.app_url'));
// we are no longer accessing the permissions for gmail - email permissions here
$token = false;
// $client = new Google_Client();
// $client->setClientId(config('ninja.auth.google.client_id'));
// $client->setClientSecret(config('ninja.auth.google.client_secret'));
// $client->setRedirectUri(config('ninja.app_url'));
try{
$token = $client->authenticate(request()->input('server_auth_code'));
}
catch(\Exception $e) {
// $token = false;
return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
// try{
// $token = $client->authenticate(request()->input('server_auth_code'));
// }
// catch(\Exception $e) {
}
// return response()
// ->json(['message' => ctrans('texts.invalid_credentials')], 401)
// ->header('X-App-Version', config('ninja.app_version'))
// ->header('X-Api-Version', config('ninja.minimum_client_version'));
$refresh_token = '';
// }
if (array_key_exists('refresh_token', $token)) {
$refresh_token = $token['refresh_token'];
}
// $refresh_token = '';
//$access_token = $token['access_token'];
// if (array_key_exists('refresh_token', $token)) {
// $refresh_token = $token['refresh_token'];
// }
$name = OAuth::splitName($google->harvestName($user));
$new_account = [

View File

@ -0,0 +1,30 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use App\Models\BillingSubscription;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class BillingSubscriptionPurchaseController extends Controller
{
public function index(BillingSubscription $billing_subscription)
{
return view('billing-portal.purchase', [
'billing_subscription' => $billing_subscription,
'hash' => Str::uuid()->toString(),
]);
}
}

View File

@ -29,6 +29,7 @@ use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Illuminate\View\View;
@ -237,11 +238,18 @@ class PaymentController extends Controller
->get();
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals];
if ($request->query('hash')) {
$hash_data['billing_context'] = Cache::get($request->query('hash'));
}
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(128);
$payment_hash->data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals];
$payment_hash->data = $hash_data;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
$totals = [

View File

@ -21,9 +21,9 @@ use Illuminate\Http\Request;
class ConnectedAccountController extends BaseController
{
protected $entity_type = CompanyUser::class;
protected $entity_type = User::class;
protected $entity_transformer = CompanyUserTransformer::class;
protected $entity_transformer = UserTransformer::class;
public function __construct()
{
@ -89,23 +89,8 @@ class ConnectedAccountController extends BaseController
$user = $google->getTokenResponse(request()->input('id_token'));
if (is_array($user)) {
$query = [
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google',
];
/* Cannot allow duplicates! */
if ($existing_user = MultiDB::hasUser($query)) {
return response()
->json(['message' => 'User already exists in system.'], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
}
if ($user) {
$client = new Google_Client();
$client->setClientId(config('ninja.auth.google.client_id'));
$client->setClientSecret(config('ninja.auth.google.client_secret'));
@ -118,7 +103,6 @@ class ConnectedAccountController extends BaseController
$refresh_token = $token['refresh_token'];
}
$connected_account = [
'password' => '',
'email' => $google->harvestEmail($user),
@ -136,7 +120,8 @@ class ConnectedAccountController extends BaseController
//$ct = CompanyUser::whereUserId(auth()->user()->id);
//return $this->listResponse($ct);
return $this->listResponse(auth()->user());
return $this->itemResponse(auth()->user());
// return $this->listResponse(auth()->user());
}
return response()

View File

@ -18,6 +18,7 @@ use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Services\PdfMaker\Design;
use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\PhantomJS\Phantom;
@ -133,6 +134,10 @@ class PreviewController extends BaseController
if (config('ninja.phantomjs_pdf_generation')) {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation')){
return (new NinjaPdf())->build($maker->getCompiledHTML(true));
}
//else
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
@ -216,6 +221,10 @@ class PreviewController extends BaseController
if (config('ninja.phantomjs_pdf_generation')) {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation')){
return (new NinjaPdf())->build($maker->getCompiledHTML(true));
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());

View File

@ -47,17 +47,28 @@ class TwoFactorController extends BaseController
public function enableTwoFactor()
{
$google2fa = new Google2FA();
$user = auth()->user();
$secret = request()->input('secret');
$oneTimePassword = request()->input('one_time_password');
if (! $secret || ! \Google2FA::verifyKey($secret, $oneTimePassword)) {
return response()->json('message' > ctrans('texts.invalid_one_time_password'));
} elseif (! $user->google_2fa_secret && $user->phone && $user->confirmed) {
$user->google_2fa_secret = encrypt($secret);
$user->save();
}
if($google2fa->verifyKey($secret, $oneTimePassword) && $user->phone && $user->email_verified_at){
return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200);
$user->google_2fa_secret = encrypt($secret);
$user->save();
return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200);
} elseif (! $secret || ! $google2fa->verifyKey($secret, $oneTimePassword)) {
return response()->json(['message' => ctrans('texts.invalid_one_time_password')], 400);
}
return response()->json(['message' => 'No phone record or user is not confirmed'], 400);
}
}

View File

@ -0,0 +1,171 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Livewire;
use App\Factory\ClientFactory;
use App\Models\ClientContact;
use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class BillingPortalPurchase extends Component
{
public $hash;
public $heading_text = 'Log in';
public $email;
public $password;
public $billing_subscription;
public $contact;
protected $rules = [
'email' => ['required', 'email'],
];
public $company_gateway_id;
public $payment_method_id;
public $steps = [
'passed_email' => false,
'existing_user' => false,
'fetched_payment_methods' => false,
'fetched_client' => false,
];
public $methods = [];
public $invoice;
public $coupon;
public function authenticate()
{
$this->validate();
$contact = ClientContact::where('email', $this->email)->first();
if ($contact && $this->steps['existing_user'] === false) {
return $this->steps['existing_user'] = true;
}
if ($contact && $this->steps['existing_user']) {
$attempt = Auth::guard('contact')->attempt(['email' => $this->email, 'password' => $this->password]);
return $attempt
? $this->getPaymentMethods($contact)
: session()->flash('message', 'These credentials do not match our records.');
}
$this->steps['existing_user'] = false;
$contact = $this->createBlankClient();
if ($contact && $contact instanceof ClientContact) {
$this->getPaymentMethods($contact);
}
}
protected function createBlankClient()
{
$company = $this->billing_subscription->company;
$user = $this->billing_subscription->user;
$client_repo = new ClientRepository(new ClientContactRepository());
$client = $client_repo->save([
'name' => 'Client Name',
'contacts' => [
['email' => $this->email],
]
], ClientFactory::create($company->id, $user->id));
return $client->contacts->first();
}
protected function getPaymentMethods(ClientContact $contact): self
{
$this->steps['fetched_payment_methods'] = true;
$this->methods = $contact->client->service()->getPaymentMethods(1000);
$this->heading_text = 'Pick a payment method';
Auth::guard('contact')->login($contact);
$this->contact = $contact;
return $this;
}
public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id)
{
$this->company_gateway_id = $company_gateway_id;
$this->payment_method_id = $gateway_type_id;
$this->handleBeforePaymentEvents();
}
public function handleBeforePaymentEvents()
{
//stubs
$data = [
'client_id' => $this->contact->client->id,
'date' => now()->format('Y-m-d'),
'invitations' => [[
'key' => '',
'client_contact_id' => $this->contact->hashed_id,
]],
'user_input_promo_code' => $this->coupon,
'quantity' => 1, // Option to increase quantity
];
$this->invoice = $this->billing_subscription
->service()
->createInvoice($data)
->service()
->markSent()
->save();
Cache::put($this->hash, [
'email' => $this->email ?? $this->contact->email,
'client_id' => $this->contact->client->id,
'invoice_id' => $this->invoice->id],
now()->addMinutes(60)
);
$this->emit('beforePaymentEventsCompleted');
}
//this isn't managed here - this is taken care of in the BS
public function applyCouponCode()
{
dd('Applying coupon code: ' . $this->coupon);
}
public function render()
{
if ($this->contact instanceof ClientContact) {
$this->getPaymentMethods($this->contact);
}
return render('components.livewire.billing-portal-purchase');
}
}

View File

@ -42,7 +42,7 @@ class PasswordProtection
if($timeout == 0)
$timeout = null;
else
$timeout = now()->addMinutes($timeout);
$timeout = now()->addMinutes($timeout/60000);
if (Cache::get(auth()->user()->hashed_id.'_logged_in')) {
@ -67,7 +67,7 @@ class PasswordProtection
];
//If OAuth and user also has a password set - check both
if ($existing_user = MultiDB::hasUser($query) && auth()->user()->has_password && Hash::check(auth()->user()->password, $request->header('X-API-PASSWORD'))) {
if ($existing_user = MultiDB::hasUser($query) && auth()->user()->has_password && Hash::check(auth()->user()->password, $request->header('X-API-PASSWORD'))) {
Cache::add(auth()->user()->hashed_id.'_logged_in', Str::random(64), $timeout);
return $next($request);

View File

@ -54,7 +54,7 @@ class QueryLogging
nlog($request->method().' - '.$request->url().": $count queries - ".$time);
// if($count > 50)
// Log::nlog($queries);
//nlog($queries);
}
}

View File

@ -19,6 +19,7 @@ use App\Models\Client;
use App\Models\GroupSetting;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
class StoreClientRequest extends Request
{
@ -48,7 +49,6 @@ class StoreClientRequest extends Request
/* Ensure we have a client name, and that all emails are unique*/
//$rules['name'] = 'required|min:1';
$rules['id_number'] = 'unique:clients,id_number,'.$this->id.',id,company_id,'.$this->company_id;
$rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [
@ -66,6 +66,10 @@ class StoreClientRequest extends Request
$rules['hosted_clients'] = new CanStoreClientsRule($this->company_id);
}
$rules['number'] = ['nullable',Rule::unique('clients')->where('company_id', auth()->user()->company()->id)];
$rules['id_number'] = ['nullable',Rule::unique('clients')->where('company_id', auth()->user()->company()->id)];
return $rules;
}
@ -122,7 +126,7 @@ class StoreClientRequest extends Request
public function messages()
{
return [
'unique' => ctrans('validation.unique', ['attribute' => 'email']),
// 'unique' => ctrans('validation.unique', ['attribute' => ['email','number']),
//'required' => trans('validation.required', ['attribute' => 'email']),
'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']),
];

View File

@ -16,6 +16,7 @@ use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdateClientRequest extends Request
{
@ -52,7 +53,14 @@ class UpdateClientRequest extends Request
$rules['country_id'] = 'integer|nullable';
$rules['shipping_country_id'] = 'integer|nullable';
//$rules['id_number'] = 'unique:clients,id_number,,id,company_id,' . auth()->user()->company()->id;
$rules['id_number'] = 'unique:clients,id_number,'.$this->id.',id,company_id,'.$this->company_id;
//$rules['id_number'] = 'unique:clients,id_number,'.$this->id.',id,company_id,'.$this->company_id;
if($this->id_number)
$rules['id_number'] = Rule::unique('clients')->where('company_id', auth()->user()->company()->id)->ignore($this->client->id);
if($this->number)
$rules['number'] = Rule::unique('clients')->where('company_id', auth()->user()->company()->id)->ignore($this->client->id);
$rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [
@ -72,7 +80,6 @@ class UpdateClientRequest extends Request
public function messages()
{
return [
'unique' => ctrans('validation.unique', ['attribute' => 'email']),
'email' => ctrans('validation.email', ['attribute' => 'email']),
'name.required' => ctrans('validation.required', ['attribute' => 'name']),
'required' => ctrans('validation.required', ['attribute' => 'email']),

View File

@ -16,6 +16,7 @@ use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdateCreditRequest extends Request
{
@ -52,9 +53,8 @@ class UpdateCreditRequest extends Request
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
}
if ($this->input('number')) {
$rules['number'] = 'unique:credits,number,'.$this->id.',id,company_id,'.$this->credit->company_id;
}
if($this->number)
$rules['number'] = Rule::unique('credits')->where('company_id', auth()->user()->company()->id)->ignore($this->credit->id);
$rules['line_items'] = 'array';

View File

@ -35,10 +35,9 @@ class StoreExpenseRequest extends Request
{
$rules = [];
if (isset($this->number)) {
if ($this->number)
$rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id);
}
if(!empty($this->client_id))
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id;

View File

@ -16,6 +16,7 @@ use App\Http\ValidationRules\Invoice\LockedInvoiceRule;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdateInvoiceRequest extends Request
{
@ -49,10 +50,8 @@ class UpdateInvoiceRequest extends Request
$rules['id'] = new LockedInvoiceRule($this->invoice);
// if ($this->input('number') && strlen($this->input('number')) >= 1) {
if ($this->input('number')) {
$rules['number'] = 'unique:invoices,number,'.$this->id.',id,company_id,'.$this->invoice->company_id;
}
if($this->number)
$rules['number'] = Rule::unique('invoices')->where('company_id', auth()->user()->company()->id)->ignore($this->invoice->id);
$rules['line_items'] = 'array';

View File

@ -16,6 +16,7 @@ use App\Http\ValidationRules\PaymentAppliedValidAmount;
use App\Http\ValidationRules\ValidCreditsPresentRule;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdatePaymentRequest extends Request
{
@ -35,12 +36,14 @@ class UpdatePaymentRequest extends Request
public function rules()
{
$rules = [
'number' => 'nullable|unique:payments,number,'.$this->id.',id,company_id,'.$this->payment->company_id,
'invoices' => ['array', new PaymentAppliedValidAmount, new ValidCreditsPresentRule],
'invoices.*.invoice_id' => 'distinct',
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
];
if($this->number)
$rules['number'] = Rule::unique('payments')->where('company_id', auth()->user()->company()->id)->ignore($this->payment->id);
if ($this->input('documents') && is_array($this->input('documents'))) {
$documents = count($this->input('documents'));
@ -68,9 +71,9 @@ class UpdatePaymentRequest extends Request
unset($input['amount']);
}
if (isset($input['number'])) {
unset($input['number']);
}
// if (isset($input['number'])) {
// unset($input['number']);
// }
if (isset($input['invoices']) && is_array($input['invoices']) !== false) {
foreach ($input['invoices'] as $key => $value) {

View File

@ -15,6 +15,7 @@ use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdateQuoteRequest extends Request
{
@ -46,9 +47,8 @@ class UpdateQuoteRequest extends Request
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
}
if ($this->input('number')) {
$rules['number'] = 'unique:quotes,number,'.$this->id.',id,company_id,'.$this->quote->company_id;
}
if($this->number)
$rules['number'] = Rule::unique('quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->quote->id);
$rules['line_items'] = 'array';

View File

@ -16,6 +16,7 @@ use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\Rule;
class UpdateRecurringInvoiceRequest extends Request
{
@ -47,9 +48,9 @@ class UpdateRecurringInvoiceRequest extends Request
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
}
if ($this->input('number')) {
$rules['number'] = 'unique:recurring_invoices,number,'.$this->recurring_invoice->id.',id,company_id,'.$this->recurring_invoice->company_id;
}
if($this->number)
$rules['number'] = Rule::unique('recurring_invoices')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_invoice->id);
return $rules;
}

View File

@ -14,6 +14,7 @@ namespace App\Http\Requests\RecurringQuote;
use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use Illuminate\Validation\Rule;
class UpdateRecurringQuoteRequest extends Request
{
@ -47,6 +48,9 @@ class UpdateRecurringQuoteRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
if($this->number)
$rules['number'] = Rule::unique('recurring_quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_quote->id);
$this->replace($input);
}
}

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\TaxRate;
use App\Http\Requests\Request;
use Illuminate\Validation\Rule;
class UpdateTaxRateRequest extends Request
{
@ -27,10 +28,14 @@ class UpdateTaxRateRequest extends Request
public function rules()
{
return [
// 'name' => 'unique:tax_rates,name,'.$this->tax_rate->name.',id,company_id,'.auth()->user()->companyId(),
'name' => 'unique:tax_rates,name,'.$this->id.',id,company_id,'.$this->company_id,
'rate' => 'numeric',
];
$rules = [];
$rules['rate'] = 'numeric';
if($this->number)
$rules['number'] = Rule::unique('tax_rates')->where('company_id', auth()->user()->company()->id)->ignore($this->tax_rate->id);
return $rules;
}
}

View File

@ -23,6 +23,6 @@ class ReconfirmUserRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->id == $this->user->id;
return auth()->user()->id == $this->user->id || auth()->user()->isAdmin();
}
}

View File

@ -14,6 +14,7 @@ namespace App\Http\Requests\Vendor;
use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdateVendorRequest extends Request
{
@ -35,18 +36,15 @@ class UpdateVendorRequest extends Request
/* Ensure we have a client name, and that all emails are unique*/
$rules['country_id'] = 'integer|nullable';
//$rules['id_number'] = 'unique:clients,id_number,,id,company_id,' . auth()->user()->company()->id;
$rules['id_number'] = 'unique:clients,id_number,'.$this->id.',id,company_id,'.$this->company_id;
if($this->number)
$rules['number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id)->ignore($this->vendor->id);
if($this->id_number)
$rules['id_number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id)->ignore($this->vendor->id);
$rules['contacts.*.email'] = 'nullable|distinct';
$contacts = request('contacts');
if (is_array($contacts)) {
// for ($i = 0; $i < count($contacts); $i++) {
// // $rules['contacts.' . $i . '.email'] = 'nullable|email|unique:client_contacts,email,' . isset($contacts[$i]['id'].',company_id,'.$this->company_id);
// //$rules['contacts.' . $i . '.email'] = 'nullable|email';
// }
}
return $rules;
}
@ -54,7 +52,6 @@ class UpdateVendorRequest extends Request
public function messages()
{
return [
'unique' => ctrans('validation.unique', ['attribute' => 'email']),
'email' => ctrans('validation.email', ['attribute' => 'email']),
'name.required' => ctrans('validation.required', ['attribute' => 'name']),
'required' => ctrans('validation.required', ['attribute' => 'email']),

View File

@ -12,6 +12,7 @@
namespace App\Jobs\Entity;
use App\Models\Account;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Design;
@ -24,6 +25,7 @@ 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;
@ -114,10 +116,13 @@ class CreateEntityPdf implements ShouldQueue
$entity_design_id = 'invoice_design_id';
}
$file_path = $path.$this->entity->number.'.pdf';
$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));
if(!$this->company->account->hasFeature(Account::FEATURE_DIFFERENT_DESIGNS))
$entity_design_id = 2;
$design = Design::find($entity_design_id);
$html = new HtmlEngine($this->invitation);
@ -156,7 +161,13 @@ class CreateEntityPdf implements ShouldQueue
$pdf = null;
try {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
if(config('ninja.invoiceninja_hosted_pdf_generation')){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
}
else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}

View File

@ -16,6 +16,7 @@ use App\Events\Invoice\InvoiceWasEmailed;
use App\Jobs\Entity\EmailEntity;
use App\Jobs\Util\WebHookHandler;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\Invoice;
use App\Models\Webhook;
use App\Utils\Ninja;
@ -207,7 +208,7 @@ class SendReminders implements ShouldQueue
$invoice->invitations->each(function ($invitation) use ($template, $invoice) {
//only send if enable_reminder setting is toggled to yes
if ($this->checkSendSetting($invoice, $template)) {
if ($this->checkSendSetting($invoice, $template) && $invoice->company->account->hasFeature(Account::FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
nlog("firing email");
EmailEntity::dispatchNow($invitation, $invitation->company, $template);

View File

@ -80,9 +80,10 @@ class UserEmailChanged implements ShouldQueue
NinjaMailerJob::dispatch($nmo);
$nmo->to_user = $this->new_user;
NinjaMailerJob::dispatch($nmo);
// $nmo->to_user = $this->new_user;
// NinjaMailerJob::dispatch($nmo);
$this->new_user->service()->invite($this->company);
}

View File

@ -214,9 +214,14 @@ class Import implements ShouldQueue
// if($check_data['status'] == 'errors')
// throw new ProcessingMigrationArchiveFailed(implode("\n", $check_data));
Mail::to($this->user->email, $this->user->name())
->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
try{
Mail::to($this->user->email, $this->user->name())
->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
}
catch(\Exception $e) {
nlog($e->getMessage());
}
/*After a migration first some basic jobs to ensure the system is up to date*/
VersionCheck::dispatch();
CompanySizeCheck::dispatch();

View File

@ -88,14 +88,14 @@ class CreditEmailEngine extends BaseEmailEngine
->setViewText(ctrans('texts.view_credit'))
->setInvitation($this->invitation);
if ($this->client->getSetting('pdf_email_attachment') !== false) {
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->credit->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$this->setAttachments([$this->credit->pdf_file_path()]);
// $this->setAttachments(['path' => $this->credit->pdf_file_path(), 'name' => basename($this->credit->pdf_file_path())]);
}
//attach third party documents
if($this->client->getSetting('document_email_attachment') !== false){
if($this->client->getSetting('document_email_attachment') !== false && $this->credit->company->account->hasFeature(Account::FEATURE_DOCUMENTS)){
// Storage::url
foreach($this->credit->documents as $document){

View File

@ -12,6 +12,7 @@
namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Models\Account;
use App\Utils\HtmlEngine;
use App\Utils\Number;
@ -97,14 +98,14 @@ class InvoiceEmailEngine extends BaseEmailEngine
->setViewText(ctrans('texts.view_invoice'))
->setInvitation($this->invitation);
if ($this->client->getSetting('pdf_email_attachment') !== false) {
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->invoice->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$this->setAttachments([$this->invoice->pdf_file_path()]);
// $this->setAttachments(['path' => $this->invoice->pdf_file_path(), 'name' => basename($this->invoice->pdf_file_path())]);
}
//attach third party documents
if($this->client->getSetting('document_email_attachment') !== false){
if($this->client->getSetting('document_email_attachment') !== false && $this->invoice->company->account->hasFeature(Account::FEATURE_DOCUMENTS)){
// Storage::url
foreach($this->invoice->documents as $document){

View File

@ -89,14 +89,14 @@ class QuoteEmailEngine extends BaseEmailEngine
->setInvitation($this->invitation);
if ($this->client->getSetting('pdf_email_attachment') !== false) {
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->quote->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$this->setAttachments([$this->quote->pdf_file_path()]);
//$this->setAttachments(['path' => $this->quote->pdf_file_path(), 'name' => basename($this->quote->pdf_file_path())]);
}
//attach third party documents
if($this->client->getSetting('document_email_attachment') !== false){
if($this->client->getSetting('document_email_attachment') !== false && $this->quote->company->account->hasFeature(Account::FEATURE_DOCUMENTS)){
// Storage::url
foreach($this->quote->documents as $document){

View File

@ -186,6 +186,15 @@ class BaseModel extends Model
*/
public function getFileName($extension = 'pdf')
{
return $this->number.'.'.$extension;
return $this->numberFormatter().'.'.$extension;
}
public function numberFormatter()
{
$formatted_number = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $this->number);
// Remove any runs of periods (thanks falstro!)
$formatted_number = mb_ereg_replace("([\.]{2,})", '', $formatted_number);
return $formatted_number;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Services\BillingSubscription\BillingSubscriptionService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -53,6 +54,11 @@ class BillingSubscription extends BaseModel
'deleted_at' => 'timestamp',
];
public function service()
{
return new BillingSubscriptionService($this);
}
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Company::class);
@ -67,5 +73,4 @@ class BillingSubscription extends BaseModel
{
return $this->belongsTo(Product::class);
}
}

View File

@ -304,6 +304,10 @@ class Client extends BaseModel implements HasLocalePreference
return $this->company->settings->{$setting};
}
elseif( property_exists(CompanySettings::defaults(), $setting) ) {
return CompanySettings::defaults()->{$setting};
}
return '';
// throw new \Exception("Settings corrupted", 1);

View File

@ -21,6 +21,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notification;
use Laracasts\Presenter\PresentableTrait;
use Illuminate\Support\Facades\Cache;
class Company extends BaseModel
{
@ -286,7 +287,7 @@ class Company extends BaseModel
*/
public function country()
{
//return $this->belongsTo(Country::class);
// return $this->belongsTo(Country::class);
return Country::find($this->settings->country_id);
}
@ -342,12 +343,13 @@ class Company extends BaseModel
return null;
}
/**
* @return BelongsTo
*/
public function currency()
{
return $this->belongsTo(Currency::class);
$currencies = Cache::get('currencies');
return $currencies->filter(function ($item) {
return $item->id == $this->settings->currency_id;
})->first();
}
/**

View File

@ -248,9 +248,9 @@ class Credit extends BaseModel
public function pdf_file_path($invitation = null)
{
$storage_path = Storage::url($this->client->credit_filepath().$this->number.'.pdf');
$storage_path = Storage::url($this->client->credit_filepath().$this->numberFormatter().'.pdf');
if (Storage::exists($this->client->credit_filepath().$this->number.'.pdf')) {
if (Storage::exists($this->client->credit_filepath().$this->numberFormatter().'.pdf')) {
return $storage_path;
}

View File

@ -126,9 +126,9 @@ class CreditInvitation extends BaseModel
public function pdf_file_path()
{
$storage_path = Storage::url($this->credit->client->quote_filepath().$this->credit->number.'.pdf');
$storage_path = Storage::url($this->credit->client->quote_filepath().$this->credit->numberFormatter().'.pdf');
if (! Storage::exists($this->credit->client->credit_filepath().$this->credit->number.'.pdf')) {
if (! Storage::exists($this->credit->client->credit_filepath().$this->credit->numberFormatter().'.pdf')) {
event(new CreditWasUpdated($this, $this->company, Ninja::eventVars()));
CreateEntityPdf::dispatchNow($this);
}

View File

@ -74,7 +74,7 @@ class Gateway extends StaticModel
* Returns an array of methods and the gatewaytypes possible
*
* @return array
*///todo remove methods replace with gatewaytype:: and then nest refund / token billing
*/
public function getMethods()
{
switch ($this->id) {

View File

@ -388,9 +388,9 @@ class Invoice extends BaseModel
$invitation = $this->invitations->first();
}
$storage_path = Storage::$type($this->client->invoice_filepath().$this->number.'.pdf');
$storage_path = Storage::$type($this->client->invoice_filepath().$this->numberFormatter().'.pdf');
if (! Storage::exists($this->client->invoice_filepath().$this->number.'.pdf')) {
if (! Storage::exists($this->client->invoice_filepath().$this->numberFormatter().'.pdf')) {
event(new InvoiceWasUpdated($this, $this->company, Ninja::eventVars()));
CreateEntityPdf::dispatchNow($invitation);
}

View File

@ -140,9 +140,9 @@ class InvoiceInvitation extends BaseModel
public function pdf_file_path()
{
$storage_path = Storage::url($this->invoice->client->invoice_filepath().$this->invoice->number.'.pdf');
$storage_path = Storage::url($this->invoice->client->invoice_filepath().$this->invoice->numberFormatter().'.pdf');
if (! Storage::exists($this->invoice->client->invoice_filepath().$this->invoice->number.'.pdf')) {
if (! Storage::exists($this->invoice->client->invoice_filepath().$this->invoice->numberFormatter().'.pdf')) {
event(new InvoiceWasUpdated($this->invoice, $this->company, Ninja::eventVars()));
CreateEntityPdf::dispatchNow($this);
}

View File

@ -37,6 +37,7 @@ class Project extends BaseModel
'custom_value4',
'assigned_user_id',
'color',
'number',
];
public function getEntityType()

View File

@ -208,11 +208,11 @@ class Quote extends BaseModel
$invitation = $this->invitations->first();
}
$storage_path = Storage::$type($this->client->quote_filepath().$this->number.'.pdf');
$storage_path = Storage::$type($this->client->quote_filepath().$this->numberFormatter().'.pdf');
nlog($storage_path);
if (! Storage::exists($this->client->quote_filepath().$this->number.'.pdf')) {
if (! Storage::exists($this->client->quote_filepath().$this->numberFormatter().'.pdf')) {
event(new QuoteWasUpdated($this, $this->company, Ninja::eventVars()));
CreateEntityPdf::dispatchNow($invitation);
}

View File

@ -130,9 +130,9 @@ class QuoteInvitation extends BaseModel
public function pdf_file_path()
{
$storage_path = Storage::url($this->quote->client->quote_filepath().$this->quote->number.'.pdf');
$storage_path = Storage::url($this->quote->client->quote_filepath().$this->quote->numberFormatter().'.pdf');
if (! Storage::exists($this->quote->client->quote_filepath().$this->quote->number.'.pdf')) {
if (! Storage::exists($this->quote->client->quote_filepath().$this->quote->numberFormatter().'.pdf')) {
event(new QuoteWasUpdated($this->quote, $this->company, Ninja::eventVars()));
CreateEntityPdf::dispatchNow($this);
}

View File

@ -52,7 +52,7 @@ class InvoiceObserver
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company);
}
// UnlinkFile::dispatchNow(config('filesystems.default'), $invoice->client->invoice_filepath() . $invoice->number.'.pdf');
// UnlinkFile::dispatchNow(config('filesystems.default'), $invoice->client->invoice_filepath() . $invoice->numberFormatter().'.pdf');
}

View File

@ -30,6 +30,7 @@ use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\Services\BillingSubscription\BillingSubscriptionService;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SystemLogTrait;
@ -207,7 +208,7 @@ class BaseDriver extends AbstractPaymentDriver
public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment
{
$this->confirmGatewayFee();
$payment = PaymentFactory::create($this->client->company->id, $this->client->user->id);
$payment->client_id = $this->client->id;
$payment->company_gateway_id = $this->company_gateway->id;
@ -240,6 +241,8 @@ class BaseDriver extends AbstractPaymentDriver
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
//(new BillingSubscriptionService)->completePurchase($this->payment_hash);
return $payment->service()->applyNumber()->save();
}
@ -345,8 +348,8 @@ class BaseDriver extends AbstractPaymentDriver
}
else if ($e instanceof Exception) {
$error = $e->getMessage();
}
else
}
else
$error = $e->getMessage();
PaymentFailureMailer::dispatch(

View File

@ -31,7 +31,7 @@ class DriverTemplate extends BaseDriver
GatewayType::CREDIT_CARD => CreditCard::class, //maps GatewayType => Implementation class
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE;
const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model
public function setPaymentMethod($payment_method_id)
{

View File

@ -136,7 +136,7 @@ class PayPalExpressPaymentDriver extends BaseDriver
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $response, 'data' => $data],
['response' => (array)$response->getData(), 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL,

View File

@ -23,7 +23,7 @@ use Illuminate\Support\Str;
class ClientContactRepository extends BaseRepository
{
public $is_primary;
public function save(array $data, Client $client) : void
{
if (isset($data['contacts'])) {
@ -37,6 +37,7 @@ class ClientContactRepository extends BaseRepository
});
$this->is_primary = true;
/* Set first record to primary - always */
$contacts = $contacts->sortByDesc('is_primary')->map(function ($contact) {
$contact['is_primary'] = $this->is_primary;

View File

@ -0,0 +1,174 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Services\BillingSubscription;
use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory;
use App\Models\BillingSubscription;
use App\Models\ClientSubscription;
use App\Models\PaymentHash;
use App\Models\Product;
use App\Repositories\InvoiceRepository;
class BillingSubscriptionService
{
/** @var BillingSubscription */
private $billing_subscription;
public function __construct(BillingSubscription $billing_subscription)
{
$this->billing_subscription = $billing_subscription;
}
public function completePurchase(PaymentHash $payment_hash)
{
if (!property_exists($payment_hash, 'billing_context')) {
return;
}
// At this point we have some state carried from the billing page
// to this, available as $payment_hash->data->billing_context. Make something awesome ⭐
// create client subscription record
//
// create recurring invoice if is_recurring
//
}
public function startTrial(array $data)
{
}
public function createInvoice($data): ?\App\Models\Invoice
{
$invoice_repo = new InvoiceRepository();
$data['line_items'] = $this->createLineItems($data);
/*
If trial_enabled -> return early
-- what we need to know that we don't already
-- Has a promo code been entered, and does it match
-- Is this a recurring subscription
--
1. Is this a recurring product?
2. What is the quantity? ie is this a multi seat product ( does this mean we need this value stored in the client sub?)
*/
return $invoice_repo->save($data, InvoiceFactory::create($this->billing_subscription->company_id, $this->billing_subscription->user_id));
}
private function createLineItems($data): array
{
$line_items = [];
$product = $this->billing_subscription->product;
$item = new InvoiceItem;
$item->quantity = $data['quantity'];
$item->product_key = $product->product_key;
$item->notes = $product->notes;
$item->cost = $product->price;
$item->tax_rate1 = $product->tax_rate1 ?: 0;
$item->tax_name1 = $product->tax_name1 ?: '';
$item->tax_rate2 = $product->tax_rate2 ?: 0;
$item->tax_name2 = $product->tax_name2 ?: '';
$item->tax_rate3 = $product->tax_rate3 ?: 0;
$item->tax_name3 = $product->tax_name3 ?: '';
$item->custom_value1 = $product->custom_value1 ?: '';
$item->custom_value2 = $product->custom_value2 ?: '';
$item->custom_value3 = $product->custom_value3 ?: '';
$item->custom_value4 = $product->custom_value4 ?: '';
//$item->type_id need to switch whether the subscription is a service or product
$line_items[] = $item;
//do we have a promocode? enter this as a line item.
if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->billing_subscription->promo_code) && $this->billing_subscription->promo_discount > 0)
$line_items[] = $this->createPromoLine($data);
return $line_items;
}
private function createPromoLine($data)
{
$product = $this->billing_subscription->product;
$discounted_amount = 0;
$discount = 0;
$amount = $data['quantity'] * $product->cost;
if ($this->billing_subscription->is_amount_discount == true) {
$discount = $this->billing_subscription->promo_discount;
}
else {
$discount = round($amount * ($this->billing_subscription->promo_discount / 100), 2);
}
$discounted_amount = $amount - $discount;
$item = new InvoiceItem;
$item->quantity = 1;
$item->product_key = ctrans('texts.promo_code');
$item->notes = ctrans('texts.promo_code');
$item->cost = $discounted_amount;
$item->tax_rate1 = $product->tax_rate1 ?: 0;
$item->tax_name1 = $product->tax_name1 ?: '';
$item->tax_rate2 = $product->tax_rate2 ?: 0;
$item->tax_name2 = $product->tax_name2 ?: '';
$item->tax_rate3 = $product->tax_rate3 ?: 0;
$item->tax_name3 = $product->tax_name3 ?: '';
return $item;
}
private function convertInvoiceToRecurring()
{
//The first invoice is a plain invoice - the second is fired on the recurring schedule.
}
public function createClientSubscription($payment_hash, $recurring_invoice_id = null)
{
//create the client sub record
//?trial enabled?
$cs = new ClientSubscription();
$cs->subscription_id = $this->billing_subscription->id;
$cs->company_id = $this->billing_subscription->company_id;
// client_id
$cs->save();
}
public function triggerWebhook($payment_hash)
{
//hit the webhook to after a successful onboarding
}
public function fireNotifications()
{
//scan for any notification we are required to send
}
}

View File

@ -140,7 +140,7 @@ class CreditService
public function deletePdf()
{
UnlinkFile::dispatchNow(config('filesystems.default'), $this->credit->client->credit_filepath() . $this->credit->number.'.pdf');
UnlinkFile::dispatchNow(config('filesystems.default'), $this->credit->client->credit_filepath() . $this->credit->numberFormatter().'.pdf');
return $this;
}

View File

@ -38,7 +38,7 @@ class GetCreditPdf extends AbstractService
$path = $this->credit->client->credit_filepath();
$file_path = $path.$this->credit->number.'.pdf';
$file_path = $path.$this->credit->numberFormatter().'.pdf';
$disk = config('filesystems.default');

View File

@ -37,7 +37,7 @@ class GetInvoicePdf extends AbstractService
$path = $this->invoice->client->invoice_filepath();
$file_path = $path.$this->invoice->number.'.pdf';
$file_path = $path.$this->invoice->numberFormatter().'.pdf';
$disk = config('filesystems.default');

View File

@ -274,8 +274,8 @@ class InvoiceService
public function deletePdf()
{
//UnlinkFile::dispatchNow(config('filesystems.default'), $this->invoice->client->invoice_filepath() . $this->invoice->number.'.pdf');
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath() . $this->invoice->number.'.pdf');
//UnlinkFile::dispatchNow(config('filesystems.default'), $this->invoice->client->invoice_filepath() . $this->invoice->numberFormatter().'.pdf');
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath() . $this->invoice->numberFormatter().'.pdf');
return $this;
}

View File

@ -302,7 +302,7 @@ class Design extends BaseDesign
public function buildTableHeader(string $type): array
{
$this->processTaxColumns($type);
$this->processCustomColumns($type);
// $this->processCustomColumns($type);
$elements = [];

View File

@ -213,6 +213,7 @@ trait PdfMakerUtilities
$css = <<<'EOT'
table.page-container {
page-break-after: always;
min-width: 100%;
}
thead.page-header {

View File

@ -36,7 +36,7 @@ class GetQuotePdf extends AbstractService
$path = $this->quote->client->quote_filepath();
$file_path = $path.$this->quote->number.'.pdf';
$file_path = $path.$this->quote->numberFormatter().'.pdf';
$disk = config('filesystems.default');

View File

@ -178,7 +178,7 @@ class QuoteService
public function deletePdf()
{
UnlinkFile::dispatchNow(config('filesystems.default'), $this->quote->client->quote_filepath() . $this->quote->number.'.pdf');
UnlinkFile::dispatchNow(config('filesystems.default'), $this->quote->client->quote_filepath() . $this->quote->numberFormatter().'.pdf');
return $this;
}

View File

@ -87,7 +87,7 @@ class RecurringService
public function deletePdf()
{
UnlinkFile::dispatchNow(config('filesystems.default'), $this->recurring_entity->client->recurring_invoice_filepath() . $this->recurring_entity->number.'.pdf');
UnlinkFile::dispatchNow(config('filesystems.default'), $this->recurring_entity->client->recurring_invoice_filepath() . $this->recurring_entity->numberFormatter().'.pdf');
return $this;
}

View File

@ -78,6 +78,7 @@ class AccountTransformer extends EntityTransformer
'is_docker' => (bool) config('ninja.is_docker'),
'is_scheduler_running' => (bool) $account->is_scheduler_running,
'default_company_id' => (string) $this->encodePrimaryKey($account->default_company_id),
'disable_auto_update' => (bool) config('ninja.disable_auto_update'),
];
}

View File

@ -57,6 +57,7 @@ class BillingSubscriptionTransformer extends EntityTransformer
'plan_map' => (string)$billing_subscription->plan_map,
'refund_period' => (int)$billing_subscription->refund_period,
'webhook_configuration' => (string)$billing_subscription->webhook_configuration,
'purchase_page' => (string)route('client.subscription.purchase', $billing_subscription->hashed_id),
'is_deleted' => (bool)$billing_subscription->is_deleted,
'created_at' => (int)$billing_subscription->created_at,
'updated_at' => (int)$billing_subscription->updated_at,

View File

@ -16,6 +16,7 @@ use App\Models\Client;
use App\Models\Document;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Utils\Traits\MakesHash;
class InvoiceTransformer extends EntityTransformer
@ -30,7 +31,7 @@ class InvoiceTransformer extends EntityTransformer
protected $availableIncludes = [
// 'invitations',
'history',
// 'payments',
'payments',
'client',
// 'documents',
];
@ -56,15 +57,15 @@ class InvoiceTransformer extends EntityTransformer
return $this->includeItem($invoice->client, $transformer, Client::class);
}
public function includePayments(Invoice $invoice)
{
$transformer = new PaymentTransformer( $this->serializer);
return $this->includeCollection($invoice->payments, $transformer, Payment::class);
}
/*
public function includePayments(Invoice $invoice)
{
$transformer = new PaymentTransformer($this->account, $this->serializer, $invoice);
return $this->includeCollection($invoice->payments, $transformer, ENTITY_PAYMENT);
}
public function includeExpenses(Invoice $invoice)
{
$transformer = new ExpenseTransformer($this->account, $this->serializer);

View File

@ -0,0 +1,37 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Utils\HostedPDF;
use GuzzleHttp\RequestOptions;
class NinjaPdf
{
private $url = 'https://pdf.invoicing.co/api/';
public function build($html)
{
$client = new \GuzzleHttp\Client(['headers' =>
[
'X-Ninja-Token' => 'test_token_for_now',
]
]);
$response = $client->post($this->url,[
RequestOptions::JSON => ['html' => $html]
]);
return $response->getBody();
}
}

View File

@ -285,6 +285,8 @@ class HtmlEngine
$data['$company.website'] = ['value' => $this->settings->website ?: '&nbsp;', 'label' => ctrans('texts.website')];
$data['$company.address'] = ['value' => $this->company->present()->address($this->settings) ?: '&nbsp;', 'label' => ctrans('texts.address')];
$data['$signature'] = ['value' => $this->settings->email_signature ?: '&nbsp;', 'label' => ''];
$data['$spc_qr_code'] = ['value' => $this->company->present()->getSpcQrCode($this->client->currency()->code, $this->entity->number, $this->entity->balance), 'label' => ''];
$logo = $this->company->present()->logo($this->settings);

View File

@ -11,6 +11,7 @@
namespace App\Utils;
use App\Models\Company;
use App\Models\Currency;
/**
@ -83,17 +84,17 @@ class Number
return floatval($value);
}
/**
* Formats a given value based on the clients currency AND country.
*
* @param floatval $value The number to be formatted
* @param $client
* @param $entity
* @return string The formatted value
*/
public static function formatMoney($value, $client) :string
public static function formatMoney($value, $entity) :string
{
$currency = $client->currency();
$currency = $entity->currency();
$thousand = $currency->thousand_separator;
$decimal = $currency->decimal_separator;
@ -101,29 +102,38 @@ class Number
$code = $currency->code;
$swapSymbol = $currency->swap_currency_symbol;
// App\Models\Client::country() returns instance of BelongsTo.
// App\Models\Company::country() returns record for the country, that's why we check for the instance.
if ($entity instanceof Company) {
$country = $entity->country();
} else {
$country = $entity->country;
}
/* Country settings override client settings */
if (isset($client->country->thousand_separator) && strlen($client->country->thousand_separator) >= 1) {
$thousand = $client->country->thousand_separator;
if (isset($country->thousand_separator) && strlen($country->thousand_separator) >= 1) {
$thousand = $country->thousand_separator;
}
if (isset($client->country->decimal_separator) && strlen($client->country->decimal_separator) >= 1) {
$decimal = $client->country->decimal_separator;
if (isset($country->decimal_separator) && strlen($country->decimal_separator) >= 1) {
$decimal = $country->decimal_separator;
}
if (isset($client->country->swap_currency_symbol) && strlen($client->country->swap_currency_symbol) >= 1) {
$swapSymbol = $client->country->swap_currency_symbol;
if (isset($country->swap_currency_symbol) && strlen($country->swap_currency_symbol) >= 1) {
$swapSymbol = $country->swap_currency_symbol;
}
$value = number_format($value, $precision, $decimal, $thousand);
$symbol = $currency->symbol;
if ($client->getSetting('show_currency_code') === true && $currency->code == 'CHF') {
if ($entity->getSetting('show_currency_code') === true && $currency->code == 'CHF') {
return "{$code} {$value}";
} elseif ($client->getSetting('show_currency_code') === true) {
} elseif ($entity->getSetting('show_currency_code') === true) {
return "{$value} {$code}";
} elseif ($swapSymbol) {
return "{$value} ".trim($symbol);
} elseif ($client->getSetting('show_currency_code') === false) {
} elseif ($entity->getSetting('show_currency_code') === false) {
return "{$symbol}{$value}";
} else {
return self::formatValue($value, $currency);

View File

@ -76,7 +76,7 @@ class Phantom
$path = $entity_obj->client->recurring_invoice_filepath();
}
$file_path = $path.$entity_obj->number.'.pdf';
$file_path = $path.$entity_obj->numberFormatter().'.pdf';
$url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret');
info($url);
@ -91,8 +91,8 @@ class Phantom
$instance = Storage::disk(config('filesystems.default'))->put($file_path, $pdf);
nlog($instance);
nlog($file_path);
// nlog($instance);
// nlog($file_path);
return $file_path;
}

View File

@ -78,9 +78,15 @@ class SystemHealth
'phantom_enabled' => (bool)config('ninja.phantomjs_pdf_generation'),
'exec' => (bool)self::checkExecWorks(),
'open_basedir' => (bool)self::checkOpenBaseDir(),
'mail_mailer' => (string)self::checkMailMailer(),
];
}
public static function checkMailMailer()
{
return config('mail.default');
}
public static function checkOpenBaseDir()
{
if (strlen(ini_get('open_basedir') == 0)) {

View File

@ -134,6 +134,9 @@ trait GeneratesCounter
return 'payment_number_counter';
break;
case Credit::class:
if ($this->hasSharedCounter($client))
return 'invoice_number_counter';
return 'credit_number_counter';
break;
case Project::class:
@ -313,7 +316,7 @@ trait GeneratesCounter
*/
public function hasSharedCounter(Client $client) : bool
{
return (bool) $client->getSetting('shared_invoice_quote_counter');
return (bool) $client->getSetting('shared_invoice_quote_counter') || (bool) $client->getSetting('shared_invoice_credit_counter');
}
/**

View File

@ -34,7 +34,7 @@ trait UserNotifies
array_push($required_permissions, 'all_user_notifications');
}
if (count(array_intersect($required_permissions, $notifications->email)) >= 1 || count(array_intersect($required_permissions, 'all_user_notifications')) >= 1 || count(array_intersect($required_permissions, 'all_notifications')) >= 1) {
if (count(array_intersect($required_permissions, $notifications->email)) >= 1 || count(array_intersect($required_permissions, ['all_user_notifications'])) >= 1 || count(array_intersect($required_permissions, ['all_notifications'])) >= 1) {
array_push($notifiable_methods, 'mail');
}

View File

@ -19,6 +19,7 @@ trait SavesDocuments
{
public function saveDocuments($document_array, $entity, $is_public = true)
{
if ($entity instanceof Company) {
$account = $entity->account;
$company = $entity;

View File

@ -38,7 +38,7 @@
"coconutcraig/laravel-postmark": "^2.10",
"composer/composer": "^2",
"czproject/git-php": "^3.17",
"dacastro4/laravel-gmail": "dev-master",
"dacastro4/laravel-gmail": "^5.1",
"doctrine/dbal": "^2.10",
"fideloper/proxy": "^4.2",
"fzaninotto/faker": "^1.4",
@ -65,12 +65,13 @@
"predis/predis": "^1.1",
"sentry/sentry-laravel": "^2",
"stripe/stripe-php": "^7.50",
"symfony/http-client": "^5.2",
"turbo124/beacon": "^1.0",
"webpatser/laravel-countries": "dev-master#75992ad",
"wildbit/swiftmailer-postmark": "^3.3"
},
"require-dev": {
"php": "^7.4",
"php": "^7.3|^7.4",
"anahkiasen/former": "^4.2",
"barryvdh/laravel-debugbar": "^3.4",
"brianium/paratest": "^6.1",

158
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d9ca08ed1ffaa0ed07c40e014a96d52a",
"content-hash": "113acad46f6d9eea9f9f5bd4501428d1",
"packages": [
{
"name": "authorizenet/authorizenet",
@ -55,16 +55,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.173.25",
"version": "3.173.28",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "69e4653acf8cd855e9010ec4e0e0a7d074c77ee1"
"reference": "593baa5930896bb443c437032daf4016e1e3878d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/69e4653acf8cd855e9010ec4e0e0a7d074c77ee1",
"reference": "69e4653acf8cd855e9010ec4e0e0a7d074c77ee1",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/593baa5930896bb443c437032daf4016e1e3878d",
"reference": "593baa5930896bb443c437032daf4016e1e3878d",
"shasum": ""
},
"require": {
@ -139,9 +139,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.173.25"
"source": "https://github.com/aws/aws-sdk-php/tree/3.173.28"
},
"time": "2021-03-09T19:14:56+00:00"
"time": "2021-03-12T19:29:55+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -1011,16 +1011,16 @@
},
{
"name": "dacastro4/laravel-gmail",
"version": "dev-master",
"version": "v5.1",
"source": {
"type": "git",
"url": "https://github.com/dacastro4/laravel-gmail.git",
"reference": "a7137e7e7aa0672ec963e4d24da6c5f22a0208c0"
"reference": "6d4cabe04f8cdd02b25ef73a1a489099b5e790bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dacastro4/laravel-gmail/zipball/a7137e7e7aa0672ec963e4d24da6c5f22a0208c0",
"reference": "a7137e7e7aa0672ec963e4d24da6c5f22a0208c0",
"url": "https://api.github.com/repos/dacastro4/laravel-gmail/zipball/6d4cabe04f8cdd02b25ef73a1a489099b5e790bd",
"reference": "6d4cabe04f8cdd02b25ef73a1a489099b5e790bd",
"shasum": ""
},
"require": {
@ -1031,16 +1031,15 @@
"illuminate/routing": "~5.8|^6.0|^7.0|^8.0",
"illuminate/session": "~5.8|^6.0|^7.0|^8.0",
"illuminate/support": "~5.8|^6.0|^7.0|^8.0",
"php": "^7.3|^8.0",
"php": "^7.2",
"swiftmailer/swiftmailer": "~5.8|^6.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^4.0|^5.0|^6.0",
"phpunit/phpunit": "^8.5|^9.0",
"orchestra/testbench": "^4.0",
"phpunit/phpunit": "^8.5",
"squizlabs/php_codesniffer": "~3.4"
},
"default-branch": true,
"type": "library",
"extra": {
"laravel": {
@ -1076,9 +1075,9 @@
],
"support": {
"issues": "https://github.com/dacastro4/laravel-gmail/issues",
"source": "https://github.com/dacastro4/laravel-gmail/tree/master"
"source": "https://github.com/dacastro4/laravel-gmail/tree/v5.1"
},
"time": "2021-03-02T17:18:12+00:00"
"time": "2020-12-13T19:17:07+00:00"
},
{
"name": "dasprid/enum",
@ -5447,16 +5446,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.5",
"version": "3.0.6",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "7c751ea006577e4c2e83326d90c8b1e8c11b8ede"
"reference": "906a5fafabe5e6ba51ef3dc65b2722a677908837"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/7c751ea006577e4c2e83326d90c8b1e8c11b8ede",
"reference": "7c751ea006577e4c2e83326d90c8b1e8c11b8ede",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/906a5fafabe5e6ba51ef3dc65b2722a677908837",
"reference": "906a5fafabe5e6ba51ef3dc65b2722a677908837",
"shasum": ""
},
"require": {
@ -5538,7 +5537,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.5"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.6"
},
"funding": [
{
@ -5554,7 +5553,7 @@
"type": "tidelift"
}
],
"time": "2021-02-12T16:18:16+00:00"
"time": "2021-03-10T13:58:31+00:00"
},
{
"name": "pragmarx/google2fa",
@ -6097,16 +6096,16 @@
},
{
"name": "psy/psysh",
"version": "v0.10.6",
"version": "v0.10.7",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "6f990c19f91729de8b31e639d6e204ea59f19cf3"
"reference": "a395af46999a12006213c0c8346c9445eb31640c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/6f990c19f91729de8b31e639d6e204ea59f19cf3",
"reference": "6f990c19f91729de8b31e639d6e204ea59f19cf3",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/a395af46999a12006213c0c8346c9445eb31640c",
"reference": "a395af46999a12006213c0c8346c9445eb31640c",
"shasum": ""
},
"require": {
@ -6167,9 +6166,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.10.6"
"source": "https://github.com/bobthecow/psysh/tree/v0.10.7"
},
"time": "2021-01-18T15:53:43+00:00"
"time": "2021-03-14T02:14:56+00:00"
},
{
"name": "ralouphie/getallheaders",
@ -7056,16 +7055,16 @@
},
{
"name": "symfony/console",
"version": "v5.2.4",
"version": "v5.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "d6d0cc30d8c0fda4e7b213c20509b0159a8f4556"
"reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/d6d0cc30d8c0fda4e7b213c20509b0159a8f4556",
"reference": "d6d0cc30d8c0fda4e7b213c20509b0159a8f4556",
"url": "https://api.github.com/repos/symfony/console/zipball/938ebbadae1b0a9c9d1ec313f87f9708609f1b79",
"reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79",
"shasum": ""
},
"require": {
@ -7133,7 +7132,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.2.4"
"source": "https://github.com/symfony/console/tree/v5.2.5"
},
"funding": [
{
@ -7149,7 +7148,7 @@
"type": "tidelift"
}
],
"time": "2021-02-23T10:08:49+00:00"
"time": "2021-03-06T13:42:15+00:00"
},
{
"name": "symfony/css-selector",
@ -7879,16 +7878,16 @@
},
{
"name": "symfony/http-kernel",
"version": "v5.2.4",
"version": "v5.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "c452dbe4f385f030c3957821bf921b13815d6140"
"reference": "b8c63ef63c2364e174c3b3e0ba0bf83455f97f73"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/c452dbe4f385f030c3957821bf921b13815d6140",
"reference": "c452dbe4f385f030c3957821bf921b13815d6140",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/b8c63ef63c2364e174c3b3e0ba0bf83455f97f73",
"reference": "b8c63ef63c2364e174c3b3e0ba0bf83455f97f73",
"shasum": ""
},
"require": {
@ -7971,7 +7970,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v5.2.4"
"source": "https://github.com/symfony/http-kernel/tree/v5.2.5"
},
"funding": [
{
@ -7987,20 +7986,20 @@
"type": "tidelift"
}
],
"time": "2021-03-04T18:05:55+00:00"
"time": "2021-03-10T17:07:35+00:00"
},
{
"name": "symfony/mime",
"version": "v5.2.4",
"version": "v5.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "5155d2fe14ef1eb150e3bdbbc1ec1455df95e9cd"
"reference": "554ba128f1955038b45db5e1fa7e93bfc683b139"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/5155d2fe14ef1eb150e3bdbbc1ec1455df95e9cd",
"reference": "5155d2fe14ef1eb150e3bdbbc1ec1455df95e9cd",
"url": "https://api.github.com/repos/symfony/mime/zipball/554ba128f1955038b45db5e1fa7e93bfc683b139",
"reference": "554ba128f1955038b45db5e1fa7e93bfc683b139",
"shasum": ""
},
"require": {
@ -8011,12 +8010,13 @@
"symfony/polyfill-php80": "^1.15"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<4.4"
},
"require-dev": {
"egulias/email-validator": "^2.1.10",
"egulias/email-validator": "^2.1.10|^3.1",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/property-access": "^4.4|^5.1",
@ -8053,7 +8053,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v5.2.4"
"source": "https://github.com/symfony/mime/tree/v5.2.5"
},
"funding": [
{
@ -8069,7 +8069,7 @@
"type": "tidelift"
}
],
"time": "2021-02-15T18:55:04+00:00"
"time": "2021-03-07T16:08:20+00:00"
},
{
"name": "symfony/options-resolver",
@ -9352,16 +9352,16 @@
},
{
"name": "symfony/translation",
"version": "v5.2.4",
"version": "v5.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "74b0353ab34ff4cca827a2cf909e325d96815e60"
"reference": "0947ab1e3aabd22a6bef393874b2555d2bb976da"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/74b0353ab34ff4cca827a2cf909e325d96815e60",
"reference": "74b0353ab34ff4cca827a2cf909e325d96815e60",
"url": "https://api.github.com/repos/symfony/translation/zipball/0947ab1e3aabd22a6bef393874b2555d2bb976da",
"reference": "0947ab1e3aabd22a6bef393874b2555d2bb976da",
"shasum": ""
},
"require": {
@ -9425,7 +9425,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v5.2.4"
"source": "https://github.com/symfony/translation/tree/v5.2.5"
},
"funding": [
{
@ -9441,7 +9441,7 @@
"type": "tidelift"
}
],
"time": "2021-03-04T15:41:09+00:00"
"time": "2021-03-06T07:59:01+00:00"
},
{
"name": "symfony/translation-contracts",
@ -9523,16 +9523,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v5.2.4",
"version": "v5.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "6a81fec0628c468cf6d5c87a4d003725e040e223"
"reference": "002ab5a36702adf0c9a11e6d8836623253e9045e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/6a81fec0628c468cf6d5c87a4d003725e040e223",
"reference": "6a81fec0628c468cf6d5c87a4d003725e040e223",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/002ab5a36702adf0c9a11e6d8836623253e9045e",
"reference": "002ab5a36702adf0c9a11e6d8836623253e9045e",
"shasum": ""
},
"require": {
@ -9591,7 +9591,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v5.2.4"
"source": "https://github.com/symfony/var-dumper/tree/v5.2.5"
},
"funding": [
{
@ -9607,7 +9607,7 @@
"type": "tidelift"
}
],
"time": "2021-02-18T23:11:19+00:00"
"time": "2021-03-06T07:59:01+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@ -11150,16 +11150,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v2.18.2",
"version": "v2.18.3",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
"reference": "18f8c9d184ba777380794a389fabc179896ba913"
"reference": "ab99202fccff2a9f97592fbe1b5c76dd06df3513"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/18f8c9d184ba777380794a389fabc179896ba913",
"reference": "18f8c9d184ba777380794a389fabc179896ba913",
"url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/ab99202fccff2a9f97592fbe1b5c76dd06df3513",
"reference": "ab99202fccff2a9f97592fbe1b5c76dd06df3513",
"shasum": ""
},
"require": {
@ -11221,6 +11221,7 @@
"tests/Test/IntegrationCaseFactoryInterface.php",
"tests/Test/InternalIntegrationCaseFactory.php",
"tests/Test/IsIdenticalConstraint.php",
"tests/Test/TokensWithObservedTransformers.php",
"tests/TestCase.php"
]
},
@ -11241,7 +11242,7 @@
"description": "A tool to automatically fix PHP code style",
"support": {
"issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
"source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.2"
"source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.3"
},
"funding": [
{
@ -11249,7 +11250,7 @@
"type": "github"
}
],
"time": "2021-01-26T00:22:21+00:00"
"time": "2021-03-10T19:39:05+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@ -13467,16 +13468,16 @@
},
{
"name": "swagger-api/swagger-ui",
"version": "v3.44.1",
"version": "v3.45.0",
"source": {
"type": "git",
"url": "https://github.com/swagger-api/swagger-ui.git",
"reference": "db830fbb51ba987dc9931172c891b318ba444d39"
"reference": "1ba7af074f97c872a64a415e0507c11cf8f3601b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/db830fbb51ba987dc9931172c891b318ba444d39",
"reference": "db830fbb51ba987dc9931172c891b318ba444d39",
"url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/1ba7af074f97c872a64a415e0507c11cf8f3601b",
"reference": "1ba7af074f97c872a64a415e0507c11cf8f3601b",
"shasum": ""
},
"type": "library",
@ -13522,9 +13523,9 @@
],
"support": {
"issues": "https://github.com/swagger-api/swagger-ui/issues",
"source": "https://github.com/swagger-api/swagger-ui/tree/v3.44.1"
"source": "https://github.com/swagger-api/swagger-ui/tree/v3.45.0"
},
"time": "2021-03-04T18:13:06+00:00"
"time": "2021-03-11T17:20:14+00:00"
},
{
"name": "symfony/debug",
@ -13727,16 +13728,16 @@
},
{
"name": "symfony/yaml",
"version": "v5.2.4",
"version": "v5.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "7d6ae0cce3c33965af681a4355f1c4de326ed277"
"reference": "298a08ddda623485208506fcee08817807a251dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/7d6ae0cce3c33965af681a4355f1c4de326ed277",
"reference": "7d6ae0cce3c33965af681a4355f1c4de326ed277",
"url": "https://api.github.com/repos/symfony/yaml/zipball/298a08ddda623485208506fcee08817807a251dd",
"reference": "298a08ddda623485208506fcee08817807a251dd",
"shasum": ""
},
"require": {
@ -13782,7 +13783,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v5.2.4"
"source": "https://github.com/symfony/yaml/tree/v5.2.5"
},
"funding": [
{
@ -13798,7 +13799,7 @@
"type": "tidelift"
}
],
"time": "2021-02-22T15:48:39+00:00"
"time": "2021-03-06T07:59:01+00:00"
},
{
"name": "theseer/tokenizer",
@ -14080,7 +14081,6 @@
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"dacastro4/laravel-gmail": 20,
"webpatser/laravel-countries": 20
},
"prefer-stable": true,

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.1.24',
'app_version' => '5.1.29',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),
@ -143,4 +143,6 @@ return [
'v4_migration_version' => '4.5.31',
'flutter_canvas_kit' => env('FLUTTER_CANVAS_KIT', false),
'webcron_secret' => env('WEBCRON_SECRET', false),
'disable_auto_update' => env('DISABLE_AUTO_UPDATE', false),
'invoiceninja_hosted_pdf_generation' => env('NINJA_HOSTED_PDF', false),
];

View File

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

View File

@ -0,0 +1,65 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUniqueConstraintsOnAllEntities extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('expenses', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('tasks', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('vendors', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('payments', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('projects', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('clients', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('payment_hashes', function (Blueprint $table) {
$table->unique(['hash']);
});
Schema::table('recurring_invoices', function (Blueprint $table) {
$table->string('number')->change();
$table->unique(['company_id', 'number']);
});
Schema::table('recurring_invoice_invitations', function (Blueprint $table) {
$table->unique(['client_contact_id', 'recurring_invoice_id'],'recur_invoice_client_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddInvoiceIdToClientSubscriptionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('client_subscriptions', function (Blueprint $table) {
$table->unsignedInteger('invoice_id')->nullable();
$table->unsignedInteger('quantity')->default(1);
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade')->onUpdate('cascade');
});
}
/**
* 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

@ -30,7 +30,7 @@ const RESOURCES = {
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "3e722fd57a6db80ee119f0e2c230ccff",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"/": "23224b5e03519aaa87594403d54412cf",
"main.dart.js": "c11c4d2efa9e671a88eb792d18c2296e",
"main.dart.js": "114d8affe0f4b7576170753cf9fb4c0a",
"version.json": "b7c8971e1ab5b627fd2a4317c52b843e",
"favicon.png": "dca91c54388f52eded692718d5a98b8b"
};

212263
public/main.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=745170b7d7a4dc7469f2",
"/css/app.css": "/css/app.css?id=e8d6d5e8cb60bc2f15b3",
"/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

@ -1976,7 +1976,7 @@ $LANG = array(
'require_invoice_signature_help' => 'Require client to provide their signature.',
'require_quote_signature' => 'Quote Signature',
'require_quote_signature_help' => 'Require client to provide their signature.',
'i_agree' => 'I Agree To',
'i_agree' => 'I Agree To The Terms',
'sign_here' => 'Please sign here:',
'authorization' => 'Authorization',
'signed' => 'Signed',
@ -3449,10 +3449,10 @@ $LANG = array(
'client_country' => 'Client Country',
'client_is_active' => 'Client is Active',
'client_balance' => 'Client Balance',
'client_address1' => 'Client Address 1',
'client_address2' => 'Client Address 2',
'client_shipping_address1' => 'Client Shipping Address 1',
'client_shipping_address2' => 'Client Shipping Address 2',
'client_address1' => 'Client Street',
'client_address2' => 'Client Apt/Suite',
'client_shipping_address1' => 'Client Shipping Street',
'client_shipping_address2' => 'Client Shipping Apt/Suite',
'tax_rate1' => 'Tax Rate 1',
'tax_rate2' => 'Tax Rate 2',
'tax_rate3' => 'Tax Rate 3',
@ -3532,8 +3532,8 @@ $LANG = array(
'marked_credit_as_sent' => 'Successfully marked credit as sent',
'email_subject_payment_partial' => 'Email Partial Payment Subject',
'is_approved' => 'Is Approved',
'migration_went_wrong' => 'Oops, something went wrong! Make sure you did proper setup with V2 of Invoice Ninja, before starting migration.',
'cross_migration_message' => 'Cross account migration is not allowed. Please read more about it here: <a href="https://invoiceninja.github.io/cross-site-migration.html">https://invoiceninja.github.io/cross-site-migration.html</a>',
'migration_went_wrong' => 'Oops, something went wrong! Please make sure you have setup an Invoice Ninja v5 instance before starting the migration.',
'cross_migration_message' => 'Cross account migration is not allowed. Please read more about it here: <a href="https://invoiceninja.github.io/docs/migration/#troubleshooting">https://invoiceninja.github.io/docs/migration/#troubleshooting</a>',
'email_credit' => 'Email Credit',
'client_email_not_set' => 'Client does not have an email address set',
'ledger' => 'Ledger',
@ -3912,7 +3912,7 @@ $LANG = array(
'show' => 'Show',
'empty_columns' => 'Empty Columns',
'project_name' => 'Project Name',
'counter_pattern_error' => 'To use :client_counter please add either :number or :id_number to prevent conflicts',
'counter_pattern_error' => 'To use :client_counter please add either :client_number or :client_id_number to prevent conflicts',
'this_quarter' => 'This Quarter',
'to_update_run' => 'To update run',
'registration_url' => 'Registration URL',
@ -3968,8 +3968,8 @@ $LANG = array(
'list_of_recurring_invoices' => 'List of recurring invoices',
'details_of_recurring_invoice' => 'Here are some details about recurring invoice',
'cancellation' => 'Cancellation',
'about_cancellation' => 'In case you want to stop the recurring invoice, please click the request the cancellation.',
'cancellation_warning' => 'Warning! You are requesting a cancellation of this service. Your service may be cancelled with no further notification to you.',
'about_cancellation' => 'In case you want to stop the recurring invoice,\n please click the request the cancellation.',
'cancellation_warning' => 'Warning! You are requesting a cancellation of this service.\n Your service may be cancelled with no further notification to you.',
'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!',
'list_of_payments' => 'List of payments',
'payment_details' => 'Details of the payment',
@ -4135,19 +4135,26 @@ $LANG = array(
'payment_message_extended' => 'Thank you for your payment of :amount for :invoice',
'online_payments_minimum_note' => 'Note: Online payments are supported only if amount is bigger than $1 or currency equivalent.',
'payment_token_not_found' => 'Payment token not found, please try again. If an issue still persist, try with another payment method',
/////////////////////////////////////////////////
'vendor_address1' => 'Vendor Street',
'vendor_address2' => 'Vendor Apt/Suite',
'partially_unapplied' => 'Partially Unapplied',
'select_a_gmail_user' => 'Please select a user authenticated with Gmail',
'list_long_press' => 'List Long Press',
'show_actions' => 'Show Actions',
'start_multiselect' => 'Start Multiselect',
'email_sent_to_confirm_email' => 'An email has been sent to confirm the email address',
'converted_paid_to_date' => 'Converted Paid to Date',
'converted_credit_balance' => 'Converted Credit Balance',
'converted_total' => 'Converted Total',
'reply_to_name' => 'Reply-To Name',
'payment_status_-2' => 'Partially Unapplied',
'color_theme' => 'Color Theme',
'start_migration' => 'Start Migration',
'recurring_cancellation_request' => 'Request for recurring invoice cancellation from :contact',
'recurring_cancellation_request_body' => ':contact from Client :client requested to cancel Recurring Invoice :invoice',
'hello' => 'Hello',
'group_documents' => 'Group documents',
'quote_approval_confirmation_label' => 'Are you sure you want to approve this quote?',
'click_agree_to_accept_terms' => 'Click "Agree" to Accept Terms.',
'agree' => 'Agree',
'pending_approval' => 'Pending Approval',
'migration_select_company_label' => 'Select companies to migrate',
'force_migration' => 'Force migration',
'require_password_with_social_login' => 'Require Password with Social Login',
@ -4167,6 +4174,32 @@ $LANG = array(
'zoho' => 'Zoho',
'accounting' => 'Accounting',
'required_files_missing' => 'Please provide all CSVs.',
'migration_auth_label' => 'Let\'s continue by authenticating.',
'api_secret' => 'API secret',
'migration_api_secret_notice' => 'You can find API_SECRET in the .env file or Invoice Ninja v5. If property is missing, leave field blank.',
'use_last_email' => 'Use last email',
'activate_company' => 'Activate Company',
'activate_company_help' => 'Enable emails, recurring invoices and notifications',
'an_error_occurred_try_again' => 'An error occurred, please try again',
'please_first_set_a_password' => 'Please first set a password',
'changing_phone_disables_two_factor' => 'Warning: Changing your phone number will disable 2FA',
'help_translate' => 'Help Translate',
'please_select_a_country' => 'Please select a country',
'disabled_two_factor' => 'Successfully disabled 2FA',
'connected_google' => 'Successfully connected account',
'disconnected_google' => 'Successfully disconnected account',
'delivered' => 'Delivered',
'spam' => 'Spam',
'view_docs' => 'View Docs',
'enter_phone_to_enable_two_factor' => 'Please provide a mobile phone number to enable two factor authentication',
'send_sms' => 'Send SMS',
'sms_code' => 'SMS Code',
'connect_google' => 'Connect Google',
'disconnect_google' => 'Disconnect Google',
'disable_two_factor' => 'Disable Two Factor',
'invoice_task_datelog' => 'Invoice Task Datelog',
'invoice_task_datelog_help' => 'Add date details to the invoice line items',
'promo_code' => 'Promo code',
);
return $LANG;

View File

@ -0,0 +1,17 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', $billing_subscription->product->product_key)
@section('body')
@livewire('billing-portal-purchase', ['billing_subscription' => $billing_subscription, 'contact' => auth('contact')->user(), 'hash' => $hash])
@stop
@push('footer')
<script>
function updateGatewayFields(companyGatewayId, paymentMethodId) {
document.getElementById('company_gateway_id').value = companyGatewayId;
document.getElementById('payment_method_id').value = paymentMethodId;
}
Livewire.on('beforePaymentEventsCompleted', () => document.getElementById('payment-method-form').submit());
</script>
@endpush

View File

@ -10,7 +10,7 @@
<br>
<br>
<p>
{{ $signature }}
{!! $signature !!}
</p>
@endif

View File

@ -10,7 +10,7 @@
<br>
<br>
<p>
{{ $signature }}
{!! $signature !!}
</p>
@endif

View File

@ -27,7 +27,7 @@
@if($signature)
<tr>
<td>
<p>{{ $signature }}</p>
<p>{!! $signature !!}</p>
</td>
</tr>
@endif

View File

@ -262,7 +262,7 @@
<div class="contacts-wrapper">
<div class="contact-wrapper-left-side">
<p class="contact-label">$to_label:</p>
<p class="contact-label">$from_label:</p>
<div class="company-info">
<div id="company-details"></div>
<div id="company-address"></div>
@ -270,7 +270,7 @@
</div>
<div class="contact-wrapper-right-side">
<p class="contact-label">$from_label:</p>
<p class="contact-label">$to_label:</p>
<div id="client-details"></div>
</div>
</div>

View File

@ -8,7 +8,9 @@
@include('portal.ninja2020.components.general.sidebar.mobile')
<!-- Static sidebar for desktop -->
@include('portal.ninja2020.components.general.sidebar.desktop')
@unless(request()->query('sidebar') === 'hidden')
@include('portal.ninja2020.components.general.sidebar.desktop')
@endunless
<div class="flex flex-col w-0 flex-1 overflow-hidden">
@include('portal.ninja2020.components.general.sidebar.header')
@ -34,4 +36,4 @@
<script>
</script>
</script>

View File

@ -0,0 +1,127 @@
<div class="grid grid-cols-12">
<div class="col-span-12 lg:col-span-6 bg-gray-50 shadow-lg lg:h-screen flex flex-col items-center">
<div class="w-full p-10 lg:w-1/2 lg:mt-48 lg:p-0">
<img class="h-8" src="{{ $billing_subscription->company->present()->logo }}"
alt="{{ $billing_subscription->company->present()->name }}">
<h1 id="billing-page-company-logo" class="text-3xl font-bold tracking-wide mt-8">
{{ $billing_subscription->product->product_key }}
</h1>
<p class="my-6">{{ $billing_subscription->product->notes }}</p>
<span class="text-sm uppercase font-bold">{{ ctrans('texts.total') }}:</span>
<h1 class="text-2xl font-bold tracking-wide">{{ App\Utils\Number::formatMoney($billing_subscription->product->price, $billing_subscription->company) }}</h1>
@if(auth('contact')->user())
<a href="{{ route('client.invoices.index') }}" class="block mt-16 inline-flex items-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-arrow-left">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
<span>{{ ctrans('texts.client_portal') }}</span>
</a>
@endif
</div>
</div>
<div class="col-span-12 lg:col-span-6 bg-white lg:shadow-lg 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">
<h2 class="text-2xl font-bold tracking-wide">{{ $heading_text }}</h2>
@if (session()->has('message'))
@component('portal.ninja2020.components.message')
{{ session('message') }}
@endcomponent
@endif
@if($this->steps['fetched_payment_methods'])
<div class="flex items-center mt-4 text-sm">
<form action="{{ route('client.payments.process', ['hash' => $hash, 'sidebar' => 'hidden']) }}"
method="post"
id="payment-method-form">
@csrf
@if($invoice instanceof \App\Models\Invoice)
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="payable_invoices[0][amount]"
value="{{ $invoice->partial > 0 ? \App\Utils\Number::formatValue($invoice->partial, $invoice->client->currency()) : \App\Utils\Number::formatValue($invoice->balance, $invoice->client->currency()) }}">
<input type="hidden" name="payable_invoices[0][invoice_id]"
value="{{ $invoice->hashed_id }}">
@endif
<input type="hidden" name="action" value="payment">
<input type="hidden" name="company_gateway_id" value="{{ $company_gateway_id }}"/>
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}"/>
</form>
@foreach($this->methods as $method)
<button
wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"
class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
{{ $method['label'] }}
</button>
@endforeach
</div>
@else
<form wire:submit.prevent="authenticate" class="mt-8">
@csrf
<label for="email_address">
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
<input wire:model.defer="email" type="email" class="input w-full"/>
@error('email')
<p class="validation validation-fail block w-full" role="alert">
{{ $message }}
</p>
@enderror
</label>
@if($steps['existing_user'])
<label for="password" class="block mt-2">
<span class="input-label">{{ ctrans('texts.password') }}</span>
<input wire:model.defer="password" type="password" class="input w-full" autofocus/>
@error('password')
<p class="validation validation-fail block w-full" role="alert">
{{ $message }}
</p>
@enderror
</label>
@endif
<button type="submit"
class="button button-block bg-primary text-white mt-4">{{ ctrans('texts.next') }}</button>
</form>
@endif
<div class="relative mt-8">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</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>
</div>
</div>
<form wire:submit.prevent="applyCouponCode" class="mt-4">
@csrf
<div class="flex items-center">
<label class="w-full mr-2">
<input type="text" wire:model.defer="coupon" class="input w-full m-0" />
</label>
<button class="button bg-primary m-0 text-white">{{ ctrans('texts.apply') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -63,6 +63,8 @@
{{-- 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>
@ -77,6 +79,8 @@
@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(){

View File

@ -31,10 +31,13 @@
<div>
@yield('gateway_content')
</div>
<span class="block mx-4 mb-4 text-xs inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
<span class="ml-1">Secure 256-bit encryption</span>
</span>
@if(Request::isSecure())
<span class="block mx-4 mb-4 text-xs inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
<span class="ml-1">Secure 256-bit encryption</span>
</span>
@endif
</div>
</div>
@endsection

View File

@ -20,7 +20,7 @@
-- Commands to create a MySQL database and user
CREATE SCHEMA `db-ninja-01` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE USER 'ninja'@'localhost' IDENTIFIED BY 'ninja';
GRANT ALL PRIVILEGES ON `ninja`.* TO 'ninja'@'localhost';
GRANT ALL PRIVILEGES ON `db-ninja-01`.* TO 'ninja'@'localhost';
FLUSH PRIVILEGES;
</pre>
</details>

View File

@ -76,6 +76,8 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
});
Route::get('client/subscription/{billing_subscription}/purchase', 'ClientPortal\BillingSubscriptionPurchaseController@index')->name('client.subscription.purchase');
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
/*Invitation catches*/
Route::get('recurring_invoice/{invitation_key}', 'ClientPortal\InvitationController@recurringRouter');

View File

@ -54,6 +54,28 @@ class ClientApiTest extends TestCase
$response->assertStatus(200);
}
public function testDuplicateNumberCatch()
{
$data = [
'name' => $this->faker->firstName,
'number' => 'iamaduplicate',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients', $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients', $data);
$response->assertStatus(302);
}
public function testClientPut()
{
$data = [
@ -67,6 +89,20 @@ class ClientApiTest extends TestCase
])->put('/api/v1/clients/'.$this->encodePrimaryKey($this->client->id), $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/clients/'.$this->encodePrimaryKey($this->client->id), $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
$response->assertStatus(302);
}
public function testClientGet()

View File

@ -131,4 +131,80 @@ class CreditTest extends TestCase
$response->assertStatus(200);
}
public function testDuplicateNumberCatch()
{
$data = [
'status_id' => 1,
'number' => 'dfdfd',
'discount' => 0,
'is_amount_discount' => 1,
'number' => '3434343',
'public_notes' => 'notes',
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $data);
$response->assertStatus(302);
}
public function testCreditPut()
{
$data = [
'status_id' => 1,
'number' => 'dfdfd',
'discount' => 0,
'is_amount_discount' => 1,
'number' => '3434343',
'public_notes' => 'notes',
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/credits/'.$this->encodePrimaryKey($this->credit->id), $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/credits/'.$this->encodePrimaryKey($this->credit->id), $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/', $data);
$response->assertStatus(302);
}
}

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