diff --git a/VERSION.txt b/VERSION.txt index 261553830a70..96c215198db3 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.8.25 \ No newline at end of file +5.8.26 \ No newline at end of file diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 3079a50988c3..d2241f95b2f5 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -495,7 +495,10 @@ class CompanySettings extends BaseSettings public $show_pdfhtml_on_mobile = true; + public $use_unapplied_payment = 'off'; //always, option, off //@implemented + public static $casts = [ + 'use_unapplied_payment' => 'string', 'show_pdfhtml_on_mobile' => 'bool', 'payment_email_all_contacts' => 'bool', 'statement_design_id' => 'string', diff --git a/app/Factory/CompanyFactory.php b/app/Factory/CompanyFactory.php index 32a9313b9ac2..afe5a6b52ca6 100644 --- a/app/Factory/CompanyFactory.php +++ b/app/Factory/CompanyFactory.php @@ -49,6 +49,14 @@ class CompanyFactory $company->markdown_enabled = false; $company->tax_data = new TaxModel(); $company->first_month_of_year = 1; + $company->smtp_encryption = 'tls'; + $company->smtp_host = ''; + $company->smtp_local_domain = ''; + $company->smtp_password = ''; + $company->smtp_port = ''; + $company->smtp_username = ''; + $company->smtp_verify_peer = true; + return $company; } } diff --git a/app/Filters/PaymentFilters.php b/app/Filters/PaymentFilters.php index 038bc114375c..5c0b733f9398 100644 --- a/app/Filters/PaymentFilters.php +++ b/app/Filters/PaymentFilters.php @@ -12,8 +12,9 @@ namespace App\Filters; use App\Models\Payment; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Eloquent\Builder; /** * PaymentFilters. @@ -163,7 +164,7 @@ class PaymentFilters extends QueryFilters { $sort_col = explode('|', $sort); - if (!is_array($sort_col) || count($sort_col) != 2) { + if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col, Schema::getColumnListing('payments'))) { return $this->builder; } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 43e72c20051b..b0abfcf207f0 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -140,6 +140,7 @@ class BaseController extends Controller 'company.quotes.invitations.company', 'company.quotes.documents', 'company.tasks.documents', + // 'company.tasks.project', 'company.subscriptions', 'company.tax_rates', 'company.tokens_hashed', @@ -458,7 +459,7 @@ class BaseController extends Controller } }, 'company.tasks' => function ($query) use ($updated_at, $user) { - $query->where('updated_at', '>=', $updated_at)->with('documents'); + $query->where('updated_at', '>=', $updated_at)->with('project','documents'); if (! $user->hasPermission('view_task')) { $query->whereNested(function ($query) use ($user) { @@ -796,7 +797,7 @@ class BaseController extends Controller } }, 'company.tasks' => function ($query) use ($created_at, $user) { - $query->where('created_at', '>=', $created_at)->with('documents'); + $query->where('created_at', '>=', $created_at)->with('project.documents','documents'); if (! $user->hasPermission('view_task')) { $query->whereNested(function ($query) use ($user) { diff --git a/app/Http/Controllers/ConnectedAccountController.php b/app/Http/Controllers/ConnectedAccountController.php index 9fc1376cff49..c588d58a9020 100644 --- a/app/Http/Controllers/ConnectedAccountController.php +++ b/app/Http/Controllers/ConnectedAccountController.php @@ -169,13 +169,16 @@ class ConnectedAccountController extends BaseController 'email_verified_at' => now(), ]; - auth()->user()->update($connected_account); - auth()->user()->email_verified_at = now(); - auth()->user()->save(); + /** @var \App\Models\User $logged_in_user */ + $logged_in_user = auth()->user(); - $this->setLoginCache(auth()->user()); + $logged_in_user->update($connected_account); + $logged_in_user->email_verified_at = now(); + $logged_in_user->save(); - return $this->itemResponse(auth()->user()); + $this->setLoginCache($logged_in_user); + + return $this->itemResponse($logged_in_user); } return response() @@ -214,20 +217,22 @@ class ConnectedAccountController extends BaseController // 'email_verified_at' =>now(), ]; - if (auth()->user()->email != $google->harvestEmail($user)) { + /** @var \App\Models\User $logged_in_user */ + $logged_in_user = auth()->user(); + + if ($logged_in_user->email != $google->harvestEmail($user)) { return response()->json(['message' => 'Primary Email differs to OAuth email. Emails must match.'], 400); } - auth()->user()->update($connected_account); - auth()->user()->email_verified_at = now(); - auth()->user()->oauth_user_token = $token; - auth()->user()->oauth_user_refresh_token = $refresh_token; + $logged_in_user->update($connected_account); + $logged_in_user->email_verified_at = now(); + $logged_in_user->oauth_user_token = $token; + $logged_in_user->oauth_user_refresh_token = $refresh_token; + $logged_in_user->save(); - auth()->user()->save(); + $this->activateGmail($logged_in_user); - $this->activateGmail(auth()->user()); - - return $this->itemResponse(auth()->user()); + return $this->itemResponse($logged_in_user); } return response() diff --git a/app/Http/Controllers/SmtpController.php b/app/Http/Controllers/SmtpController.php new file mode 100644 index 000000000000..5588c668edb0 --- /dev/null +++ b/app/Http/Controllers/SmtpController.php @@ -0,0 +1,64 @@ +user(); + $company = $user->company(); + + + + config([ + 'mail.mailers.smtp' => [ + 'transport' => 'smtp', + 'host' => $request->input('smtp_host', $company->smtp_host), + 'port' => $request->input('smtp_port', $company->smtp_port), + 'username' => $request->input('smtp_username', $company->smtp_username), + 'password' => $request->input('smtp_password', $company->smtp_password), + 'encryption' => $request->input('smtp_encryption', $company->smtp_encryption ?? 'tls'), + 'local_domain' => $request->input('smtp_local_domain', strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null), + 'verify_peer' => $request->input('verify_peer', $company->smtp_verify_peer ?? true), + 'timeout' => 5, + ], + ]); + + (new \Illuminate\Mail\MailServiceProvider(app()))->register(); + + try { + Mail::to($user->email, $user->present()->name())->send(new TestMailServer('Email Server Works!', strlen($company->settings->custom_sending_email) > 1 ? $company->settings->custom_sending_email : $user->email)); + } catch (\Exception $e) { + app('mail.manager')->forgetMailers(); + return response()->json(['message' => $e->getMessage()], 400); + } + + app('mail.manager')->forgetMailers(); + + return response()->json(['message' => 'Ok'], 200); + + } + +} \ No newline at end of file diff --git a/app/Http/Requests/Client/StoreClientRequest.php b/app/Http/Requests/Client/StoreClientRequest.php index 66aa798b0ad7..30697ab9b09f 100644 --- a/app/Http/Requests/Client/StoreClientRequest.php +++ b/app/Http/Requests/Client/StoreClientRequest.php @@ -49,6 +49,9 @@ class StoreClientRequest extends Request } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; } + else { + $rules['documents'] = 'bail|sometimes|array'; + } if ($this->file('file') && is_array($this->file('file'))) { $rules['file.*'] = $this->file_validation; diff --git a/app/Http/Requests/Client/UpdateClientRequest.php b/app/Http/Requests/Client/UpdateClientRequest.php index b891731cbd23..11b8c4a60ffc 100644 --- a/app/Http/Requests/Client/UpdateClientRequest.php +++ b/app/Http/Requests/Client/UpdateClientRequest.php @@ -53,6 +53,8 @@ class UpdateClientRequest extends Request $rules['file.*'] = $this->file_validation; } elseif ($this->file('file')) { $rules['file'] = $this->file_validation; + } else { + $rules['documents'] = 'bail|sometimes|array'; } $rules['company_logo'] = 'mimes:jpeg,jpg,png,gif|max:10000'; diff --git a/app/Http/Requests/Company/StoreCompanyRequest.php b/app/Http/Requests/Company/StoreCompanyRequest.php index acab4c2a502e..29dac543e06d 100644 --- a/app/Http/Requests/Company/StoreCompanyRequest.php +++ b/app/Http/Requests/Company/StoreCompanyRequest.php @@ -56,6 +56,15 @@ class StoreCompanyRequest extends Request } } + $rules['smtp_host'] = 'sometimes|string|nullable'; + $rules['smtp_port'] = 'sometimes|string|nullable'; + $rules['smtp_encryption'] = 'sometimes|string'; + $rules['smtp_local_domain'] = 'sometimes|string|nullable'; + $rules['smtp_encryption'] = 'sometimes|string|nullable'; + $rules['smtp_local_domain'] = 'sometimes|string|nullable'; + + // $rules['smtp_verify_peer'] = 'sometimes|in:true,false'; + return $rules; } @@ -67,11 +76,11 @@ class StoreCompanyRequest extends Request $input['name'] = 'Untitled Company'; } - if (array_key_exists('google_analytics_url', $input)) { + if (isset($input['google_analytics_url'])) { $input['google_analytics_key'] = $input['google_analytics_url']; } - if (array_key_exists('portal_domain', $input)) { + if (isset($input['portal_domain'])) { $input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/"); } @@ -79,6 +88,17 @@ class StoreCompanyRequest extends Request $input['subdomain'] = MultiDB::randomSubdomainGenerator(); } + if(isset($input['smtp_username']) && strlen(str_replace("*", "", $input['smtp_username'])) < 2) { + unset($input['smtp_username']); + } + + if(isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) { + unset($input['smtp_password']); + } + + if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) + $input['smtp_verify_peer'] == 'true' ? true : false; + $this->replace($input); } } diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index b5329ecb43e2..949400997c6e 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -57,8 +57,14 @@ class UpdateCompanyRequest extends Request $rules['matomo_id'] = 'nullable|integer'; $rules['e_invoice_certificate_passphrase'] = 'sometimes|nullable'; $rules['e_invoice_certificate'] = 'sometimes|nullable|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin'; - // $rules['client_registration_fields'] = 'array'; + $rules['smtp_host'] = 'sometimes|string|nullable'; + $rules['smtp_port'] = 'sometimes|string|nullable'; + $rules['smtp_encryption'] = 'sometimes|string|nullable'; + $rules['smtp_local_domain'] = 'sometimes|string|nullable'; + // $rules['smtp_verify_peer'] = 'sometimes|string'; + + if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) { $rules['portal_domain'] = 'bail|nullable|sometimes|url'; } @@ -74,23 +80,35 @@ class UpdateCompanyRequest extends Request { $input = $this->all(); - if (array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1) { + if (isset($input['portal_domain']) && strlen($input['portal_domain']) > 1) { $input['portal_domain'] = $this->addScheme($input['portal_domain']); $input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/"); } - if (array_key_exists('settings', $input)) { + if (isset($input['settings'])) { $input['settings'] = (array)$this->filterSaveableSettings($input['settings']); } - if(array_key_exists('subdomain', $input) && $this->company->subdomain == $input['subdomain']) { + if(isset($input['subdomain']) && $this->company->subdomain == $input['subdomain']) { unset($input['subdomain']); } - if(array_key_exists('e_invoice_certificate_passphrase', $input) && empty($input['e_invoice_certificate_passphrase'])) { + if(isset($input['e_invoice_certificate_passphrase']) && empty($input['e_invoice_certificate_passphrase'])) { unset($input['e_invoice_certificate_passphrase']); } + if(isset($input['smtp_username']) && strlen(str_replace("*","", $input['smtp_username'])) < 2) { + unset($input['smtp_username']); + } + + if(isset($input['smtp_password']) && strlen(str_replace("*", "", $input['smtp_password'])) < 2) { + unset($input['smtp_password']); + } + + if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) { + $input['smtp_verify_peer'] == 'true' ? true : false; + } + $this->replace($input); } diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index 8d01aefc95af..c24005063d68 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -50,6 +50,8 @@ class StoreCreditRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 5ec021bc6e1c..7c0c3adc0274 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -52,6 +52,8 @@ class UpdateCreditRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Expense/StoreExpenseRequest.php b/app/Http/Requests/Expense/StoreExpenseRequest.php index 9bab8f25b360..72c2e7506850 100644 --- a/app/Http/Requests/Expense/StoreExpenseRequest.php +++ b/app/Http/Requests/Expense/StoreExpenseRequest.php @@ -52,6 +52,7 @@ class StoreExpenseRequest extends Request $rules['category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.$user->company()->id.',is_deleted,0'; $rules['payment_date'] = 'bail|nullable|sometimes|date:Y-m-d'; $rules['date'] = 'bail|sometimes|date:Y-m-d'; + $rules['documents'] = 'bail|sometimes|array'; return $this->globalRules($rules); } diff --git a/app/Http/Requests/Expense/UpdateExpenseRequest.php b/app/Http/Requests/Expense/UpdateExpenseRequest.php index 961a15709d61..bece7242b0eb 100644 --- a/app/Http/Requests/Expense/UpdateExpenseRequest.php +++ b/app/Http/Requests/Expense/UpdateExpenseRequest.php @@ -29,25 +29,32 @@ class UpdateExpenseRequest extends Request */ public function authorize(): bool { - return auth()->user()->can('edit', $this->expense); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('edit', $this->expense); } public function rules() { + /** @var \App\Models\User $user */ + $user = auth()->user(); + /* Ensure we have a client name, and that all emails are unique*/ $rules = []; if (isset($this->number)) { - $rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->expense->id); + $rules['number'] = Rule::unique('expenses')->where('company_id', $user->company()->id)->ignore($this->expense->id); } if ($this->client_id) { - $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id; + $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.$user->company()->id; } - $rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; - $rules['transaction_id'] = 'bail|sometimes|nullable|exists:bank_transactions,id,company_id,'.auth()->user()->company()->id; - $rules['invoice_id'] = 'bail|sometimes|nullable|exists:invoices,id,company_id,'.auth()->user()->company()->id; + $rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.$user->company()->id.',is_deleted,0'; + $rules['transaction_id'] = 'bail|sometimes|nullable|exists:bank_transactions,id,company_id,'.$user->company()->id; + $rules['invoice_id'] = 'bail|sometimes|nullable|exists:invoices,id,company_id,'.$user->company()->id; + $rules['documents'] = 'bail|sometimes|array'; return $this->globalRules($rules); @@ -55,6 +62,10 @@ class UpdateExpenseRequest extends Request public function prepareForValidation() { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $input = $this->all(); $input = $this->decodePrimaryKeys($input); @@ -64,7 +75,7 @@ class UpdateExpenseRequest extends Request } if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { - $input['currency_id'] = (string) auth()->user()->company()->settings->currency_id; + $input['currency_id'] = (string) $user->company()->settings->currency_id; } /* Ensure the project is related */ diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index acd2ad0655f2..018a1d7c7b36 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -47,6 +47,8 @@ class StoreInvoiceRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { @@ -76,6 +78,7 @@ class StoreInvoiceRequest extends Request $rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date']; + return $rules; } diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index ad4d584ae689..501c2bab31d3 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -49,6 +49,8 @@ class UpdateInvoiceRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { @@ -77,6 +79,7 @@ class UpdateInvoiceRequest extends Request $rules['partial'] = 'bail|sometimes|nullable|numeric'; $rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date']; + return $rules; } diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index 361d9e07f556..25b3b43500c6 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -126,6 +126,8 @@ class StorePaymentRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Payment/UpdatePaymentRequest.php b/app/Http/Requests/Payment/UpdatePaymentRequest.php index 208f0445f0ab..52a3c29d895b 100644 --- a/app/Http/Requests/Payment/UpdatePaymentRequest.php +++ b/app/Http/Requests/Payment/UpdatePaymentRequest.php @@ -55,6 +55,8 @@ class UpdatePaymentRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Product/StoreProductRequest.php b/app/Http/Requests/Product/StoreProductRequest.php index 9619815078d6..c53ad07edbea 100644 --- a/app/Http/Requests/Product/StoreProductRequest.php +++ b/app/Http/Requests/Product/StoreProductRequest.php @@ -35,6 +35,8 @@ class StoreProductRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Product/UpdateProductRequest.php b/app/Http/Requests/Product/UpdateProductRequest.php index 1434945dcc7d..0a76f6a4ab08 100644 --- a/app/Http/Requests/Product/UpdateProductRequest.php +++ b/app/Http/Requests/Product/UpdateProductRequest.php @@ -25,7 +25,11 @@ class UpdateProductRequest extends Request */ public function authorize(): bool { - return auth()->user()->can('edit', $this->product); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('edit', $this->product); } public function rules() @@ -34,6 +38,8 @@ class UpdateProductRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Project/StoreProjectRequest.php b/app/Http/Requests/Project/StoreProjectRequest.php index b57cb6ffa806..8f8e4760df0c 100644 --- a/app/Http/Requests/Project/StoreProjectRequest.php +++ b/app/Http/Requests/Project/StoreProjectRequest.php @@ -53,6 +53,8 @@ class StoreProjectRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Project/UpdateProjectRequest.php b/app/Http/Requests/Project/UpdateProjectRequest.php index fd5ea29a3407..e68c90383790 100644 --- a/app/Http/Requests/Project/UpdateProjectRequest.php +++ b/app/Http/Requests/Project/UpdateProjectRequest.php @@ -49,6 +49,8 @@ class UpdateProjectRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index e08db93727a5..91564eab092d 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -57,6 +57,8 @@ class StorePurchaseOrderRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + } else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index ce9ce0f93841..66984cd29a51 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -59,6 +59,8 @@ class UpdatePurchaseOrderRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index 50094a262809..3e0a498610cc 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -49,6 +49,8 @@ class StoreQuoteRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { @@ -59,11 +61,8 @@ class StoreQuoteRequest extends Request $rules['number'] = ['nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)]; $rules['discount'] = 'sometimes|numeric'; - $rules['is_amount_discount'] = ['boolean']; $rules['exchange_rate'] = 'bail|sometimes|numeric'; - - // $rules['number'] = new UniqueQuoteNumberRule($this->all()); $rules['line_items'] = 'array'; return $rules; diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index 94dc735a59a0..4644e5af691c 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -46,6 +46,8 @@ class UpdateQuoteRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index af9a45673330..7d69a2587e1d 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -49,6 +49,8 @@ class StoreRecurringInvoiceRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index 8800ab04a0a4..f59edc91d61a 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -48,6 +48,8 @@ class UpdateRecurringInvoiceRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Smtp/CheckSmtpRequest.php b/app/Http/Requests/Smtp/CheckSmtpRequest.php new file mode 100644 index 000000000000..d4a4c22914af --- /dev/null +++ b/app/Http/Requests/Smtp/CheckSmtpRequest.php @@ -0,0 +1,54 @@ +user(); + + return $user->isAdmin(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + ]; + } + + public function prepareForValidation() + { + $input = $this->input(); + + if(isset($input['smtp_username']) && $input['smtp_username'] == '********') + unset($input['smtp_username']); + + if(isset($input['smtp_password'])&& $input['smtp_password'] == '********') + unset($input['smtp_password']); + + $this->replace($input); + } +} diff --git a/app/Http/Requests/Task/StoreTaskRequest.php b/app/Http/Requests/Task/StoreTaskRequest.php index df09bf3ae0bd..e5fee49132a0 100644 --- a/app/Http/Requests/Task/StoreTaskRequest.php +++ b/app/Http/Requests/Task/StoreTaskRequest.php @@ -82,6 +82,8 @@ class StoreTaskRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index 0b367b6283d2..299905d8cd58 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -88,6 +88,8 @@ class UpdateTaskRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Vendor/StoreVendorRequest.php b/app/Http/Requests/Vendor/StoreVendorRequest.php index 5ad488e2a64d..7cb6fc14f193 100644 --- a/app/Http/Requests/Vendor/StoreVendorRequest.php +++ b/app/Http/Requests/Vendor/StoreVendorRequest.php @@ -64,6 +64,8 @@ class StoreVendorRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Http/Requests/Vendor/UpdateVendorRequest.php b/app/Http/Requests/Vendor/UpdateVendorRequest.php index cf2d5a884f74..b06b861adfe6 100644 --- a/app/Http/Requests/Vendor/UpdateVendorRequest.php +++ b/app/Http/Requests/Vendor/UpdateVendorRequest.php @@ -65,6 +65,8 @@ class UpdateVendorRequest extends Request $rules['documents.*'] = $this->file_validation; } elseif ($this->file('documents')) { $rules['documents'] = $this->file_validation; + }else { + $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { diff --git a/app/Import/Transformer/BaseTransformer.php b/app/Import/Transformer/BaseTransformer.php index ee9b137dcff5..81d38ba75e75 100644 --- a/app/Import/Transformer/BaseTransformer.php +++ b/app/Import/Transformer/BaseTransformer.php @@ -315,14 +315,11 @@ class BaseTransformer public function getFloat($data, $field) { if (array_key_exists($field, $data)) { - //$number = preg_replace('/[^0-9-.]+/', '', $data[$field]); return Number::parseFloat($data[$field]); - } else { - //$number = 0; - return 0; - } - - // return Number::parseFloat($number); + } + + return 0; + } /** diff --git a/app/Models/Company.php b/app/Models/Company.php index ea2e2915ae15..f9bd44bd630d 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -112,6 +112,13 @@ use Laracasts\Presenter\PresentableTrait; * @property int $notify_vendor_when_paid * @property int $invoice_task_hours * @property int $deleted_at + * @property string $smtp_username + * @property string $smtp_password + * @property string $smtp_host + * @property string $smtp_port + * @property string $smtp_encryption + * @property string $smtp_local_domain + * @property boolean $smtp_verify_peer * @property-read \App\Models\Account $account * @property-read \Illuminate\Database\Eloquent\Collection $activities * @property-read int|null $activities_count @@ -352,12 +359,19 @@ class Company extends BaseModel 'calculate_taxes', 'tax_data', 'e_invoice_certificate_passphrase', + 'smtp_host', + 'smtp_port', + 'smtp_encryption', + 'smtp_local_domain', + 'smtp_verify_peer', ]; protected $hidden = [ 'id', 'db', 'ip', + 'smtp_username', + 'smtp_password', ]; protected $casts = [ @@ -372,6 +386,8 @@ class Company extends BaseModel 'tax_data' => 'object', 'origin_tax_data' => 'object', 'e_invoice_certificate_passphrase' => EncryptedCast::class, + 'smtp_username' => 'encrypted', + 'smtp_password' => 'encrypted', ]; protected $with = []; diff --git a/app/Models/Document.php b/app/Models/Document.php index e393ef3c4129..061210041ff8 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -176,7 +176,11 @@ class Document extends BaseModel public function generateRoute($absolute = false) { + try{ return route('api.documents.show', ['document' => $this->hashed_id]).'/download'; + }catch(\Exception $e){ + return ''; + } } public function deleteFile() diff --git a/app/Models/License.php b/app/Models/License.php index d2af140651e3..d7af06bb61e9 100644 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -17,7 +17,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * App\Models\License * * @property int $id - * @property int|null $created_at + * @property \Carbon\Carbon $created_at * @property int|null $updated_at * @property int|null $deleted_at * @property string|null $first_name @@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property string|null $transaction_reference * @property int|null $product_id * @property int|null $recurring_invoice_id + * @property-read \App\Models\RecurringInvoice $recurring_invoice * @method static \Illuminate\Database\Eloquent\Builder|StaticModel company() * @method static \Illuminate\Database\Eloquent\Builder|StaticModel exclude($columns) * @method static \Illuminate\Database\Eloquent\Builder|License newModelQuery() @@ -53,4 +54,24 @@ use Illuminate\Database\Eloquent\SoftDeletes; class License extends StaticModel { use SoftDeletes; -} + + protected $casts = [ + 'created_at' => 'date', + ]; + + public function expiry(): string + { + return $this->created_at->addYear()->format('Y-m-d'); + } + + public function recurring_invoice() + { + return $this->belongsTo(RecurringInvoice::class); + } + + public function url() + { + $contact = $this->recurring_invoice->client->contacts()->where('email', $this->email)->first(); + + } +} \ No newline at end of file diff --git a/app/Models/Project.php b/app/Models/Project.php index be9784d0a83a..ca00a95d8ddc 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -75,6 +75,10 @@ class Project extends BaseModel 'number', ]; + protected $with = [ + 'documents', + ]; + public function getEntityType() { return self::class; diff --git a/app/Models/Task.php b/app/Models/Task.php index 16903f6fff62..f1716b15a870 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -131,6 +131,10 @@ class Task extends BaseModel 'deleted_at' => 'timestamp', ]; + protected $with = [ + // 'project', + ]; + protected $touches = []; public function getEntityType() diff --git a/app/Repositories/CompanyRepository.php b/app/Repositories/CompanyRepository.php index da42bd02c97f..96babe4146b7 100644 --- a/app/Repositories/CompanyRepository.php +++ b/app/Repositories/CompanyRepository.php @@ -39,6 +39,7 @@ class CompanyRepository extends BaseRepository $company->fill($data); + // nlog($data); /** Only required to handle v4 migration workloads */ if(Ninja::isHosted() && $company->isDirty('is_disabled') && !$company->is_disabled) { Ninja::triggerForwarding($company->company_key, $company->owner()->email); @@ -48,6 +49,14 @@ class CompanyRepository extends BaseRepository $company->saveSettings($data['settings'], $company); } + if(isset($data['smtp_username'])) { + $company->smtp_username = $data['smtp_username']; + } + + if(isset($data['smtp_password'])) { + $company->smtp_password = $data['smtp_password']; + } + $company->save(); return $company; diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 73eac355eefe..b0d81c5c9af5 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -22,6 +22,8 @@ use App\Models\Invoice; use App\Models\Payment; use App\Models\PaymentHash; use App\Models\PaymentType; +use App\Repositories\CreditRepository; +use App\Repositories\PaymentRepository; use App\Services\AbstractService; use App\Utils\Ninja; use Illuminate\Support\Str; @@ -31,7 +33,7 @@ class AutoBillInvoice extends AbstractService private Client $client; private array $used_credit = []; - + /*Specific variable for partial payments */ private bool $is_partial_amount = false; @@ -66,8 +68,12 @@ class AutoBillInvoice extends AbstractService $this->applyCreditPayment(); } + if($this->client->getSetting('use_unapplied_payment') != 'off') { + $this->applyUnappliedPayment(); + } + //If this returns true, it means a partial invoice amount was paid as a credit and there is no further balance payable - if ($this->is_partial_amount && $this->invoice->partial == 0) { + if (($this->is_partial_amount && $this->invoice->partial == 0) || (int)$this->invoice->balance == 0) { return; } @@ -176,9 +182,6 @@ class AutoBillInvoice extends AbstractService $payment->amount = 0; $payment->applied = 0; - - // $payment->amount = $amount; - // $payment->applied = $amount; $payment->client_id = $this->invoice->client_id; $payment->currency_id = $this->invoice->client->getSetting('currency_id'); $payment->date = now()->addSeconds($this->invoice->company->utc_offset())->format('Y-m-d'); @@ -217,8 +220,6 @@ class AutoBillInvoice extends AbstractService ->client ->service() ->updateBalanceAndPaidToDate($amount * -1, $amount) - // ->updateBalance($amount * -1) - // ->updatePaidToDate($amount) ->adjustCreditBalance($amount * -1) ->save(); @@ -233,9 +234,7 @@ class AutoBillInvoice extends AbstractService //if we have paid the invoice in full using credits, then we need to fire the event if($this->invoice->balance == 0) { - event(new InvoiceWasPaid($this->invoice, $payment, $payment->company, Ninja::eventVars())); - } return $this->invoice @@ -243,6 +242,84 @@ class AutoBillInvoice extends AbstractService ->setCalculatedStatus() ->save(); } + + /** + * If the client has unapplied payments on file + * we will use these prior to charging a + * payment method on file. + * + * This needs to be wrapped in a transaction. + * + * @return self + */ + private function applyUnappliedPayment(): self + { + $unapplied_payments = Payment::query() + ->where('client_id', $this->client->id) + ->where('status_id', Payment::STATUS_COMPLETED) + ->where('is_deleted', false) + ->whereColumn('amount', '>', 'applied') + ->where('amount', '>', 0) + ->orderBy('created_at') + ->get(); + + $available_unapplied_balance = $unapplied_payments->sum('amount') - $unapplied_payments->sum('applied'); + + nlog("available unapplied balance = {$available_unapplied_balance}"); + + if ((int) $available_unapplied_balance == 0) { + return $this; + } + + if ($this->invoice->partial > 0) { + $this->is_partial_amount = true; + } + + $payment_repo = new PaymentRepository(new CreditRepository()); + + foreach ($unapplied_payments as $key => $payment) { + $payment_balance = $payment->amount - $payment->applied; + + if ($this->is_partial_amount) { + //more than needed + if ($payment_balance > $this->invoice->partial) { + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->partial]]]; + $payment_repo->save($payload, $payment); + + $this->invoice = $this->invoice->fresh(); + + return $this; + } else { + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; + $payment_repo->save($payload, $payment); + } + } else { + //more than needed + if ($payment_balance > $this->invoice->balance) { + + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->balance]]]; + $payment_repo->save($payload, $payment); + + $this->invoice = $this->invoice->fresh(); + + return $this; + + } else { + + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; + $payment_repo->save($payload, $payment); + + } + } + + if((int)$this->invoice->balance == 0) { + event(new InvoiceWasPaid($this->invoice, $payment, $payment->company, Ninja::eventVars())); + return $this; + } + } + + return $this; + } /** * Applies credits to a payment prior to push @@ -260,7 +337,7 @@ class AutoBillInvoice extends AbstractService $available_credit_balance = $available_credits->sum('balance'); - info("available credit balance = {$available_credit_balance}"); + nlog("available credit balance = {$available_credit_balance}"); if ((int) $available_credit_balance == 0) { return $this; @@ -332,14 +409,6 @@ class AutoBillInvoice extends AbstractService })->orderBy('is_default', 'DESC') ->get(); - // $gateway_tokens = $this->client - // ->gateway_tokens() - // ->whereHas('gateway', function ($query) { - // $query->where('is_deleted', 0) - // ->where('deleted_at', null); - // })->orderBy('is_default', 'DESC') - // ->get(); - $filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use ($amount) { $company_gateway = $gateway_token->gateway; diff --git a/app/Services/Pdf/PdfMock.php b/app/Services/Pdf/PdfMock.php index c39fc6f2632b..d6fb4e5aa53c 100644 --- a/app/Services/Pdf/PdfMock.php +++ b/app/Services/Pdf/PdfMock.php @@ -204,9 +204,9 @@ class PdfMock [ '$client.shipping_postal_code' => '46420', '$client.billing_postal_code' => '11243', - '$company.city_state_postal' => 'Beveley Hills, CA, 90210', - '$company.postal_city_state' => 'CA', - '$company.postal_city' => '90210, CA', + '$company.city_state_postal' => "{$this->settings->city}, {$this->settings->state}, {$this->settings->postal_code}", + '$company.postal_city_state' => "{$this->settings->postal_code}, {$this->settings->city}, {$this->settings->state}", + '$company.postal_city' => "{$this->settings->postal_code}, {$this->settings->state}", '$product.gross_line_total' => '100', '$client.classification' => 'Individual', '$company.classification' => 'Business', @@ -262,9 +262,9 @@ class PdfMock '$company.id_number' => $this->settings->id_number, '$invoice.po_number' => 'PO12345', '$invoice_total_raw' => 0.0, - '$postal_city_state' => '11243 Aufderharchester, North Carolina', + '$postal_city_state' => "{$this->settings->postal_code}, {$this->settings->city}, {$this->settings->state}", '$client.vat_number' => '975977515', - '$city_state_postal' => 'Aufderharchester, North Carolina 11243', + '$city_state_postal' => "{$this->settings->city}, {$this->settings->state}, {$this->settings->postal_code}", '$contact.full_name' => 'Benedict Eichmann', '$contact.last_name' => 'Eichmann', '$company.country_2' => 'US', @@ -275,7 +275,7 @@ class PdfMock '$statement_amount' => '', '$task.description' => '', '$product.discount' => '', - '$entity_issued_to' => 'Bob JOnes', + '$entity_issued_to' => 'Bob Jones', '$assigned_to_user' => '', '$product.quantity' => '', '$total_tax_labels' => '', @@ -303,10 +303,10 @@ class PdfMock '$invoice.custom2' => 'custom value', '$invoice.custom3' => 'custom value', '$invoice.custom4' => 'custom value', - '$company.custom1' => 'custom value', - '$company.custom2' => 'custom value', - '$company.custom3' => 'custom value', - '$company.custom4' => 'custom value', + '$company.custom1' => $this->company->custom_value1, + '$company.custom2' => $this->company->custom_value2, + '$company.custom3' => $this->company->custom_value3, + '$company.custom4' => $this->company->custom_value4, '$quote.po_number' => 'PO12345', '$company.website' => $this->settings->website, '$balance_due_raw' => '0.00', @@ -317,8 +317,8 @@ class PdfMock '$user.first_name' => 'Derrick Monahan DDS', '$created_by_user' => 'Derrick Monahan DDS Erna Wunsch', '$client.currency' => 'USD', - '$company.country' => 'United States', - '$company.address' => 'Christiansen Garden
70218 Lori Station Suite 529
New Loy, Delaware 29359
United States
Phone: 1-240-886-2233
Email: immanuel53@example.net
', + '$company.country' => $this->company->country()?->name ?? 'USA', + '$company.address' => $this->company->present()->address(), '$tech_hero_image' => 'http://ninja.test:8000/images/pdf-designs/tech-hero-image.jpg', '$task.tax_name1' => '', '$task.tax_name2' => '', @@ -450,15 +450,15 @@ class PdfMock '$task.tax' => '', '$discount' => '$0.00', '$subtotal' => '$0.00', - '$company1' => 'custom value', - '$company2' => 'custom value', - '$company3' => 'custom value', - '$company4' => 'custom value', + '$company1' => $this->company->custom_value1, + '$company2' => $this->company->custom_value2, + '$company3' => $this->company->custom_value3, + '$company4' => $this->company->custom_value4, '$due_date' => '2022-01-01', '$poNumber' => 'PO-123456', '$quote_no' => '0029', - '$address2' => '63993 Aiyana View', - '$address1' => '8447', + '$address2' => $this->settings->address2, + '$address1' => $this->settings->address1, '$viewLink' => 'View Invoice', '$autoBill' => 'This invoice will automatically be billed to your credit card on file on the due date.', '$view_url' => 'http://ninja.test:8000/client/invoice/UAUY8vIPuno72igmXbbpldwo5BDDKIqs', @@ -477,7 +477,7 @@ class PdfMock '$country' => 'United States', '$contact' => 'Benedict Eichmann', '$app_url' => 'http://ninja.test:8000', - '$website' => 'http://www.parisian.org/', + '$website' => $this->settings->website, '$entity' => '', '$thanks' => 'Thanks!', '$amount' => '$30.00', diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 3d9d21c1b3a5..cdfc7b430b6f 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -47,6 +47,7 @@ use App\Utils\Traits\SubscriptionHooker; use Carbon\Carbon; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Str; +use Illuminate\Mail\Mailables\Address; class SubscriptionService { @@ -208,7 +209,7 @@ class SubscriptionService $invitation = $invoice->invitations()->first(); $email_object = new EmailObject(); - $email_object->to = [$contact->email]; + $email_object->to = [new Address($contact->email, $contact->present()->name())]; $email_object->subject = ctrans('texts.white_label_link') . " " .ctrans('texts.payment_subject'); $email_object->body = ctrans('texts.white_label_body', ['license_key' => $license_key]); $email_object->client_id = $invoice->client_id; diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index da5c7bf0e3b9..9ab4eaa25d35 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -711,7 +711,10 @@ class TemplateService private function getPaymentRefundActivity(Payment $payment): array { - return collect($payment->refund_meta ?? []) + if(!is_array($payment->refund_meta)) + return []; + + return collect($payment->refund_meta) ->map(function ($refund) use ($payment) { $date = \Carbon\Carbon::parse($refund['date'])->addSeconds($payment->client->timezone_offset()); diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index a52cb317b094..9b838f542fe5 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -204,6 +204,13 @@ class CompanyTransformer extends EntityTransformer 'invoice_task_project_header' => (bool) $company->invoice_task_project_header, 'invoice_task_item_description' => (bool) $company->invoice_task_item_description, 'origin_tax_data' => $company->origin_tax_data ?: new \stdClass(), + 'smtp_host' => (string)$company->smtp_host ?? '', + 'smtp_port' => (int)$company->smtp_port ?? 25, + 'smtp_encryption' => (string)$company->smtp_encryption ?? 'tls', + 'smtp_username' => $company->smtp_username ? '********' : '', + 'smtp_password' => $company->smtp_password ? '********' : '', + 'smtp_local_domain' => (string)$company->smtp_local_domain ?? '', + 'smtp_verify_peer' => (bool)$company->smtp_verify_peer, ]; } diff --git a/app/Transformers/ProjectTransformer.php b/app/Transformers/ProjectTransformer.php index 729552b58e69..6f66dd23f15c 100644 --- a/app/Transformers/ProjectTransformer.php +++ b/app/Transformers/ProjectTransformer.php @@ -41,7 +41,10 @@ class ProjectTransformer extends EntityTransformer { $transformer = new DocumentTransformer($this->serializer); - return $this->includeCollection($project->documents, $transformer, Document::class); + if($project->documents) + return $this->includeCollection($project->documents, $transformer, Document::class); + + return null; } public function includeClient(Project $project): \League\Fractal\Resource\Item diff --git a/app/Transformers/TaskTransformer.php b/app/Transformers/TaskTransformer.php index 131789b32ff6..1bfcf64e9b51 100644 --- a/app/Transformers/TaskTransformer.php +++ b/app/Transformers/TaskTransformer.php @@ -100,11 +100,10 @@ class TaskTransformer extends EntityTransformer { $transformer = new ProjectTransformer($this->serializer); - if (!$task->project) { - return null; - } + if ($task->project) + return $this->includeItem($task->project, $transformer, Project::class); - return $this->includeItem($task->project, $transformer, Project::class); + return null; } public function transform(Task $task) diff --git a/app/Utils/Number.php b/app/Utils/Number.php index 67568d6da81b..2fa7e64a462a 100644 --- a/app/Utils/Number.php +++ b/app/Utils/Number.php @@ -95,6 +95,14 @@ class Number */ public static function parseFloat($value) { + if(!$value) + return 0; + + $multiplier = false; + + if(substr($value, 0,1) == '-') + $multiplier = -1; + // convert "," to "." $s = str_replace(',', '.', $value); @@ -108,7 +116,9 @@ class Number // remove all separators from first part and keep the end $s = str_replace('.', '', substr($s, 0, -3)).substr($s, -3); - // return float + if($multiplier) + $s = floatval($s)*-1; + return (float) $s; } diff --git a/app/Utils/Traits/CleanLineItems.php b/app/Utils/Traits/CleanLineItems.php index 9cb96b913664..6ff730bfc520 100644 --- a/app/Utils/Traits/CleanLineItems.php +++ b/app/Utils/Traits/CleanLineItems.php @@ -75,11 +75,11 @@ trait CleanLineItems } if(isset($item['notes'])) { - $item['notes'] = str_replace(" env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.8.25'), - 'app_tag' => env('APP_TAG', '5.8.25'), + 'app_version' => env('APP_VERSION', '5.8.26'), + 'app_tag' => env('APP_TAG', '5.8.26'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), diff --git a/database/migrations/2024_02_16_011055_smtp_configuration.php b/database/migrations/2024_02_16_011055_smtp_configuration.php new file mode 100644 index 000000000000..38e7ea54e53b --- /dev/null +++ b/database/migrations/2024_02_16_011055_smtp_configuration.php @@ -0,0 +1,34 @@ +string('smtp_host')->nullable(); + $table->unsignedInteger('smtp_port')->nullable(); + $table->string('smtp_encryption')->nullable(); + $table->text('smtp_username')->nullable(); + $table->text('smtp_password')->nullable(); + $table->string('smtp_local_domain')->nullable(); + $table->boolean('smtp_verify_peer')->default(0); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index 3e995d821340..8b6027093966 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4925,7 +4925,7 @@ $lang = array( 'no_assigned_tasks' => 'No billable tasks for this project', 'authorization_failure' => 'Insufficient permissions to perform this action', 'authorization_sms_failure' => 'Please verify your account to send emails.', - 'white_label_body' => 'Thank you for purchasing a white label license.

