diff --git a/VERSION.txt b/VERSION.txt index 5c637b26e9b1..b7e4264e5292 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.8.37 \ No newline at end of file +5.8.38 \ No newline at end of file diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 31ccdc2908f7..b4402a4bb7fa 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -169,26 +169,21 @@ class CheckData extends Command private function checkCompanyTokens() { - // CompanyUser::whereDoesntHave('token', function ($query){ - // return $query->where('is_system', 1); - // })->cursor()->each(function ($cu){ - // if ($cu->user) { - // $this->logMessage("Creating missing company token for user # {$cu->user->id} for company id # {$cu->company->id}"); - // (new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle(); - // } else { - // $this->logMessage("Dangling User ID # {$cu->id}"); - // } - // }); - CompanyUser::query()->cursor()->each(function ($cu) { + if (CompanyToken::where('user_id', $cu->user_id)->where('company_id', $cu->company_id)->where('is_system', 1)->doesntExist()) { - $this->logMessage("Creating missing company token for user # {$cu->user_id} for company id # {$cu->company_id}"); + if ($cu->company && $cu->user) { + $this->logMessage("Creating missing company token for user # {$cu->user_id} for company id # {$cu->company_id}"); (new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle(); - } else { - // $cu->forceDelete(); + } + + if (!$cu->user) { + $this->logMessage("No user found for company user - removing company user"); + $cu->forceDelete(); } + } }); } @@ -482,6 +477,14 @@ class CheckData extends Command } } else { $this->logMessage("No contact present, so cannot add invitation for {$entity_key} - {$entity->id}"); + + try{ + $entity->service()->createInvitations()->save(); + } + catch(\Exception $e){ + + } + } try { diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 52cd745418e6..37ef67318927 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -246,6 +246,8 @@ class InvoiceSum if ($this->invoice->status_id != Invoice::STATUS_DRAFT) { if ($this->invoice->amount != $this->invoice->balance) { + // $paid_to_date = $this->invoice->amount - $this->invoice->balance; + $this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision) - $this->invoice->paid_to_date; //21-02-2024 cannot use the calculated $paid_to_date here as it could send the balance backward. } else { $this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision); diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 9185b94bc153..7b5de6d26771 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -278,7 +278,7 @@ class InvitationController extends Controller auth()->guard('contact')->loginUsingId($invitation->contact->id, true); - $invoice = $invitation->invoice; + $invoice = $invitation->invoice->service()->removeUnpaidGatewayFees()->save(); if ($invoice->partial > 0) { $amount = round($invoice->partial, (int)$invoice->client->currency()->precision); diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 95135856d4ab..a43c4e834d90 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -72,7 +72,7 @@ class InvoiceController extends Controller $variables = ($invitation && auth()->guard('contact')->user()->client->getSetting('show_accept_invoice_terms')) ? (new HtmlEngine($invitation))->generateLabelsAndValues() : false; $data = [ - 'invoice' => $invoice, + 'invoice' => $invoice->service()->removeUnpaidGatewayFees()->save(), 'invitation' => $invitation ?: $invoice->invitations->first(), 'key' => $invitation ? $invitation->key : false, 'hash' => $hash, diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 47f47ccbf9ad..27a2f9ebe820 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -14,6 +14,7 @@ namespace App\Http\Controllers; use App\DataMapper\Analytics\LivePreview; use App\Http\Requests\Preview\DesignPreviewRequest; use App\Http\Requests\Preview\PreviewInvoiceRequest; +use App\Http\Requests\Preview\ShowPreviewRequest; use App\Jobs\Util\PreviewPdf; use App\Models\Client; use App\Models\ClientContact; @@ -131,9 +132,9 @@ class PreviewController extends BaseController * Used in the Custom Designer to preview design changes * @return mixed */ - public function show() + public function show(ShowPreviewRequest $request) { - if(request()->has('template')) { + if($request->input('design.is_template')) { return $this->template(); } @@ -238,7 +239,6 @@ class PreviewController extends BaseController private function liveTemplate(array $request_data) { - nlog($request_data['entity_type']); /** @var \App\Models\User $user */ $user = auth()->user(); @@ -292,8 +292,6 @@ class PreviewController extends BaseController ->setTemplate($design_object) ->mock(); } catch(SyntaxError $e) { - - // return response()->json(['message' => 'Twig syntax is invalid.', 'errors' => new \stdClass], 422); } if (request()->query('html') == 'true') { diff --git a/app/Http/Requests/Preview/ShowPreviewRequest.php b/app/Http/Requests/Preview/ShowPreviewRequest.php new file mode 100644 index 000000000000..1a59b7242f32 --- /dev/null +++ b/app/Http/Requests/Preview/ShowPreviewRequest.php @@ -0,0 +1,45 @@ +all(); + + $this->replace($input); + } +} diff --git a/app/Http/Requests/Report/GenericReportRequest.php b/app/Http/Requests/Report/GenericReportRequest.php index c7c94a8fd86e..936bb3c1b547 100644 --- a/app/Http/Requests/Report/GenericReportRequest.php +++ b/app/Http/Requests/Report/GenericReportRequest.php @@ -11,10 +11,14 @@ namespace App\Http\Requests\Report; +use App\Utils\Ninja; use App\Http\Requests\Request; +use Illuminate\Auth\Access\AuthorizationException; class GenericReportRequest extends Request { + private string $error_message = ''; + /** * Determine if the user is authorized to make this request. * @@ -22,11 +26,7 @@ class GenericReportRequest extends Request */ public function authorize(): bool { - /** @var \App\Models\User $user */ - $user = auth()->user(); - - return $user->isAdmin() || $user->hasPermission('view_reports'); - + return $this->checkAuthority(); } public function rules() @@ -70,4 +70,25 @@ class GenericReportRequest extends Request $this->replace($input); } + + private function checkAuthority() + { + $this->error_message = ctrans('texts.authorization_failure'); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + if(Ninja::isHosted() && $user->account->isFreeHostedClient()){ + $this->error_message = ctrans('texts.upgrade_to_view_reports'); + return false; + } + + return $user->isAdmin() || $user->hasPermission('view_reports'); + + } + + protected function failedAuthorization() + { + throw new AuthorizationException($this->error_message); + } } diff --git a/app/Http/Requests/Report/ProductSalesReportRequest.php b/app/Http/Requests/Report/ProductSalesReportRequest.php index 619c73667309..28e2c5b2fdca 100644 --- a/app/Http/Requests/Report/ProductSalesReportRequest.php +++ b/app/Http/Requests/Report/ProductSalesReportRequest.php @@ -11,13 +11,17 @@ namespace App\Http\Requests\Report; +use App\Utils\Ninja; use App\Http\Requests\Request; use App\Utils\Traits\MakesHash; +use Illuminate\Auth\Access\AuthorizationException; class ProductSalesReportRequest extends Request { use MakesHash; + private string $error_message = ''; + /** * Determine if the user is authorized to make this request. * @@ -25,18 +29,22 @@ class ProductSalesReportRequest extends Request */ public function authorize(): bool { - return auth()->user()->isAdmin(); + return $this->checkAuthority(); } public function rules() { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + return [ 'date_range' => 'bail|required|string', 'end_date' => 'bail|required_if:date_range,custom|nullable|date', 'start_date' => 'bail|required_if:date_range,custom|nullable|date', 'report_keys' => 'bail|present|array', 'send_email' => 'bail|required|bool', - 'client_id' => 'bail|nullable|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0', + 'client_id' => 'bail|nullable|sometimes|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0', ]; } @@ -67,4 +75,26 @@ class ProductSalesReportRequest extends Request $this->replace($input); } + + private function checkAuthority() + { + $this->error_message = ctrans('texts.authorization_failure'); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + if(Ninja::isHosted() && $user->account->isFreeHostedClient()){ + $this->error_message = ctrans('texts.upgrade_to_view_reports'); + return false; + } + + return $user->isAdmin() || $user->hasPermission('view_reports'); + + } + + protected function failedAuthorization() + { + throw new AuthorizationException($this->error_message); + } + } diff --git a/app/Http/Requests/Report/ProfitLossRequest.php b/app/Http/Requests/Report/ProfitLossRequest.php index 43628300a57d..4cd2155b1f28 100644 --- a/app/Http/Requests/Report/ProfitLossRequest.php +++ b/app/Http/Requests/Report/ProfitLossRequest.php @@ -11,10 +11,15 @@ namespace App\Http\Requests\Report; +use App\Utils\Ninja; use App\Http\Requests\Request; +use Illuminate\Auth\Access\AuthorizationException; class ProfitLossRequest extends Request { + + private string $error_message = ''; + /** * Determine if the user is authorized to make this request. * @@ -22,10 +27,7 @@ class ProfitLossRequest extends Request */ public function authorize(): bool { - /** @var \App\Models\User $user */ - $user = auth()->user(); - - return $user->isAdmin(); + return $this->checkAuthority(); } public function rules() @@ -51,4 +53,26 @@ class ProfitLossRequest extends Request $this->replace($input); } + + private function checkAuthority() + { + $this->error_message = ctrans('texts.authorization_failure'); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + if(Ninja::isHosted() && $user->account->isFreeHostedClient()){ + $this->error_message = ctrans('texts.upgrade_to_view_reports'); + return false; + } + + return $user->isAdmin() || $user->hasPermission('view_reports'); + + } + + protected function failedAuthorization() + { + throw new AuthorizationException($this->error_message); + } + } diff --git a/app/Http/Requests/Report/ReportPreviewRequest.php b/app/Http/Requests/Report/ReportPreviewRequest.php index 5261e1f8804c..4f95f6a24ad2 100644 --- a/app/Http/Requests/Report/ReportPreviewRequest.php +++ b/app/Http/Requests/Report/ReportPreviewRequest.php @@ -11,10 +11,14 @@ namespace App\Http\Requests\Report; +use App\Utils\Ninja; use App\Http\Requests\Request; +use Illuminate\Auth\Access\AuthorizationException; class ReportPreviewRequest extends Request { + private string $error_message = ''; + /** * Determine if the user is authorized to make this request. * @@ -22,11 +26,7 @@ class ReportPreviewRequest extends Request */ public function authorize(): bool { - /** @var \App\Models\User $user */ - $user = auth()->user(); - - return $user->isAdmin() || $user->hasPermission('view_reports'); - + return $this->checkAuthority(); } public function rules() @@ -38,4 +38,26 @@ class ReportPreviewRequest extends Request public function prepareForValidation() { } + + private function checkAuthority() + { + $this->error_message = ctrans('texts.authorization_failure'); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + if(Ninja::isHosted() && $user->account->isFreeHostedClient()){ + $this->error_message = ctrans('texts.upgrade_to_view_reports'); + return false; + } + + return $user->isAdmin() || $user->hasPermission('view_reports'); + + } + + protected function failedAuthorization() + { + throw new AuthorizationException($this->error_message); + } + } diff --git a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php index aacd7b32d03a..d7ba6fc3b691 100644 --- a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php +++ b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php @@ -59,15 +59,12 @@ class CleanStaleInvoiceOrder implements ShouldQueue Invoice::query() ->withTrashed() ->where('status_id', Invoice::STATUS_SENT) - ->whereBetween('created_at', [now()->subHours(1), now()->subMinutes(30)]) + ->where('created_at', '<', now()->subMinutes(30)) ->where('balance', '>', 0) + ->whereJsonContains('line_items', ['type_id' => '3']) ->cursor() ->each(function ($invoice) { - - if (collect($invoice->line_items)->contains('type_id', 3)) { - $invoice->service()->removeUnpaidGatewayFees(); - } - + $invoice->service()->removeUnpaidGatewayFees(); }); return; @@ -86,6 +83,18 @@ class CleanStaleInvoiceOrder implements ShouldQueue $invoice->is_proforma = false; $repo->delete($invoice); }); + + Invoice::query() + ->withTrashed() + ->where('status_id', Invoice::STATUS_SENT) + ->where('created_at', '<', now()->subMinutes(30)) + ->where('balance', '>', 0) + ->whereJsonContains('line_items', ['type_id' => '3']) + ->cursor() + ->each(function ($invoice) { + $invoice->service()->removeUnpaidGatewayFees(); + }); + } } diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 3be4137148e1..7e6f6b928c09 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -126,14 +126,9 @@ class BaseDriver extends AbstractPaymentDriver $fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required']; } - // if ($this->company_gateway->require_contact_name) { - $fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required']; - $fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required']; - // } - - // if ($this->company_gateway->require_contact_email) { - $fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc']; - // } + $fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc']; if ($this->company_gateway->require_client_phone) { $fields[] = ['name' => 'client_phone', 'label' => ctrans('texts.client_phone'), 'type' => 'tel', 'validation' => 'required']; @@ -166,12 +161,10 @@ class BaseDriver extends AbstractPaymentDriver $fields[] = ['name' => 'client_custom_value2', 'label' => $this->helpers->makeCustomField($this->client->company->custom_fields, 'client2'), 'type' => 'text', 'validation' => 'required']; } - if ($this->company_gateway->require_custom_value3) { $fields[] = ['name' => 'client_custom_value3', 'label' => $this->helpers->makeCustomField($this->client->company->custom_fields, 'client3'), 'type' => 'text', 'validation' => 'required']; } - if ($this->company_gateway->require_custom_value4) { $fields[] = ['name' => 'client_custom_value4', 'label' => $this->helpers->makeCustomField($this->client->company->custom_fields, 'client4'), 'type' => 'text', 'validation' => 'required']; } diff --git a/app/PaymentDrivers/BraintreePaymentDriver.php b/app/PaymentDrivers/BraintreePaymentDriver.php index cb214f62be02..dbbb37c3a10b 100644 --- a/app/PaymentDrivers/BraintreePaymentDriver.php +++ b/app/PaymentDrivers/BraintreePaymentDriver.php @@ -287,6 +287,27 @@ class BraintreePaymentDriver extends BaseDriver } } + + /** + * Required fields for client to fill, to proceed with gateway actions. + * + * @return array[] + */ + public function getClientRequiredFields(): array + { + $fields = []; + + $fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc']; + $fields[] = ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required']; + $fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required']; + + return $fields; + } + public function processWebhookRequest($request) { $validator = Validator::make($request->all(), [ diff --git a/app/PaymentDrivers/Eway/CreditCard.php b/app/PaymentDrivers/Eway/CreditCard.php index 0dd316f6d381..962efe695d13 100644 --- a/app/PaymentDrivers/Eway/CreditCard.php +++ b/app/PaymentDrivers/Eway/CreditCard.php @@ -61,9 +61,9 @@ class CreditCard 'State' => $this->eway_driver->client->state, 'PostalCode' => $this->eway_driver->client->postal_code, 'Country' => $this->eway_driver->client->country->iso_3166_2, - 'Phone' => $this->eway_driver->client->phone, - 'Email' => $this->eway_driver->client->contacts()->first()->email, - 'Url' => $this->eway_driver->client->website, + 'Phone' => $this->eway_driver->client->phone ?? '', + 'Email' => $this->eway_driver->client->contacts()->first()->email ?? '', + 'Url' => $this->eway_driver->client->website ?? '', 'Method' => \Eway\Rapid\Enum\PaymentMethod::CREATE_TOKEN_CUSTOMER, 'SecuredCardData' => $securefieldcode, ]; diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index c89c246a49da..9378593f1208 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -453,12 +453,6 @@ class InvoiceService if ((int) $pre_count != (int) $post_count) { $adjustment = $balance - $new_balance; - // $this->invoice - // ->client - // ->service() - // ->updateBalance($adjustment * -1) - // ->save(); - $this->invoice ->ledger() ->updateInvoiceBalance($adjustment * -1, 'Adjustment for removing gateway fee'); diff --git a/app/Services/PdfMaker/PdfMaker.php b/app/Services/PdfMaker/PdfMaker.php index 19ced3d605a0..f7b8718e80af 100644 --- a/app/Services/PdfMaker/PdfMaker.php +++ b/app/Services/PdfMaker/PdfMaker.php @@ -80,18 +80,29 @@ class PdfMaker $replacements = []; $contents = $this->document->getElementsByTagName('ninja'); - $ts = new TemplateService(); + $ts = new TemplateService(); - if(isset($this->data['template']['entity'])) { + if(isset($this->options['client'])) { + $client = $this->options['client']; try { - $entity = $this->data['template']['entity']; - $ts->setCompany($entity->company); + $ts->setCompany($client->company); + $ts->addGlobal(['currency_code' => $client->company->currency()->code]); } catch(\Exception $e) { - + nlog($e->getMessage()); + } + } + + if(isset($this->options['vendor'])) { + $vendor = $this->options['vendor']; + try { + $ts->setCompany($vendor->company); + $ts->addGlobal(['currency_code' => $vendor->company->currency()->code]); + } catch(\Exception $e) { + nlog($e->getMessage()); } } - $data = $ts->processData($this->options)->getData(); + $data = $ts->processData($this->options)->setGlobals()->getData(); $twig = $ts->twig; foreach ($contents as $content) { diff --git a/app/Services/Template/TemplateAction.php b/app/Services/Template/TemplateAction.php index f739a88bceff..c418d94a132f 100644 --- a/app/Services/Template/TemplateAction.php +++ b/app/Services/Template/TemplateAction.php @@ -79,7 +79,7 @@ class TemplateAction implements ShouldQueue */ public function handle() { - // nlog("inside template action"); + nlog("inside template action"); MultiDB::setDb($this->db); @@ -108,7 +108,14 @@ class TemplateAction implements ShouldQueue ->where('company_id', $this->company->id) ->get(); - // nlog($result->toArray()); + /** Set a global currency_code */ + $first_entity = $result->first(); + if($first_entity->client) + $currency_code = $first_entity->client->currency()->code; + elseif($first_entity instanceof Client) + $currency_code = $first_entity->currency()->code; + else + $currency_code = $this->company->currency()->code; if($result->count() <= 1) { $data[$key] = collect($result); @@ -118,10 +125,9 @@ class TemplateAction implements ShouldQueue $ts = $template_service ->setCompany($this->company) + ->addGlobal(['currency_code' => $currency_code]) ->build($data); - // nlog($ts->getHtml()); - if($this->send_email) { $pdf = $ts->getPdf(); $this->sendEmail($pdf, $template); diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index d3710ba722b0..d36e464efd89 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -157,9 +157,9 @@ class TemplateService return $this; } - private function setGlobals(): self + public function setGlobals(): self { - + foreach($this->global_vars as $key => $value) { $this->twig->addGlobal($key, $value); } @@ -241,8 +241,6 @@ class TemplateService public function getPdf(): string { - // nlog($this->getHtml()); - if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { $pdf = (new NinjaPdf())->build($this->compiled_html); } else { diff --git a/config/ninja.php b/config/ninja.php index 35b1eb1e05e3..80ddd773f262 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.8.37'), - 'app_tag' => env('APP_TAG', '5.8.37'), + 'app_version' => env('APP_VERSION', '5.8.38'), + 'app_tag' => env('APP_TAG', '5.8.38'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false),