diff --git a/app/Factory/ClientFactory.php b/app/Factory/ClientFactory.php index 35b600618c..b27d0101a9 100644 --- a/app/Factory/ClientFactory.php +++ b/app/Factory/ClientFactory.php @@ -28,7 +28,7 @@ class ClientFactory $client->public_notes = ''; $client->balance = 0; $client->paid_to_date = 0; - $client->country_id = 840; + $client->country_id = null; $client->is_deleted = 0; $client->client_hash = Str::random(40); $client->settings = ClientSettings::defaults(); diff --git a/app/Factory/CompanyFactory.php b/app/Factory/CompanyFactory.php index 4f62bca9fd..543c297484 100644 --- a/app/Factory/CompanyFactory.php +++ b/app/Factory/CompanyFactory.php @@ -45,7 +45,7 @@ class CompanyFactory $company->enabled_modules = config('ninja.enabled_modules'); //32767;//8191; //4095 $company->default_password_timeout = 1800000; - + $company->markdown_email_enabled = true; return $company; } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 950333cf3a..c6fed355f5 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -16,6 +16,7 @@ use App\Jobs\Account\CreateAccount; use App\Models\Account; use App\Models\CompanyUser; use App\Transformers\CompanyUserTransformer; +use App\Utils\TruthSource; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Response; @@ -150,7 +151,11 @@ class AccountController extends BaseController $ct = CompanyUser::whereUserId(auth()->user()->id); - config(['ninja.company_id' => $ct->first()->company->id]); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($ct->first()); + $truth->setUser(auth()->user()); + $truth->setCompany($ct->first()->company); + return $this->listResponse($ct); } diff --git a/app/Http/Controllers/Auth/ContactRegisterController.php b/app/Http/Controllers/Auth/ContactRegisterController.php index 4b7c9f8883..424fa6c071 100644 --- a/app/Http/Controllers/Auth/ContactRegisterController.php +++ b/app/Http/Controllers/Auth/ContactRegisterController.php @@ -35,7 +35,10 @@ class ContactRegisterController extends Controller public function showRegisterForm(string $company_key = '') { - $key = request()->session()->has('company_key') ? request()->session()->get('company_key') : $company_key; + if(strlen($company_key) > 2) + $key = $company_key; + else + $key = request()->session()->has('company_key') ? request()->session()->get('company_key') : $company_key; $company = Company::where('company_key', $key)->firstOrFail(); @@ -43,7 +46,7 @@ class ContactRegisterController extends Controller $t = app('translator'); $t->replace(Ninja::transformTranslations($company->settings)); - return render('auth.register', ['company' => $company, 'account' => $company->account]); + return render('auth.register', ['register_company' => $company, 'account' => $company->account]); } public function register(RegisterRequest $request) @@ -60,6 +63,7 @@ class ContactRegisterController extends Controller private function getClient(array $data) { + $client = ClientFactory::create($data['company']->id, $data['company']->owner()->id); $client->fill($data); @@ -67,14 +71,12 @@ class ContactRegisterController extends Controller $client->number = $this->getNextClientNumber($client); $client->save(); - if(!$client->country_id && strlen($client->company->settings->country_id) > 1){ + if(!array_key_exists('country_id', $data) && strlen($client->company->settings->country_id) > 1){ $client->update(['country_id' => $client->company->settings->country_id]); } - $this->getClientContact($data, $client); - return $client; } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 1cd89bf264..9f7808fc19 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -242,6 +242,8 @@ class LoginController extends BaseController } + $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $user->account->default_company->id)->first()); + /*On the hosted platform, only owners can login for free/pro accounts*/ if(Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient()) return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); @@ -388,13 +390,11 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', auth()->user()->id); - // $cu->first()->account->companies->each(function ($company) use($cu){ - // if($company->tokens()->where('is_system', true)->count() == 0) - // { - // CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); - // } - // }); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($cu->first()); + $truth->setUser($existing_user); + $truth->setCompany($existing_user->account->default_company); if($existing_user->company_users()->count() != $existing_user->tokens()->count()) @@ -412,6 +412,7 @@ class LoginController extends BaseController } + $truth->setCompanyToken(CompanyToken::where('user_id', $existing_user->id)->where('company_id', $existing_user->account->default_company->id)->first()); if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_user->account->isEnterpriseClient()) @@ -440,13 +441,11 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', auth()->user()->id); - // $cu->first()->account->companies->each(function ($company) use($cu){ + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($cu->first()); + $truth->setUser($existing_login_user); + $truth->setCompany($existing_login_user->account->default_company); - // if($company->tokens()->where('is_system', true)->count() == 0) - // { - // CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); - // } - // }); if($existing_login_user->company_users()->count() != $existing_login_user->tokens()->count()) { @@ -463,6 +462,8 @@ class LoginController extends BaseController } + $truth->setCompanyToken(CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $existing_login_user->account->default_company->id)->first()); + if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient()) @@ -495,13 +496,10 @@ class LoginController extends BaseController $cu = CompanyUser::query() ->where('user_id', auth()->user()->id); - // $cu->first()->account->companies->each(function ($company) use($cu){ - - // if($company->tokens()->where('is_system', true)->count() == 0) - // { - // CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); - // } - // }); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($cu->first()); + $truth->setUser($existing_login_user); + $truth->setCompany($existing_login_user->account->default_company); if($existing_login_user->company_users()->count() != $existing_login_user->tokens()->count()) @@ -519,6 +517,7 @@ class LoginController extends BaseController } + $truth->setCompanyToken(CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $existing_login_user->account->default_company->id)->first()); if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient()) return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); @@ -556,13 +555,11 @@ class LoginController extends BaseController $cu = CompanyUser::whereUserId(auth()->user()->id); - // $cu->first()->account->companies->each(function ($company) use($cu){ - // if($company->tokens()->where('is_system', true)->count() == 0) - // { - // CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT')); - // } - // }); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($cu->first()); + $truth->setUser(auth()->user()); + $truth->setCompany(auth()->user()->account->default_company); if(auth()->user()->company_users()->count() != auth()->user()->tokens()->count()) { @@ -579,6 +576,7 @@ class LoginController extends BaseController } + $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', auth()->user()->account->default_company->id)->first()); if(Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterpriseClient()) diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php index 698b4098e0..cfaf0bdb2d 100644 --- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php +++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php @@ -163,7 +163,7 @@ class NinjaPlanController extends Controller $recurring_invoice->save(); $r = $recurring_invoice->calc()->getRecurringInvoice(); - $recurring_invoice->service()->start()->save(); + $recurring_invoice->service()->applyNumber()->start()->save(); LightLogs::create(new TrialStarted()) ->increment() diff --git a/app/Http/Controllers/SetupController.php b/app/Http/Controllers/SetupController.php index 07d0ea1ed2..fbb0aed716 100644 --- a/app/Http/Controllers/SetupController.php +++ b/app/Http/Controllers/SetupController.php @@ -53,10 +53,9 @@ class SetupController extends Controller return redirect('/'); } - // not sure if we really need this. - // if(File::exists(base_path('.env'))) - // abort(400, '.env file already exists, delete file to start Setup again.'); - + if(Ninja::isHosted()) + return redirect('/'); + return view('setup.index', ['check' => $check]); } diff --git a/app/Http/Middleware/ContactRegister.php b/app/Http/Middleware/ContactRegister.php index 1ada8e1c56..22eb6e0b8e 100644 --- a/app/Http/Middleware/ContactRegister.php +++ b/app/Http/Middleware/ContactRegister.php @@ -68,11 +68,9 @@ class ContactRegister // For self-hosted platforms with multiple companies, resolving is done using company key // if it doesn't resolve using a domain. - - if ($request->company_key && Ninja::isSelfHost()) { + + if ($request->company_key && Ninja::isSelfHost() && $company = Company::where('company_key', $request->company_key)->first()) { - $company = Company::where('company_key', $request->company_key)->firstOrFail(); - if(! (bool)$company->client_can_register) abort(400, 'Registration disabled'); diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index 4778719a04..a280d14392 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -31,7 +31,7 @@ class TokenAuth */ public function handle($request, Closure $next) { - if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user', 'company', 'cu'])->where('token', $request->header('X-API-TOKEN'))->first())) { + if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user', 'company'])->where('token', $request->header('X-API-TOKEN'))->first())) { $user = $company_token->user; diff --git a/app/Http/Requests/ClientPortal/PaymentMethod/CreatePaymentMethodRequest.php b/app/Http/Requests/ClientPortal/PaymentMethod/CreatePaymentMethodRequest.php index d90438ac8c..dbf905f5f8 100644 --- a/app/Http/Requests/ClientPortal/PaymentMethod/CreatePaymentMethodRequest.php +++ b/app/Http/Requests/ClientPortal/PaymentMethod/CreatePaymentMethodRequest.php @@ -18,7 +18,7 @@ class CreatePaymentMethodRequest extends FormRequest public function authorize(): bool { /** @var Client $client */ - $client = auth()->user()->client; + $client = auth()->('guard')->user()->client; $available_methods = []; diff --git a/app/Http/Requests/Product/CreateProductRequest.php b/app/Http/Requests/Product/CreateProductRequest.php index eefa8ca998..f8c099e827 100644 --- a/app/Http/Requests/Product/CreateProductRequest.php +++ b/app/Http/Requests/Product/CreateProductRequest.php @@ -29,7 +29,6 @@ class CreateProductRequest extends Request public function rules() : array { return [ - 'product_key' => 'required', ]; } } diff --git a/app/Jobs/Company/CreateCompany.php b/app/Jobs/Company/CreateCompany.php index cbb6a1afe1..eb852501af 100644 --- a/app/Jobs/Company/CreateCompany.php +++ b/app/Jobs/Company/CreateCompany.php @@ -64,6 +64,7 @@ class CreateCompany $company->custom_fields = new \stdClass; $company->default_password_timeout = 1800000; $company->client_registration_fields = ClientRegistrationFields::generate(); + $company->markdown_email_enabled = true; if(Ninja::isHosted()) $company->subdomain = MultiDB::randomSubdomainGenerator(); diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 1b8713cd10..ecd34f8d62 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -210,15 +210,8 @@ class NinjaMailerJob implements ShouldQueue $user = $user->fresh(); } - //17-01-2022 - ensure we have a token otherwise we fail gracefully to default sending engine - // if(strlen($user->oauth_user_token) == 0){ - // $this->nmo->settings->email_sending_method = 'default'; - // return $this->setMailDriver(); - // } - $google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); - //need to slow down gmail requests otherwise we hit 429's sleep(rand(2,6)); } catch(\Exception $e) { @@ -227,6 +220,16 @@ class NinjaMailerJob implements ShouldQueue return $this->setMailDriver(); } + /** + * If the user doesn't have a valid token, notify them + */ + + if(!$user->oauth_user_token) { + $this->company->account->gmailCredentialNotification(); + return; + } + + /* * Now that our token is refreshed and valid we can boot the * mail driver at runtime and also set the token which will persist diff --git a/app/Listeners/Invoice/InvoiceArchivedActivity.php b/app/Listeners/Invoice/InvoiceArchivedActivity.php index 4b37f0070c..63903cda92 100644 --- a/app/Listeners/Invoice/InvoiceArchivedActivity.php +++ b/app/Listeners/Invoice/InvoiceArchivedActivity.php @@ -42,8 +42,6 @@ class InvoiceArchivedActivity implements ShouldQueue public function handle($event) { MultiDB::setDb($event->company->db); - - // $event->invoice->service()->deletePdf(); $fields = new stdClass; diff --git a/app/Mail/Ninja/GmailTokenInvalid.php b/app/Mail/Ninja/GmailTokenInvalid.php new file mode 100644 index 0000000000..eaf7bc852a --- /dev/null +++ b/app/Mail/Ninja/GmailTokenInvalid.php @@ -0,0 +1,64 @@ +company = $company; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + App::setLocale($this->company->getLocale()); + + $this->settings = $this->company->settings; + $this->logo = $this->company->present()->logo(); + $this->title = ctrans('texts.gmail_credentials_invalid_subject'); + $this->body = ctrans('texts.gmail_credentials_invalid_body'); + $this->whitelabel = $this->company->account->isPaid(); + $this->replyTo('contact@invoiceninja.com', 'Contact'); + + return $this->from(config('mail.from.address'), config('mail.from.name')) + ->subject(ctrans('texts.gmail_credentials_invalid_subject')) + ->view('email.admin.email_quota_exceeded'); + } +} diff --git a/app/Models/Account.php b/app/Models/Account.php index 222e49aa49..1a4962ba85 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -14,8 +14,10 @@ namespace App\Models; use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerObject; use App\Mail\Ninja\EmailQuotaExceeded; +use App\Mail\Ninja\GmailTokenInvalid; use App\Models\Presenters\AccountPresenter; use App\Notifications\Ninja\EmailQuotaNotification; +use App\Notifications\Ninja\GmailCredentialNotification; use App\Utils\Ninja; use App\Utils\Traits\MakesHash; use Carbon\Carbon; @@ -424,4 +426,43 @@ class Account extends BaseModel return false; } + public function gmailCredentialNotification() :bool + { + + if(is_null(Cache::get($this->key))) + return false; + + try { + + if(is_null(Cache::get("gmail_credentials_notified:{$this->key}"))) { + + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->companies()->first()->settings)); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new GmailTokenInvalid($this->companies()->first()); + $nmo->company = $this->companies()->first(); + $nmo->settings = $this->companies()->first()->settings; + $nmo->to_user = $this->companies()->first()->owner(); + NinjaMailerJob::dispatch($nmo); + + Cache::put("gmail_credentials_notified:{$this->key}", true, 60 * 24); + + if(config('ninja.notification.slack')) + $this->companies()->first()->notification(new GmailCredentialNotification($this))->ninja(); + } + + return true; + + } + catch(\Exception $e){ + \Sentry\captureMessage("I encountered an error with sending with gmail for account {$this->key}"); + } + + return false; + + + } + } diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index d8222df064..63c4d499e1 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -346,6 +346,8 @@ class CompanyGateway extends BaseModel if ($fees_and_limits->fee_amount) { $adjusted_fee += $fees_and_limits->fee_amount + $amount; } + else + $adjusted_fee = $amount; if ($fees_and_limits->fee_percent) { diff --git a/app/Models/CompanyToken.php b/app/Models/CompanyToken.php index 72c57361f2..24a9926239 100644 --- a/app/Models/CompanyToken.php +++ b/app/Models/CompanyToken.php @@ -59,6 +59,9 @@ class CompanyToken extends BaseModel public function cu() { - return $this->hasOneThrough(CompanyUser::class, Company::class, 'id', 'company_id', 'company_id', 'id'); + return $this->hasOne(CompanyUser::class, 'user_id', 'user_id') + ->where('company_id', $this->company_id) + ->where('user_id', $this->user_id); + } } diff --git a/app/Models/Paymentable.php b/app/Models/Paymentable.php index 745bea4482..f271c42344 100644 --- a/app/Models/Paymentable.php +++ b/app/Models/Paymentable.php @@ -43,4 +43,5 @@ class Paymentable extends Pivot { return $this->belongsTo(Payment::class); } + } diff --git a/app/Models/Presenters/CompanyPresenter.php b/app/Models/Presenters/CompanyPresenter.php index 371c2312bc..4c800e1c60 100644 --- a/app/Models/Presenters/CompanyPresenter.php +++ b/app/Models/Presenters/CompanyPresenter.php @@ -66,11 +66,11 @@ class CompanyPresenter extends EntityPresenter ); if(strlen($settings->company_logo) >= 1 && (strpos($settings->company_logo, 'http') !== false)) - return "data:image/png;base64, ". base64_encode(file_get_contents($settings->company_logo, false, stream_context_create($context_options))); + return "data:image/png;base64, ". base64_encode(@file_get_contents($settings->company_logo, false, stream_context_create($context_options))); else if(strlen($settings->company_logo) >= 1) - return "data:image/png;base64, ". base64_encode(file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options))); + return "data:image/png;base64, ". base64_encode(@file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options))); else - return "data:image/png;base64, ". base64_encode(file_get_contents(asset('images/new_logo.png'), false, stream_context_create($context_options))); + return "data:image/png;base64, ". base64_encode(@file_get_contents(asset('images/new_logo.png'), false, stream_context_create($context_options))); } diff --git a/app/Models/User.php b/app/Models/User.php index bf2195fb37..7288f94a7e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -156,7 +156,6 @@ class User extends Authenticatable implements MustVerifyEmail return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first(); } - return $this->tokens()->first(); } @@ -371,9 +370,10 @@ class User extends Authenticatable implements MustVerifyEmail return $this->isOwner() || $this->isAdmin() || - (stripos($this->token()->cu->permissions, $all_permission) !== false) || - (stripos($this->token()->cu->permissions, $permission) !== false); + (is_int(stripos($this->token()->cu->permissions, $all_permission))) || + (is_int(stripos($this->token()->cu->permissions, $permission))); + //23-03-2021 - stripos return an int if true and bool false, but 0 is also interpreted as false, so we simply use is_int() to verify state // return $this->isOwner() || // $this->isAdmin() || // (stripos($this->company_user->permissions, $all_permission) !== false) || @@ -404,9 +404,6 @@ class User extends Authenticatable implements MustVerifyEmail if($this->token()->cu->slack_webhook_url) return $this->token()->cu->slack_webhook_url; - // if ($this->company_user->slack_webhook_url) { - // return $this->company_user->slack_webhook_url; - // } } public function routeNotificationForMail($notification) diff --git a/app/Notifications/Ninja/GmailCredentialNotification.php b/app/Notifications/Ninja/GmailCredentialNotification.php new file mode 100644 index 0000000000..27684f39a5 --- /dev/null +++ b/app/Notifications/Ninja/GmailCredentialNotification.php @@ -0,0 +1,88 @@ +account = $account; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return MailMessage + */ + public function toMail($notifiable) + { + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } + + public function toSlack($notifiable) + { + + $content = "GMail credentials invalid for Account {$this->account->key} \n"; + + $owner = $this->account->companies()->first()->owner(); + + $content .= "Owner {$owner->present()->name() } | {$owner->email}"; + + return (new SlackMessage) + ->success() + ->from(ctrans('texts.notification_bot')) + ->image('https://app.invoiceninja.com/favicon.png') + ->content($content); + } +} diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 7718c478ac..70b4308027 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -322,8 +322,8 @@ class BaseDriver extends AbstractPaymentDriver if (collect($invoice->line_items)->contains('type_id', '3')) { $invoice->service()->toggleFeesPaid()->save(); - $invoice->client->service()->updateBalance($fee_total)->save(); - $invoice->ledger()->updateInvoiceBalance($fee_total, "Gateway fee adjustment for invoice {$invoice->number}"); + // $invoice->client->service()->updateBalance($fee_total)->save(); + // $invoice->ledger()->updateInvoiceBalance($fee_total, "Gateway fee adjustment for invoice {$invoice->number}"); } $transaction = [ diff --git a/app/PaymentDrivers/Eway/CreditCard.php b/app/PaymentDrivers/Eway/CreditCard.php index b79c6a2afc..bf7aab9657 100644 --- a/app/PaymentDrivers/Eway/CreditCard.php +++ b/app/PaymentDrivers/Eway/CreditCard.php @@ -160,7 +160,6 @@ class CreditCard 'TotalAmount' => $this->convertAmountForEway(), 'CurrencyCode' => $this->eway_driver->client->currency()->code, 'InvoiceNumber' => $invoice_numbers, - 'InvoiceReference' => $description, ], 'TransactionType' => \Eway\Rapid\Enum\TransactionType::PURCHASE, 'SecuredCardData' => $request->input('securefieldcode'), @@ -168,19 +167,17 @@ class CreditCard $response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction); + $this->logResponse($response); + $response_status = ErrorCode::getStatus($response->ResponseMessage); if(!$response_status['success']){ - $this->logResponse($response, false); - $this->eway_driver->sendFailureMail($response_status['message']); throw new PaymentFailed($response_status['message'], 400); } - $this->logResponse($response, true); - $payment = $this->storePayment($response); return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); @@ -252,13 +249,15 @@ class CreditCard 'TotalAmount' => $this->convertAmountForEway($amount), 'CurrencyCode' => $this->eway_driver->client->currency()->code, 'InvoiceNumber' => $invoice_numbers, - 'InvoiceReference' => $description, ], 'TransactionType' => \Eway\Rapid\Enum\TransactionType::RECURRING, ]; $response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction); + nlog('eway'); + nlog($response); + $response_status = ErrorCode::getStatus($response->ResponseMessage); if(!$response_status['success']){ diff --git a/app/Repositories/ClientRepository.php b/app/Repositories/ClientRepository.php index cd2bd9bff1..cccf3d04d8 100644 --- a/app/Repositories/ClientRepository.php +++ b/app/Repositories/ClientRepository.php @@ -58,12 +58,16 @@ class ClientRepository extends BaseRepository return $client; } - if(!$client->id && auth()->user() && auth()->user()->company() && (!array_key_exists('country_id', $data) || empty($data['country_id']))){ - $data['country_id'] = auth()->user()->company()->settings->country_id; + $client->fill($data); + + + if(auth()->user() && !$client->country_id){ + $client->country_id = auth()->user()->company()->settings->country_id; + } - $client->fill($data); $client->save(); + if (!isset($client->number) || empty($client->number) || strlen($client->number) == 0) { $client->number = $this->getNextClientNumber($client); diff --git a/app/Services/Invoice/AddGatewayFee.php b/app/Services/Invoice/AddGatewayFee.php index 882e03fe3f..773c299cb5 100644 --- a/app/Services/Invoice/AddGatewayFee.php +++ b/app/Services/Invoice/AddGatewayFee.php @@ -74,6 +74,8 @@ class AddGatewayFee extends AbstractService private function processGatewayFee($gateway_fee) { + $balance = $this->invoice->balance; + App::forgetInstance('translator'); $t = app('translator'); $t->replace(Ninja::transformTranslations($this->invoice->company->settings)); @@ -100,11 +102,30 @@ class AddGatewayFee extends AbstractService /**Refresh Invoice values*/ $this->invoice = $this->invoice->calc()->getInvoice(); + $new_balance = $this->invoice->balance; + + if(floatval($new_balance) - floatval($balance) != 0) + { + $adjustment = $new_balance - $balance; + + $this->invoice + ->client + ->service() + ->updateBalance($adjustment) + ->save(); + + $this->invoice + ->ledger() + ->updateInvoiceBalance($adjustment, 'Adjustment for removing gateway fee'); + } + return $this->invoice; } private function processGatewayDiscount($gateway_fee) { + $balance = $this->invoice->balance; + App::forgetInstance('translator'); $t = app('translator'); $t->replace(Ninja::transformTranslations($this->invoice->company->settings)); @@ -129,6 +150,25 @@ class AddGatewayFee extends AbstractService $this->invoice = $this->invoice->calc()->getInvoice(); + $new_balance = $this->invoice->balance; + + + if(floatval($new_balance) - floatval($balance) != 0) + { + $adjustment = $new_balance - $balance; + + $this->invoice + ->client + ->service() + ->updateBalance($adjustment * -1) + ->save(); + + $this->invoice + ->ledger() + ->updateInvoiceBalance($adjustment * -1, 'Adjustment for removing gateway fee'); + } + + return $this->invoice; } } diff --git a/app/Services/Invoice/ApplyPaymentAmount.php b/app/Services/Invoice/ApplyPaymentAmount.php index 51ec799595..092d3e01a1 100644 --- a/app/Services/Invoice/ApplyPaymentAmount.php +++ b/app/Services/Invoice/ApplyPaymentAmount.php @@ -84,6 +84,13 @@ class ApplyPaymentAmount extends AbstractService ->deletePdf() ->save(); + $this->invoice + ->client + ->service() + ->updateBalance($payment->amount * -1) + ->updatePaidToDate($payment->amount) + ->save(); + if ($this->invoice->client->getSetting('client_manual_payment_notification')) $payment->service()->sendEmail(); @@ -92,13 +99,6 @@ class ApplyPaymentAmount extends AbstractService $payment->ledger() ->updatePaymentBalance($payment->amount * -1); - $this->invoice - ->client - ->service() - ->updateBalance($payment->amount * -1) - ->updatePaidToDate($payment->amount) - ->save(); - $this->invoice->service()->workFlow()->save(); event('eloquent.created: App\Models\Payment', $payment); diff --git a/app/Services/Invoice/GetInvoicePdf.php b/app/Services/Invoice/GetInvoicePdf.php index 0923c0ea22..758cb92b59 100644 --- a/app/Services/Invoice/GetInvoicePdf.php +++ b/app/Services/Invoice/GetInvoicePdf.php @@ -29,12 +29,16 @@ class GetInvoicePdf extends AbstractService public function run() { + if (! $this->contact) { $this->contact = $this->invoice->client->primary_contact()->first() ?: $this->invoice->client->contacts()->first(); } $invitation = $this->invoice->invitations->where('client_contact_id', $this->contact->id)->first(); + if(!$invitation) + $invitation = $this->invoice->invitations->first(); + $path = $this->invoice->client->invoice_filepath($invitation); $file_path = $path.$this->invoice->numberFormatter().'.pdf'; @@ -48,8 +52,7 @@ class GetInvoicePdf extends AbstractService $file_path = CreateEntityPdf::dispatchNow($invitation); } - // return Storage::disk($disk)->path($file_path); - // return $file_path; + } } diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 233644b207..5b91c111a6 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -11,6 +11,7 @@ namespace App\Services\Invoice; +use App\Events\Invoice\InvoiceWasArchived; use App\Jobs\Entity\CreateEntityPdf; use App\Jobs\Invoice\InvoiceWorkflowSettings; use App\Jobs\Util\UnlinkFile; @@ -361,6 +362,8 @@ class InvoiceService public function removeUnpaidGatewayFees() { + $balance = $this->invoice->balance; + //return early if type three does not exist. if(!collect($this->invoice->line_items)->contains('type_id', 3)) return $this; @@ -372,6 +375,25 @@ class InvoiceService $this->invoice = $this->invoice->calc()->getInvoice(); + /* 24-03-2022 */ + $new_balance = $this->invoice->balance; + + if(floatval($balance) - floatval($new_balance) != 0) + { + $adjustment = $balance - $new_balance; + + $this->invoice + ->client + ->service() + ->updateBalance($adjustment * -1) + ->save(); + + $this->invoice + ->ledger() + ->updateInvoiceBalance($adjustment * -1, 'Adjustment for removing gateway fee'); + } + + return $this; } @@ -518,35 +540,16 @@ class InvoiceService if ($this->invoice->status_id == Invoice::STATUS_PAID && $this->invoice->client->getSetting('auto_archive_invoice')) { /* Throws: Payment amount xxx does not match invoice totals. */ - $base_repository = new BaseRepository(); - $base_repository->archive($this->invoice); - - } + if ($this->invoice->trashed()) + return; - /* - //if paid invoice is attached to a recurring invoice - check if we need to unpause the recurring invoice + $this->invoice->delete(); + + event(new InvoiceWasArchived($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); - if ($this->invoice->status_id == Invoice::STATUS_PAID && - $this->invoice->recurring_id && - $this->invoice->company->pause_recurring_until_paid && - ($this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_ACTIVE || $this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_COMPLETED)) - { - $recurring_invoice = $this->invoice->recurring_invoice; - - // Check next_send_date if it is in the past - calculate - $next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay(); - - if(next_send_date->lt(now())){ - $recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d')); - $recurring_invoice->save(); - } - - // Start the recurring invoice - $recurring_invoice->service() - ->start(); } - */ + return $this; } diff --git a/app/Services/Payment/RefundPayment.php b/app/Services/Payment/RefundPayment.php index cdf86f2d0f..ac1d0fdf42 100644 --- a/app/Services/Payment/RefundPayment.php +++ b/app/Services/Payment/RefundPayment.php @@ -263,6 +263,8 @@ class RefundPayment foreach ($this->refund_data['invoices'] as $refunded_invoice) { $invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']); + $invoice->restore(); + $invoice->service()->updateBalance($refunded_invoice['amount'])->save(); $invoice->ledger()->updateInvoiceBalance($refunded_invoice['amount'], "Refund of payment # {$this->payment->number}")->save(); $invoice->paid_to_date -= $refunded_invoice['amount']; @@ -292,6 +294,9 @@ class RefundPayment TransactionLog::dispatch(TransactionEvent::PAYMENT_REFUND, $transaction, $invoice->company->db); + if($invoice->is_deleted) + $invoice->delete(); + } $client = $this->payment->client->fresh(); diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index 1a7529058e..3a34b2e803 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -71,6 +71,9 @@ class UpdateInvoicePayment ->updatePaidToDate($paid_amount) ->updateStatus() ->touchPdf() + ->save(); + + $invoice->service() ->workFlow() ->save(); diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index 7abc5e4900..02e7a03a3b 100644 --- a/app/Services/PdfMaker/Design.php +++ b/app/Services/PdfMaker/Design.php @@ -428,16 +428,33 @@ class Design extends BaseDesign $tbody = []; - foreach ($this->payments as $payment) { - foreach ($payment->invoices as $invoice) { + // foreach ($this->payments as $payment) { + // foreach ($payment->invoices as $invoice) { + // $element = ['element' => 'tr', 'elements' => []]; + + // $element['elements'][] = ['element' => 'td', 'content' => $invoice->number]; + // $element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($payment->date, $this->client->date_format(), $this->client->locale()) ?: ' ']; + // $element['elements'][] = ['element' => 'td', 'content' => $payment->type ? $payment->type->name : ctrans('texts.manual_entry')]; + // $element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->amount, $this->client) ?: ' ']; + + // $tbody[] = $element; + // } + // } + + + //24-03-2022 show payments per invoice + foreach ($this->invoices as $invoice) { + foreach ($invoice->payments as $payment) { + $element = ['element' => 'tr', 'elements' => []]; $element['elements'][] = ['element' => 'td', 'content' => $invoice->number]; $element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($payment->date, $this->client->date_format(), $this->client->locale()) ?: ' ']; $element['elements'][] = ['element' => 'td', 'content' => $payment->type ? $payment->type->name : ctrans('texts.manual_entry')]; - $element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->amount, $this->client) ?: ' ']; + $element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->pivot->amount, $this->client) ?: ' ']; $tbody[] = $element; + } } @@ -646,7 +663,7 @@ class Design extends BaseDesign return [ ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [ ['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: block; align-items: flex-start; page-break-inside: avoid; visible !important;'], 'elements' => [ - ['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'overflow: visible !important; display: block; page-break-inside: avoid; height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']], + ['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']], ]], ]], ]; @@ -658,21 +675,6 @@ class Design extends BaseDesign $variables = $this->context['pdf_variables']['total_columns']; - /* 'labels' is a protected value - if the user enters labels it attempts to replace this string again - we need to set labels are a protected text label and remove it from the string */ - // $elements = [ - // ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [ - // ['element' => 'p', 'content' => strtr(str_replace("labels", "", $_variables['values']['$entity.public_notes']), $_variables), 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']], - // ['element' => 'p', 'content' => '', 'properties' => ['style' => 'text-align: left; display: flex; flex-direction: column;'], 'elements' => [ - // ['element' => 'span', 'content' => '$entity.terms_label: ', 'properties' => ['hidden' => $this->entityVariableCheck('$entity.terms'), 'data-ref' => 'total_table-terms-label', 'style' => 'font-weight: bold; text-align: left; margin-top: 1rem;']], - // ['element' => 'span', 'content' => strtr(str_replace("labels", "", $_variables['values']['$entity.terms']), $_variables['labels']), 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']], - // ]], - // ['element' => 'img', 'properties' => ['style' => 'max-width: 50%; height: auto;', 'src' => '$contact.signature', 'id' => 'contact-signature']], - // ['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: block; align-items: flex-start; page-break-inside: avoid; visible !important;'], 'elements' => [ - // ['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'overflow: visible !important; display: block; page-break-inside: avoid; height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']], - // ]], - // ]], - // ['element' => 'div', 'properties' => ['class' => 'totals-table-right-side', 'dir' => '$dir'], 'elements' => []], - // ]; $elements = [ ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [ diff --git a/app/Services/Quote/ConvertQuote.php b/app/Services/Quote/ConvertQuote.php index 77137f5c24..ae6875e10e 100644 --- a/app/Services/Quote/ConvertQuote.php +++ b/app/Services/Quote/ConvertQuote.php @@ -17,11 +17,13 @@ use App\Factory\InvoiceInvitationFactory; use App\Models\Invoice; use App\Models\Quote; use App\Repositories\InvoiceRepository; +use App\Utils\Traits\GeneratesConvertedQuoteCounter; use App\Utils\Traits\MakesHash; class ConvertQuote { use MakesHash; + use GeneratesConvertedQuoteCounter; private $client; @@ -49,6 +51,19 @@ class ConvertQuote $invoice_array = $invoice->toArray(); $invoice_array['invitations'] = $invites; + //try and convert the invoice number to a quote number here. + if($this->client->getSetting('shared_invoice_quote_counter')) + { + + $converted_number = $this->harvestQuoteCounter($quote, $invoice, $this->client); + + if($converted_number) + { + $invoice_array['number'] = $converted_number; + } + + } + $invoice = $this->invoice_repo->save($invoice_array, $invoice); $invoice->fresh(); diff --git a/app/Utils/Traits/ClientGroupSettingsSaver.php b/app/Utils/Traits/ClientGroupSettingsSaver.php index 8ed5856a55..c4cce0e598 100644 --- a/app/Utils/Traits/ClientGroupSettingsSaver.php +++ b/app/Utils/Traits/ClientGroupSettingsSaver.php @@ -212,7 +212,8 @@ trait ClientGroupSettingsSaver case 'real': case 'float': case 'double': - return is_float($value) || is_numeric(strval($value)); + return !is_string($value) && (is_float($value) || is_numeric(strval($value))); + //return is_float($value) || is_numeric(strval($value)); case 'string': return ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value); case 'bool': diff --git a/app/Utils/Traits/CompanyGatewayFeesAndLimitsSaver.php b/app/Utils/Traits/CompanyGatewayFeesAndLimitsSaver.php index 26c4e5d1b5..0d075f26ea 100644 --- a/app/Utils/Traits/CompanyGatewayFeesAndLimitsSaver.php +++ b/app/Utils/Traits/CompanyGatewayFeesAndLimitsSaver.php @@ -59,7 +59,8 @@ trait CompanyGatewayFeesAndLimitsSaver case 'real': case 'float': case 'double': - return is_float($value) || is_numeric(strval($value)); + return !is_string($value) && (is_float($value) || is_numeric(strval($value))); + // return is_float($value) || is_numeric(strval($value)); case 'string': return ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value); case 'bool': diff --git a/app/Utils/Traits/CompanySettingsSaver.php b/app/Utils/Traits/CompanySettingsSaver.php index cd2534d6ce..d2b370bd9d 100644 --- a/app/Utils/Traits/CompanySettingsSaver.php +++ b/app/Utils/Traits/CompanySettingsSaver.php @@ -193,6 +193,11 @@ trait CompanySettingsSaver settype($settings->{$key}, 'object'); } + //try casting floats here + if($value == 'float' && property_exists($settings, $key)){ + $settings->{$key} = floatval($settings->{$key}); + } + /* Handles unset settings or blank strings */ if (! property_exists($settings, $key) || is_null($settings->{$key}) || ! isset($settings->{$key}) || $settings->{$key} == '') { continue; @@ -229,7 +234,8 @@ trait CompanySettingsSaver case 'real': case 'float': case 'double': - return is_float($value) || is_numeric(strval($value)); + return !is_string($value) && (is_float($value) || is_numeric(strval($value))); +// return is_float($value) || is_numeric(strval($value)); case 'string': return (is_string($value) && method_exists($value, '__toString')) || is_null($value) || is_string($value); case 'bool': diff --git a/app/Utils/Traits/GeneratesConvertedQuoteCounter.php b/app/Utils/Traits/GeneratesConvertedQuoteCounter.php new file mode 100644 index 0000000000..705709aa81 --- /dev/null +++ b/app/Utils/Traits/GeneratesConvertedQuoteCounter.php @@ -0,0 +1,758 @@ +getMergedSettings(); + + $pattern = $settings->quote_number_pattern; + + if(strlen($pattern) > 1 && (stripos($pattern, 'counter') === false)) + $pattern = $pattern.'{$counter}'; + + $number = $this->applyNumberPattern($quote, '_stubling_', $pattern); + + $prefix_counter = str_replace('_stubling_', "", $number); + $counter = str_replace($prefix_counter, "", $quote->number); + + return $this->getNextEntityNumber($invoice, $client, intval($counter)); + } + + private function getNextEntityNumber($invoice, Client $client, $counter) + { + + $settings = $client->getMergedSettings(); + + $pattern = $settings->invoice_number_pattern; + + if(strlen($pattern) > 1 && (stripos($pattern, 'counter') === false)){ + $pattern = $pattern.'{$counter}'; + } + + $padding = $client->getSetting('counter_padding'); + + $number = $this->padCounter($counter, $padding); + + $number = $this->applyNumberPattern($invoice, $number, $pattern); + + $check = Invoice::whereCompanyId($client->company_id)->whereNumber($number)->withTrashed()->exists(); + + if($check){ + return false; + } + + return $number; + + } + + private function getNumberPattern($entity, Client $client) + { + $pattern_string = ''; + + switch ($entity) { + case Invoice::class: + $pattern_string = 'invoice_number_pattern'; + break; + case Quote::class: + $pattern_string = 'quote_number_pattern'; + break; + case RecurringInvoice::class: + $pattern_string = 'recurring_invoice_number_pattern'; + break; + case Payment::class: + $pattern_string = 'payment_number_pattern'; + break; + case Credit::class: + $pattern_string = 'credit_number_pattern'; + break; + case Project::class: + $pattern_string = 'project_number_pattern'; + break; + } + + return $client->getSetting($pattern_string); + } + + private function getEntityCounter($entity, $client) + { + switch ($entity) { + case Invoice::class: + return 'invoice_number_counter'; + break; + case Quote::class: + + if ($this->hasSharedCounter($client, 'quote')) + return 'invoice_number_counter'; + + return 'quote_number_counter'; + break; + case RecurringInvoice::class: + return 'recurring_invoice_number_counter'; + break; + case RecurringQuote::class: + return 'recurring_quote_number_counter'; + break; + case RecurringExpense::class: + return 'recurring_expense_number_counter'; + break; + case Payment::class: + return 'payment_number_counter'; + break; + case Credit::class: + if ($this->hasSharedCounter($client, 'credit')) + return 'invoice_number_counter'; + + return 'credit_number_counter'; + break; + case Project::class: + return 'project_number_counter'; + break; + + default: + return 'default_number_counter'; + break; + } + } + + /** + * Gets the next invoice number. + * + * @param Client $client The client + * + * @param Invoice|null $invoice + * @return string The next invoice number. + */ + public function getNextInvoiceNumber(Client $client, ?Invoice $invoice, $is_recurring = false) :string + { + $entity_number = $this->getNextEntityNumber(Invoice::class, $client, $is_recurring); + + return $this->replaceUserVars($invoice, $entity_number); + } + + /** + * Gets the next credit number. + * + * @param Client $client The client + * + * @return string The next credit number. + */ + public function getNextCreditNumber(Client $client, ?Credit $credit) :string + { + $entity_number = $this->getNextEntityNumber(Credit::class, $client); + + return $this->replaceUserVars($credit, $entity_number); + + } + + /** + * Gets the next quote number. + * + * @param Client $client The client + * + * @return string The next credit number. + */ + public function getNextQuoteNumber(Client $client, ?Quote $quote) + { + $entity_number = $this->getNextEntityNumber(Quote::class, $client); + + return $this->replaceUserVars($quote, $entity_number); + + } + + public function getNextRecurringInvoiceNumber(Client $client, $recurring_invoice) + { + $entity_number = $this->getNextEntityNumber(RecurringInvoice::class, $client); + + return $this->replaceUserVars($recurring_invoice, $entity_number); + + } + + public function getNextRecurringQuoteNumber(Client $client, $recurring_quote) + { + $entity_number = $this->getNextEntityNumber(RecurringQuote::class, $client); + + return $this->replaceUserVars($recurring_quote, $entity_number); + + } + + /** + * Gets the next Payment number. + * + * @param Client $client The client + * + * @return string The next payment number. + */ + public function getNextPaymentNumber(Client $client, ?Payment $payment) :string + { + $entity_number = $this->getNextEntityNumber(Payment::class, $client); + + return $this->replaceUserVars($payment, $entity_number); + + } + + /** + * Gets the next client number. + * + * @param Client $client The client + * + * @return string The next client number. + * @throws \Exception + */ + public function getNextClientNumber(Client $client) :string + { + //Reset counters if enabled + $this->resetCounters($client); + + $counter = $client->getSetting('client_number_counter'); + $setting_entity = $client->getSettingEntity('client_number_counter'); + + $client_number = $this->checkEntityNumber(Client::class, $client, $counter, $client->getSetting('counter_padding'), $client->getSetting('client_number_pattern')); + + $this->incrementCounter($setting_entity, 'client_number_counter'); + + $entity_number = $client_number; + + return $this->replaceUserVars($client, $entity_number); + + } + + + /** + * Gets the next client number. + * + * @param Vendor $vendor The vendor + * @return string The next vendor number. + */ + public function getNextVendorNumber(Vendor $vendor) :string + { + $this->resetCompanyCounters($vendor->company); + + $counter = $vendor->company->settings->vendor_number_counter; + $setting_entity = $vendor->company->settings->vendor_number_counter; + + $vendor_number = $this->checkEntityNumber(Vendor::class, $vendor, $counter, $vendor->company->settings->counter_padding, $vendor->company->settings->vendor_number_pattern); + + $this->incrementCounter($vendor->company, 'vendor_number_counter'); + + $entity_number = $vendor_number; + + return $this->replaceUserVars($vendor, $entity_number); + + } + + /** + * Project Number Generator. + * @param Project $project + * @return string The project number + */ + public function getNextProjectNumber(Project $project) :string + { + $entity_number = $this->getNextEntityNumber(Project::class, $project->client, false); + + return $this->replaceUserVars($project, $entity_number); + } + + + /** + * Gets the next task number. + * + * @param Task $task The task + * @return string The next task number. + */ + public function getNextTaskNumber(Task $task) :string + { + $this->resetCompanyCounters($task->company); + + $counter = $task->company->settings->task_number_counter; + $setting_entity = $task->company->settings->task_number_counter; + + $task_number = $this->checkEntityNumber(Task::class, $task, $counter, $task->company->settings->counter_padding, $task->company->settings->task_number_pattern); + + $this->incrementCounter($task->company, 'task_number_counter'); + + $entity_number = $task_number; + + return $this->replaceUserVars($task, $entity_number); + + } + + /** + * Gets the next expense number. + * + * @param Expense $expense The expense + * @return string The next expense number. + */ + public function getNextExpenseNumber(Expense $expense) :string + { + $this->resetCompanyCounters($expense->company); + + $counter = $expense->company->settings->expense_number_counter; + $setting_entity = $expense->company->settings->expense_number_counter; + + $expense_number = $this->checkEntityNumber(Expense::class, $expense, $counter, $expense->company->settings->counter_padding, $expense->company->settings->expense_number_pattern); + + $this->incrementCounter($expense->company, 'expense_number_counter'); + + $entity_number = $expense_number; + + return $this->replaceUserVars($expense, $entity_number); + + } + + /** + * Gets the next expense number. + * + * @param RecurringExpense $expense The expense + * @return string The next expense number. + */ + public function getNextRecurringExpenseNumber(RecurringExpense $expense) :string + { + $this->resetCompanyCounters($expense->company); + + // - 18/09/21 need to set this property if it doesn't exist. //todo refactor this for other properties + if(!property_exists($expense->company->settings, 'recurring_expense_number_counter')){ + $settings = $expense->company->settings; + $settings->recurring_expense_number_counter = 1; + $settings->recurring_expense_number_pattern = ''; + $expense->company->settings = $settings; + $expense->company->save(); + } + + $counter = $expense->company->settings->recurring_expense_number_counter; + $setting_entity = $expense->company->settings->recurring_expense_number_counter; + + $expense_number = $this->checkEntityNumber(RecurringExpense::class, $expense, $counter, $expense->company->settings->counter_padding, $expense->company->settings->recurring_expense_number_pattern); + + $this->incrementCounter($expense->company, 'recurring_expense_number_counter'); + + $entity_number = $expense_number; + + return $this->replaceUserVars($expense, $entity_number); + + } + + + /** + * Determines if it has shared counter. + * + * @param Client $client The client + * + * @return bool True if has shared counter, False otherwise. + */ + public function hasSharedCounter(Client $client, string $type = 'quote') : bool + { + if($type == 'quote') + return (bool) $client->getSetting('shared_invoice_quote_counter'); + + if($type == 'credit') + return (bool) $client->getSetting('shared_invoice_credit_counter'); + } + + /** + * Checks that the number has not already been used. + * + * @param $class + * @param Collection $entity The entity ie App\Models\Client, Invoice, Quote etc + * @param int $counter The counter + * @param int $padding The padding + * + * @param string $pattern + * @param string $prefix + * @return string The padded and prefixed entity number + */ + private function checkEntityNumber($class, $entity, $counter, $padding, $pattern, $prefix = '') + { + + $check = false; + $check_counter = 1; + + do { + + $number = $this->padCounter($counter, $padding); + + $number = $this->applyNumberPattern($entity, $number, $pattern); + + $number = $this->prefixCounter($number, $prefix); + + $check = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists(); + + $counter++; + $check_counter++; + + if($check_counter > 100) + return $number . "_" . Str::random(5); + + } while ($check); + + return $number; + } + + + /*Check if a number is available for use. */ + public function checkNumberAvailable($class, $entity, $number) :bool + { + + if ($entity = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists()) + return false; + + return true; + + } + + /** + * Saves counters at both the company and client level. + * + * @param $entity + * @param string $counter_name The counter name + */ + private function incrementCounter($entity, string $counter_name) :void + { + $settings = $entity->settings; + + if ($counter_name == 'invoice_number_counter' && ! property_exists($entity->settings, 'invoice_number_counter')) { + $settings->invoice_number_counter = 0; + } + + if(!property_exists($settings, $counter_name)) + $settings->{$counter_name} = 1; + + $settings->{$counter_name} = $settings->{$counter_name} + 1; + + $entity->settings = $settings; + + $entity->save(); + } + + private function prefixCounter($counter, $prefix) : string + { + if (strlen($prefix) == 0) { + return $counter; + } + + return $prefix.$counter; + } + + /** + * Pads a number with leading 000000's. + * + * @param int $counter The counter + * @param int $padding The padding + * + * @return string the padded counter + */ + private function padCounter($counter, $padding) :string + { + return str_pad($counter, $padding, '0', STR_PAD_LEFT); + } + + /** + * If we are using counter reset, + * check if we need to reset here. + * + * @param Client $client client entity + * @return void + */ + private function resetCounters(Client $client) + { + $reset_counter_frequency = (int)$client->getSetting('reset_counter_frequency_id'); + + if($reset_counter_frequency == 0) + return; + + $timezone = Timezone::find($client->getSetting('timezone_id')); + + $reset_date = Carbon::parse($client->getSetting('reset_counter_date'), $timezone->name); + + if (! $reset_date->lte(now()) || ! $client->getSetting('reset_counter_date')) { + return false; + } + + switch ($reset_counter_frequency) { + case RecurringInvoice::FREQUENCY_DAILY: + $new_reset_date = $reset_date->addDay(); + break; + case RecurringInvoice::FREQUENCY_WEEKLY: + $new_reset_date = $reset_date->addWeek(); + break; + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + $new_reset_date = $reset_date->addWeeks(2); + break; + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + $new_reset_date = $reset_date->addWeeks(4); + break; + case RecurringInvoice::FREQUENCY_MONTHLY: + $new_reset_date = $reset_date->addMonth(); + break; + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + $new_reset_date = $reset_date->addMonths(2); + break; + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + $new_reset_date = $reset_date->addMonths(3); + break; + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + $new_reset_date = $reset_date->addMonths(4); + break; + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + $new_reset_date = $reset_date->addMonths(6); + break; + case RecurringInvoice::FREQUENCY_ANNUALLY: + $new_reset_date = $reset_date->addYear(); + break; + case RecurringInvoice::FREQUENCY_TWO_YEARS: + $new_reset_date = $reset_date->addYears(2); + break; + + default: + $new_reset_date = $reset_date->addYear(); + break; + } + + $settings = $client->company->settings; + $settings->reset_counter_date = $new_reset_date->format('Y-m-d'); + $settings->invoice_number_counter = 1; + $settings->quote_number_counter = 1; + $settings->credit_number_counter = 1; + + $client->company->settings = $settings; + $client->company->save(); + + } + + private function resetCompanyCounters($company) + { + $timezone = Timezone::find($company->settings->timezone_id); + + $reset_date = Carbon::parse($company->settings->reset_counter_date, $timezone->name); + + if (! $reset_date->lte(now()) || ! $company->settings->reset_counter_date) { + return false; + } + + switch ($company->reset_counter_frequency_id) { + case RecurringInvoice::FREQUENCY_DAILY: + $reset_date->addDay(); + break; + case RecurringInvoice::FREQUENCY_WEEKLY: + $reset_date->addWeek(); + break; + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + $reset_date->addWeeks(2); + break; + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + $reset_date->addWeeks(4); + break; + case RecurringInvoice::FREQUENCY_MONTHLY: + $reset_date->addMonth(); + break; + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + $reset_date->addMonths(2); + break; + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + $reset_date->addMonths(3); + break; + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + $reset_date->addMonths(4); + break; + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + $reset_date->addMonths(6); + break; + case RecurringInvoice::FREQUENCY_ANNUALLY: + $reset_date->addYear(); + break; + case RecurringInvoice::FREQUENCY_TWO_YEARS: + $reset_date->addYears(2); + break; + } + + $settings = $company->settings; + $settings->reset_counter_date = $reset_date->format('Y-m-d'); + $settings->invoice_number_counter = 1; + $settings->quote_number_counter = 1; + $settings->credit_number_counter = 1; + $settings->vendor_number_counter = 1; + $settings->ticket_number_counter = 1; + $settings->payment_number_counter = 1; + $settings->project_number_counter = 1; + $settings->task_number_counter = 1; + $settings->expense_number_counter = 1; + $settings->recurring_expense_number_counter =1; + + $company->settings = $settings; + $company->save(); + } + + /** + * Formats a entity number by pattern + * + * @param BaseModel $entity The entity object + * @param string $counter The counter + * @param null|string $pattern The pattern + * + * @return string The formatted number pattern + */ + private function applyNumberPattern($entity, string $counter, $pattern) :string + { + if (! $pattern) { + return $counter; + } + + $search = ['{$year}']; + $replace = [date('Y')]; + + $search[] = '{$counter}'; + $replace[] = $counter; + + $search[] = '{$client_counter}'; + $replace[] = $counter; + + $search[] = '{$clientCounter}'; + $replace[] = $counter; + + $search[] = '{$group_counter}'; + $replace[] = $counter; + + $search[] = '{$year}'; + $replace[] = date('Y'); + + if (strstr($pattern, '{$user_id}') || strstr($pattern, '{$userId}')) { + $user_id = $entity->user_id ? $entity->user_id : 0; + $search[] = '{$user_id}'; + $replace[] = str_pad(($user_id), 2, '0', STR_PAD_LEFT); + $search[] = '{$userId}'; + $replace[] = str_pad(($user_id), 2, '0', STR_PAD_LEFT); + } + + $matches = false; + preg_match('/{\$date:(.*?)}/', $pattern, $matches); + if (count($matches) > 1) { + $format = $matches[1]; + $search[] = $matches[0]; + + /* The following adjusts for the company timezone - may bork tests depending on the time of day the tests are run!!!!!!*/ + $date = Carbon::now($entity->company->timezone()->name)->format($format); + $replace[] = str_replace($format, $date, $matches[1]); + } + + if ($entity instanceof Vendor) { + $search[] = '{$vendor_id_number}'; + $replace[] = $entity->id_number; + } + + if ($entity instanceof Expense) { + if ($entity->vendor) { + $search[] = '{$vendor_id_number}'; + $replace[] = $entity->vendor->id_number; + + $search[] = '{$vendor_number}'; + $replace[] = $entity->vendor->number; + + $search[] = '{$vendor_custom1}'; + $replace[] = $entity->vendor->custom_value1; + + $search[] = '{$vendor_custom2}'; + $replace[] = $entity->vendor->custom_value2; + + $search[] = '{$vendor_custom3}'; + $replace[] = $entity->vendor->custom_value3; + + $search[] = '{$vendor_custom4}'; + $replace[] = $entity->vendor->custom_value4; + } + + $search[] = '{$expense_id_number}'; + $replace[] = $entity->id_number; + } + + if ($entity->client || ($entity instanceof Client)) { + $client = $entity->client ?: $entity; + + $search[] = '{$client_custom1}'; + $replace[] = $client->custom_value1; + + $search[] = '{$clientCustom1}'; + $replace[] = $client->custom_value1; + + $search[] = '{$client_custom2}'; + $replace[] = $client->custom_value2; + + $search[] = '{$clientCustom2}'; + $replace[] = $client->custom_value2; + + $search[] = '{$client_custom3}'; + $replace[] = $client->custom_value3; + + $search[] = '{$client_custom4}'; + $replace[] = $client->custom_value4; + + $search[] = '{$client_number}'; + $replace[] = $client->number; + + $search[] = '{$client_id_number}'; + $replace[] = $client->id_number; + + $search[] = '{$clientIdNumber}'; + $replace[] = $client->id_number; + } + + return str_replace($search, $replace, $pattern); + } + + private function replaceUserVars($entity, $pattern) + { + + if(!$entity) + return $pattern; + + $search = []; + $replace = []; + + $search[] = '{$user_custom1}'; + $replace[] = $entity->user->custom_value1; + + $search[] = '{$user_custom2}'; + $replace[] = $entity->user->custom_value2; + + $search[] = '{$user_custom3}'; + $replace[] = $entity->user->custom_value3; + + $search[] = '{$user_custom4}'; + $replace[] = $entity->user->custom_value4; + + return str_replace($search, $replace, $pattern); + + } +} diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index d84793295f..57180e986f 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -293,26 +293,9 @@ trait GeneratesCounter */ public function getNextProjectNumber(Project $project) :string { - // 08/12/2021 - allows projects to have client counters. - - // $this->resetCompanyCounters($project->company); - - // $counter = $project->company->settings->project_number_counter; - // $setting_entity = $project->company->settings->project_number_counter; - - // $project_number = $this->checkEntityNumber(Project::class, $project, $counter, $project->company->settings->counter_padding, $project->company->settings->project_number_pattern); - - // $this->incrementCounter($project->company, 'project_number_counter'); - - // $entity_number = $project_number; - - // return $this->replaceUserVars($project, $entity_number); - $entity_number = $this->getNextEntityNumber(Project::class, $project->client, false); return $this->replaceUserVars($project, $entity_number); - - } diff --git a/app/Utils/Traits/SettingsSaver.php b/app/Utils/Traits/SettingsSaver.php index 0699c04325..5237afbf32 100644 --- a/app/Utils/Traits/SettingsSaver.php +++ b/app/Utils/Traits/SettingsSaver.php @@ -92,7 +92,8 @@ trait SettingsSaver case 'real': case 'float': case 'double': - return is_float($value) || is_numeric(strval($value)); + return !is_string($value) && (is_float($value) || is_numeric(strval($value))); + // return is_float($value) || is_numeric(strval($value)); case 'string': return !is_int($value) || ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value); case 'bool': diff --git a/database/migrations/2022_03_24_090728_markdown_email_enabled_wysiwyg_editor.php b/database/migrations/2022_03_24_090728_markdown_email_enabled_wysiwyg_editor.php new file mode 100644 index 0000000000..6ab00a2844 --- /dev/null +++ b/database/migrations/2022_03_24_090728_markdown_email_enabled_wysiwyg_editor.php @@ -0,0 +1,30 @@ +boolean('markdown_email_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 74f196d270..83d1876a81 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4570,6 +4570,8 @@ $LANG = array( 'credits_backup_subject' => 'Your credits are ready for download', 'document_download_subject' => 'Your documents are ready for download', 'reminder_message' => 'Reminder for invoice :number for :balance', + 'gmail_credentials_invalid_subject' => 'Send with GMail invalid credentials', + 'gmail_credentials_invalid_body' => 'Your GMail credentials are not correct, please log into the administrator portal and navigate to Settings > User Details and disconnect and reconnect your GMail account. We will send you this notification daily until this issue is resolved', ); return $LANG; diff --git a/resources/views/portal/ninja2020/auth/register.blade.php b/resources/views/portal/ninja2020/auth/register.blade.php index 214157bd5b..9a8f13540c 100644 --- a/resources/views/portal/ninja2020/auth/register.blade.php +++ b/resources/views/portal/ninja2020/auth/register.blade.php @@ -5,21 +5,21 @@
- {{ ctrans('texts.logo') }} + {{ ctrans('texts.logo') }}