Your license key is:

:license_key', + 'white_label_body' => 'Thank you for purchasing a white label license.

Your license key is:

:license_key

You can manage your license here: https://invoiceninja.invoicing.co/client/login', 'payment_type_Klarna' => 'Klarna', 'payment_type_Interac E Transfer' => 'Interac E Transfer', 'xinvoice_payable' => 'Payable within :payeddue days net until :paydate', diff --git a/resources/views/email/support/message.blade.php b/resources/views/email/support/message.blade.php index 332f9888e252..ea8449647a5d 100644 --- a/resources/views/email/support/message.blade.php +++ b/resources/views/email/support/message.blade.php @@ -1,4 +1,4 @@ -@component('email.template.admin', ['settings' => $settings, 'logo' => $logo ?? 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png']) +@component('email.template.admin', ['settings' => $settings, 'logo' => $logo ?? 'https://pdf.invoicing.co/favicon-v2.png']) {{-- Body --}} {!! $support_message !!} diff --git a/routes/api.php b/routes/api.php index 904885c59174..311360ae66d8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,113 +10,114 @@ | is assigned the "api" middleware group. Enjoy building your API! | */ -use App\Http\Controllers\AccountController; -use App\Http\Controllers\ActivityController; -use App\Http\Controllers\Auth\ForgotPasswordController; -use App\Http\Controllers\Auth\LoginController; -use App\Http\Controllers\Auth\PasswordTimeoutController; -use App\Http\Controllers\Bank\NordigenController; -use App\Http\Controllers\Bank\YodleeController; -use App\Http\Controllers\BankIntegrationController; -use App\Http\Controllers\BankTransactionController; -use App\Http\Controllers\BankTransactionRuleController; +use Illuminate\Support\Facades\Route; use App\Http\Controllers\BaseController; +use App\Http\Controllers\PingController; +use App\Http\Controllers\SmtpController; +use App\Http\Controllers\TaskController; +use App\Http\Controllers\UserController; use App\Http\Controllers\ChartController; +use App\Http\Controllers\EmailController; +use App\Http\Controllers\QuoteController; +use App\Http\Controllers\TokenController; use App\Http\Controllers\ClientController; -use App\Http\Controllers\ClientGatewayTokenController; -use App\Http\Controllers\ClientStatementController; -use App\Http\Controllers\CompanyController; -use App\Http\Controllers\CompanyGatewayController; -use App\Http\Controllers\CompanyLedgerController; -use App\Http\Controllers\CompanyUserController; -use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\CreditController; use App\Http\Controllers\DesignController; -use App\Http\Controllers\DocumentController; -use App\Http\Controllers\EmailController; -use App\Http\Controllers\EmailHistoryController; -use App\Http\Controllers\ExpenseCategoryController; -use App\Http\Controllers\ExpenseController; use App\Http\Controllers\ExportController; use App\Http\Controllers\FilterController; -use App\Http\Controllers\GroupSettingController; -use App\Http\Controllers\HostedMigrationController; use App\Http\Controllers\ImportController; -use App\Http\Controllers\ImportJsonController; -use App\Http\Controllers\InAppPurchase\AppleController; +use App\Http\Controllers\LogoutController; +use App\Http\Controllers\SearchController; +use App\Http\Controllers\StaticController; +use App\Http\Controllers\StripeController; +use App\Http\Controllers\TwilioController; +use App\Http\Controllers\VendorController; +use App\Http\Controllers\AccountController; +use App\Http\Controllers\CompanyController; +use App\Http\Controllers\ExpenseController; use App\Http\Controllers\InvoiceController; use App\Http\Controllers\LicenseController; -use App\Http\Controllers\LogoutController; -use App\Http\Controllers\MailgunWebhookController; -use App\Http\Controllers\MigrationController; -use App\Http\Controllers\OneTimeTokenController; use App\Http\Controllers\PaymentController; -use App\Http\Controllers\PaymentNotificationWebhookController; -use App\Http\Controllers\PaymentTermController; -use App\Http\Controllers\PaymentWebhookController; -use App\Http\Controllers\PingController; -use App\Http\Controllers\PostMarkController; use App\Http\Controllers\PreviewController; -use App\Http\Controllers\PreviewPurchaseOrderController; use App\Http\Controllers\ProductController; use App\Http\Controllers\ProjectController; -use App\Http\Controllers\ProtectedDownloadController; +use App\Http\Controllers\TaxRateController; +use App\Http\Controllers\WebCronController; +use App\Http\Controllers\WebhookController; +use App\Http\Controllers\ActivityController; +use App\Http\Controllers\DocumentController; +use App\Http\Controllers\PostMarkController; +use App\Http\Controllers\TemplateController; +use App\Http\Controllers\MigrationController; +use App\Http\Controllers\SchedulerController; +use App\Http\Controllers\SubdomainController; +use App\Http\Controllers\SystemLogController; +use App\Http\Controllers\TwoFactorController; +use App\Http\Controllers\Auth\LoginController; +use App\Http\Controllers\ImportJsonController; +use App\Http\Controllers\SelfUpdateController; +use App\Http\Controllers\TaskStatusController; +use App\Http\Controllers\Bank\YodleeController; +use App\Http\Controllers\CompanyUserController; +use App\Http\Controllers\PaymentTermController; +use App\PaymentDrivers\PayPalPPCPPaymentDriver; +use App\Http\Controllers\EmailHistoryController; +use App\Http\Controllers\GroupSettingController; +use App\Http\Controllers\OneTimeTokenController; +use App\Http\Controllers\SubscriptionController; +use App\Http\Controllers\Bank\NordigenController; +use App\Http\Controllers\CompanyLedgerController; use App\Http\Controllers\PurchaseOrderController; -use App\Http\Controllers\QuoteController; +use App\Http\Controllers\TaskSchedulerController; +use App\Http\Controllers\CompanyGatewayController; +use App\Http\Controllers\MailgunWebhookController; +use App\Http\Controllers\PaymentWebhookController; +use App\Http\Controllers\RecurringQuoteController; +use App\Http\Controllers\BankIntegrationController; +use App\Http\Controllers\BankTransactionController; +use App\Http\Controllers\ClientStatementController; +use App\Http\Controllers\ExpenseCategoryController; +use App\Http\Controllers\HostedMigrationController; +use App\Http\Controllers\TemplatePreviewController; +use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\RecurringExpenseController; use App\Http\Controllers\RecurringInvoiceController; -use App\Http\Controllers\RecurringQuoteController; -use App\Http\Controllers\Reports\ActivityReportController; -use App\Http\Controllers\Reports\ARDetailReportController; -use App\Http\Controllers\Reports\ARSummaryReportController; -use App\Http\Controllers\Reports\ClientBalanceReportController; -use App\Http\Controllers\Reports\ClientContactReportController; +use App\Http\Controllers\ProtectedDownloadController; +use App\Http\Controllers\ClientGatewayTokenController; +use App\Http\Controllers\Reports\TaskReportController; +use App\Http\Controllers\Auth\ForgotPasswordController; +use App\Http\Controllers\BankTransactionRuleController; +use App\Http\Controllers\InAppPurchase\AppleController; +use App\Http\Controllers\Reports\QuoteReportController; +use App\Http\Controllers\Auth\PasswordTimeoutController; +use App\Http\Controllers\PreviewPurchaseOrderController; use App\Http\Controllers\Reports\ClientReportController; -use App\Http\Controllers\Reports\ClientSalesReportController; use App\Http\Controllers\Reports\CreditReportController; -use App\Http\Controllers\Reports\DocumentReportController; +use App\Http\Controllers\Reports\ReportExportController; +use App\Http\Controllers\Reports\VendorReportController; use App\Http\Controllers\Reports\ExpenseReportController; -use App\Http\Controllers\Reports\InvoiceItemReportController; use App\Http\Controllers\Reports\InvoiceReportController; use App\Http\Controllers\Reports\PaymentReportController; use App\Http\Controllers\Reports\ProductReportController; -use App\Http\Controllers\Reports\ProductSalesReportController; use App\Http\Controllers\Reports\ProfitAndLossController; -use App\Http\Controllers\Reports\PurchaseOrderItemReportController; -use App\Http\Controllers\Reports\PurchaseOrderReportController; -use App\Http\Controllers\Reports\QuoteItemReportController; -use App\Http\Controllers\Reports\QuoteReportController; -use App\Http\Controllers\Reports\RecurringInvoiceReportController; -use App\Http\Controllers\Reports\ReportExportController; use App\Http\Controllers\Reports\ReportPreviewController; -use App\Http\Controllers\Reports\TaskReportController; -use App\Http\Controllers\Reports\TaxSummaryReportController; +use App\Http\Controllers\Reports\ActivityReportController; +use App\Http\Controllers\Reports\ARDetailReportController; +use App\Http\Controllers\Reports\DocumentReportController; +use App\Http\Controllers\Reports\ARSummaryReportController; +use App\Http\Controllers\Reports\QuoteItemReportController; use App\Http\Controllers\Reports\UserSalesReportController; -use App\Http\Controllers\Reports\VendorReportController; -use App\Http\Controllers\SchedulerController; -use App\Http\Controllers\SearchController; -use App\Http\Controllers\SelfUpdateController; -use App\Http\Controllers\StaticController; -use App\Http\Controllers\StripeController; -use App\Http\Controllers\SubdomainController; -use App\Http\Controllers\SubscriptionController; +use App\Http\Controllers\Reports\TaxSummaryReportController; use App\Http\Controllers\Support\Messages\SendingController; -use App\Http\Controllers\SystemLogController; -use App\Http\Controllers\TaskController; -use App\Http\Controllers\TaskSchedulerController; -use App\Http\Controllers\TaskStatusController; -use App\Http\Controllers\TaxRateController; -use App\Http\Controllers\TemplateController; -use App\Http\Controllers\TemplatePreviewController; -use App\Http\Controllers\TokenController; -use App\Http\Controllers\TwilioController; -use App\Http\Controllers\TwoFactorController; -use App\Http\Controllers\UserController; -use App\Http\Controllers\VendorController; -use App\Http\Controllers\WebCronController; -use App\Http\Controllers\WebhookController; -use App\PaymentDrivers\PayPalPPCPPaymentDriver; -use Illuminate\Support\Facades\Route; +use App\Http\Controllers\Reports\ClientSalesReportController; +use App\Http\Controllers\Reports\InvoiceItemReportController; +use App\Http\Controllers\PaymentNotificationWebhookController; +use App\Http\Controllers\Reports\ProductSalesReportController; +use App\Http\Controllers\Reports\ClientBalanceReportController; +use App\Http\Controllers\Reports\ClientContactReportController; +use App\Http\Controllers\Reports\PurchaseOrderReportController; +use App\Http\Controllers\Reports\RecurringInvoiceReportController; +use App\Http\Controllers\Reports\PurchaseOrderItemReportController; Route::group(['middleware' => ['throttle:api', 'api_secret_check']], function () { Route::post('api/v1/signup', [AccountController::class, 'store'])->name('signup.submit'); @@ -392,6 +393,8 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] // Route::post('hooks', [SubscriptionController::class, 'subscribe'])->name('hooks.subscribe'); // Route::delete('hooks/{subscription_id}', [SubscriptionController::class, 'unsubscribe'])->name('hooks.unsubscribe'); + Route::post('smtp/check', [SmtpController::class, 'check'])->name('smtp.check')->middleware('throttle:10,1'); + Route::post('stripe/update_payment_methods', [StripeController::class, 'update'])->middleware('password_protected')->name('stripe.update'); Route::post('stripe/import_customers', [StripeController::class, 'import'])->middleware('password_protected')->name('stripe.import'); diff --git a/tests/Feature/ClientApiTest.php b/tests/Feature/ClientApiTest.php index 882fd3cdd459..0d08a8642b10 100644 --- a/tests/Feature/ClientApiTest.php +++ b/tests/Feature/ClientApiTest.php @@ -59,6 +59,78 @@ class ClientApiTest extends TestCase Model::reguard(); } + public function testDocumentValidation() + { + $data = [ + 'name' => 'name of client', + 'documents' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/clients",$data) + ->assertStatus(200); + + } + + public function testDocumentValidationFails() + { + $data = [ + 'name' => 'name of client', + 'documents' => 'wut', + ]; + + $response = $this->withHeaders([ + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/clients", $data) + ->assertStatus(422); + + $data = [ + 'name' => 'name of client', + 'documents' => null, + ]; + + $response = $this->withHeaders([ + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/clients", $data) + ->assertStatus(422); + + } + + public function testDocumentValidationPutFails() + { + $data = [ + 'name' => 'name of client', + 'documents' => 'wut', + ]; + + $response = $this->withHeaders([ + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/clients/{$this->client->hashed_id}", $data) + ->assertStatus(422); + + $data = [ + 'name' => 'name of client', + 'documents' => null, + ]; + + $response = $this->withHeaders([ + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/clients/{$this->client->hashed_id}", $data) + ->assertStatus(422); + + $data = [ + 'name' => 'name of client', + 'documents' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/clients/{$this->client->hashed_id}", $data) + ->assertStatus(200); + + } + public function testClientDocumentQuery() { diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index 91e412812951..a9c02ef83045 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -50,6 +50,15 @@ class CompanyTest extends TestCase $this->makeTestData(); } + public function testEnsureStrReplace() + { + $x = '**********'; + + $new_string = str_replace("*", "", $x); + + $this->assertEquals(0, strlen($new_string)); + } + public function testCompanyTaxInit() { TaxRate::query()->delete(); diff --git a/tests/Feature/Payments/AutoUnappliedPaymentTest.php b/tests/Feature/Payments/AutoUnappliedPaymentTest.php new file mode 100644 index 000000000000..96e3a967a22f --- /dev/null +++ b/tests/Feature/Payments/AutoUnappliedPaymentTest.php @@ -0,0 +1,180 @@ +faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + // $this->withoutExceptionHandling(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + public function testUnappliedPaymentsAreEnabled() + { + + $settings = ClientSettings::defaults(); + $settings->use_unapplied_payment = 'always'; + + $client = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'settings' => $settings, + ]); + + $this->assertEquals('always', $client->settings->use_unapplied_payment); + + $invoice = Invoice::factory()->for($client)->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'auto_bill_enabled' => true, + 'client_id' => $client->id, + ]); + + $invoice = $invoice->calc()->getInvoice(); + + $payment = Payment::factory()->for($client)->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'amount' => 100, + 'applied' => 0, + 'refunded' => 0, + 'status_id' => Payment::STATUS_COMPLETED, + 'is_deleted' => 0, + ]); + + $invoice->service()->markSent()->save(); + + $this->assertGreaterThan(0, $invoice->balance); + + nlog($invoice->balance); + + try{ + $invoice->service()->autoBill()->save(); + } + catch(\Exception $e){ + + } + + $invoice = $invoice->fresh(); + $payment = $payment->fresh(); + + nlog($invoice->toArray()); + nlog($payment->toArray()); + + $this->assertEquals($payment->applied, $invoice->paid_to_date); + $this->assertGreaterThan(2, $invoice->status_id); + $this->assertGreaterThan(0, $payment->applied); + + // $this->assertEquals(Invoice::STATUS_PAID, $invoice->status_id); + // $this->assertEquals(0, $invoice->balance); + + } + + + public function testUnappliedPaymentsAreDisabled() + { + + $settings = ClientSettings::defaults(); + $settings->use_unapplied_payment = 'off'; + + $client = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'settings' => $settings, + ]); + + $this->assertEquals('off', $client->settings->use_unapplied_payment); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'auto_bill_enabled' => true, + 'status_id' => 2 + ]); + $invoice = $invoice->calc()->getInvoice(); + $invoice_balance = $invoice->balance; + + $payment = Payment::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'amount' => 100, + 'applied' => 0, + 'refunded' => 0, + 'status_id' => Payment::STATUS_COMPLETED + ]); + + $invoice->service()->markSent()->save(); + + $this->assertGreaterThan(0, $invoice->balance); + + try { + $invoice->service()->autoBill()->save(); + } + catch(\Exception $e) { + + } + + $invoice = $invoice->fresh(); + $payment = $payment->fresh(); + + $this->assertEquals($invoice_balance, $invoice->balance); + $this->assertEquals(0, $payment->applied); + $this->assertEquals(2, $invoice->status_id); + $this->assertEquals(0, $invoice->paid_to_date); + $this->assertEquals($invoice->amount, $invoice->balance); + + // $this->assertEquals($payment->applied, $invoice->paid_to_date); + // $this->assertEquals(2, $invoice->status_id); + + + } + +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a69d65..ffa0f237e183 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,9 +2,19 @@ namespace Tests; +use App\Utils\Traits\AppSetup; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; + use AppSetup; + + protected function setUp() :void + { + parent::setUp(); + + $this->buildCache(true); + + } }