{{ ctrans('texts.register') }}

{{ ctrans('texts.register_label') }}

- @if($company) - + @if($register_company) + @endif @csrf
- @if($company->client_registration_fields) - @foreach($company->client_registration_fields as $field) + @if($register_company->client_registration_fields) + @foreach($register_company->client_registration_fields as $field) @if($field['required'])
@@ -108,15 +108,15 @@
- @if(!empty($company->settings->client_portal_terms) || !empty($company->settings->client_portal_privacy_policy)) + @if(!empty($register_company->settings->client_portal_terms) || !empty($register_company->settings->client_portal_privacy_policy)) {{ ctrans('texts.i_agree_to_the') }} @endif - @includeWhen(!empty($company->settings->client_portal_terms), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'terms_of_service', 'title' => ctrans('texts.terms_of_service'), 'content' => $company->settings->client_portal_terms]) - @includeWhen(!empty($company->settings->client_portal_privacy_policy), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'privacy_policy', 'title' => ctrans('texts.privacy_policy'), 'content' => $company->settings->client_portal_privacy_policy]) + @includeWhen(!empty($register_company->settings->client_portal_terms), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'terms_of_service', 'title' => ctrans('texts.terms_of_service'), 'content' => $register_company->settings->client_portal_terms]) + @includeWhen(!empty($register_company->settings->client_portal_privacy_policy), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'privacy_policy', 'title' => ctrans('texts.privacy_policy'), 'content' => $register_company->settings->client_portal_privacy_policy]) @error('terms')

{{ $message }}

diff --git a/resources/views/portal/ninja2020/layout/app.blade.php b/resources/views/portal/ninja2020/layout/app.blade.php index 8b50d6859e..fcda5464af 100644 --- a/resources/views/portal/ninja2020/layout/app.blade.php +++ b/resources/views/portal/ninja2020/layout/app.blade.php @@ -74,7 +74,7 @@ {{-- Feel free to push anything to header using @push('header') --}} @stack('head') - @if((bool) \App\Utils\Ninja::isSelfHost() && !empty($client->getSetting('portal_custom_head'))) + @if((isset($account) && $account->isPaid()) || ((bool) \App\Utils\Ninja::isSelfHost() && !empty($client->getSetting('portal_custom_head'))))
{!! $client->getSetting('portal_custom_head') !!}
diff --git a/tests/Unit/GeneratesConvertedQuoteCounterTest.php b/tests/Unit/GeneratesConvertedQuoteCounterTest.php new file mode 100644 index 0000000000..a66e390ff8 --- /dev/null +++ b/tests/Unit/GeneratesConvertedQuoteCounterTest.php @@ -0,0 +1,153 @@ +faker = \Faker\Factory::create(); + Model::reguard(); + + } + + public function testCounterExtraction() + { + + $this->account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000 + ]); + + $this->account->num_users = 3; + $this->account->save(); + + $user = User::whereEmail('user@example.com')->first(); + + if (! $user) { + $user = User::factory()->create([ + 'account_id' => $this->account->id, + 'confirmation_code' => $this->createDbHash(config('database.default')), + 'email' => 'user@example.com', + ]); + } + + $user_id = $user->id; + + $this->company = Company::factory()->create([ + 'account_id' => $this->account->id, + ]); + + $this->client = Client::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + + $contact = ClientContact::factory()->create([ + 'user_id' => $user_id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + $settings = $this->client->getMergedSettings(); + $settings->invoice_number_counter = 1; + $settings->invoice_number_pattern = '{$year}-I{$counter}'; + $settings->quote_number_pattern = '{$year}-Q{$counter}'; + $settings->shared_invoice_quote_counter = 1; + $this->company->settings = $settings; + + $this->company->save(); + + $this->client->settings = $settings; + $this->client->save(); + + $quote = Quote::factory()->create([ + 'user_id' => $this->client->user_id, + 'company_id' => $this->client->company_id, + 'client_id' => $this->client->id + ]); + + $quote = $quote->service()->markSent()->convert()->save(); + + $invoice = Invoice::find($quote->invoice_id); + + $this->assertNotNull($invoice); + + $this->assertEquals('2022-Q0001', $quote->number); + $this->assertEquals('2022-I0001', $invoice->number); + + + $settings = $this->client->getMergedSettings(); + $settings->invoice_number_counter = 100; + $settings->invoice_number_pattern = 'I{$counter}'; + $settings->quote_number_pattern = 'Q{$counter}'; + $settings->shared_invoice_quote_counter = 1; + $this->company->settings = $settings; + + $this->company->save(); + + $this->client->settings = $settings; + $this->client->save(); + + $quote = Quote::factory()->create([ + 'user_id' => $this->client->user_id, + 'company_id' => $this->client->company_id, + 'client_id' => $this->client->id + ]); + + $quote = $quote->service()->markSent()->convert()->save(); + + $invoice = Invoice::find($quote->invoice_id); + + $this->assertNotNull($invoice); + + $this->assertEquals('Q0100', $quote->number); + $this->assertEquals('I0100', $invoice->number); + + } + + +}