diff --git a/VERSION.txt b/VERSION.txt index c355d6e2184c..a62031042c45 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.10.0 \ No newline at end of file +5.10.1 \ No newline at end of file diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index b3ab47c4a5d4..374e7783c3fe 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -1243,6 +1243,7 @@ class BaseExport * Add Date Range * * @param Builder $query + * @param ?string $table_name * @return Builder */ protected function addDateRange(Builder $query, ?string $table_name = null): Builder @@ -1251,7 +1252,7 @@ class BaseExport $date_range = $this->input['date_range']; - if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1 && ($this->table_name && $this->columnExists($table_name, $this->input['date_key']))) { + if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) { $this->date_key = $this->input['date_key']; } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 84f25f501828..f163bb45ad91 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -503,7 +503,7 @@ class InvoiceController extends BaseController $invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get(); - if (! $invoices) { + if ($invoices->count() == 0 ) { return response()->json(['message' => 'No Invoices Found']); } diff --git a/app/Http/Requests/Invoice/BulkInvoiceRequest.php b/app/Http/Requests/Invoice/BulkInvoiceRequest.php index 9b5fcfb54d6c..153a4ea42f19 100644 --- a/app/Http/Requests/Invoice/BulkInvoiceRequest.php +++ b/app/Http/Requests/Invoice/BulkInvoiceRequest.php @@ -12,6 +12,7 @@ namespace App\Http\Requests\Invoice; use App\Http\Requests\Request; +use App\Exceptions\DuplicatePaymentException; class BulkInvoiceRequest extends Request { @@ -29,7 +30,21 @@ class BulkInvoiceRequest extends Request 'template' => 'sometimes|string', 'template_id' => 'sometimes|string', 'send_email' => 'sometimes|bool', - 'subscriptin_id' => 'sometimes|string', + 'subscription_id' => 'sometimes|string', ]; } + + public function prepareForValidation() + { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('action', 0)."|".json_encode($this->input('ids', ''))."|".$user->company()->company_key)) + throw new DuplicatePaymentException('Duplicate request.', 429); + + \Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('action', 0)."|".json_encode($this->input('ids', ''))."|".$user->company()->company_key), true, 1); + + } + } diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index eb0395450ac9..1b8d8693729e 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -123,7 +123,7 @@ class StoreInvoiceRequest extends Request $client = \App\Models\Client::withTrashed()->find($input['client_id']); if($client) { - $input['due_date'] = \Illuminate\Support\Carbon::parse($input['date'])->addDays($client->getSetting('payment_terms'))->format('Y-m-d'); + $input['due_date'] = \Illuminate\Support\Carbon::parse($input['date'])->addDays((int)$client->getSetting('payment_terms'))->format('Y-m-d'); } } diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index f4ccb51684c9..0c71d72ac8ed 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -116,7 +116,7 @@ class UpdateInvoiceRequest extends Request //handles edge case where we need for force set the due date of the invoice. if((isset($input['partial_due_date']) && strlen($input['partial_due_date']) > 1) && (!array_key_exists('due_date', $input) || (empty($input['due_date']) && empty($this->invoice->due_date)))) { $client = \App\Models\Client::withTrashed()->find($input['client_id']); - $input['due_date'] = \Illuminate\Support\Carbon::parse($input['date'])->addDays($client->getSetting('payment_terms'))->format('Y-m-d'); + $input['due_date'] = \Illuminate\Support\Carbon::parse($input['date'])->addDays((int)$client->getSetting('payment_terms'))->format('Y-m-d'); } $this->replace($input); diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index f1e7ba47b74a..e3d0bd4cfb52 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -105,7 +105,7 @@ class StoreQuoteRequest extends Request if(isset($input['partial_due_date']) && (!isset($input['due_date']) || strlen($input['due_date']) <= 1)) { $client = \App\Models\Client::withTrashed()->find($input['client_id']); $valid_days = ($client && strlen($client->getSetting('valid_until')) >= 1) ? $client->getSetting('valid_until') : 7; - $input['due_date'] = \Carbon\Carbon::parse($input['date'])->addDays($valid_days)->format('Y-m-d'); + $input['due_date'] = \Carbon\Carbon::parse($input['date'])->addDays((int)$valid_days)->format('Y-m-d'); } $this->replace($input); diff --git a/app/Jobs/Ninja/SystemMaintenance.php b/app/Jobs/Ninja/SystemMaintenance.php index 4fec40e89cb0..4b6fbe594dbd 100644 --- a/app/Jobs/Ninja/SystemMaintenance.php +++ b/app/Jobs/Ninja/SystemMaintenance.php @@ -71,7 +71,7 @@ class SystemMaintenance implements ShouldQueue } Invoice::with('invitations') - ->whereBetween('created_at', [now()->subYear(), now()->subDays($delete_pdf_days)]) + ->whereBetween('created_at', [now()->subYear(), now()->subDays((int)$delete_pdf_days)]) ->withTrashed() ->cursor() ->each(function ($invoice) { @@ -81,7 +81,7 @@ class SystemMaintenance implements ShouldQueue }); Quote::with('invitations') - ->whereBetween('created_at', [now()->subYear(), now()->subDays($delete_pdf_days)]) + ->whereBetween('created_at', [now()->subYear(), now()->subDays((int)$delete_pdf_days)]) ->withTrashed() ->cursor() ->each(function ($quote) { @@ -91,7 +91,7 @@ class SystemMaintenance implements ShouldQueue }); Credit::with('invitations') - ->whereBetween('created_at', [now()->subYear(), now()->subDays($delete_pdf_days)]) + ->whereBetween('created_at', [now()->subYear(), now()->subDays((int)$delete_pdf_days)]) ->withTrashed() ->cursor() ->each(function ($credit) { @@ -107,7 +107,7 @@ class SystemMaintenance implements ShouldQueue return; } - Backup::where('created_at', '<', now()->subDays($delete_backup_days)) + Backup::where('created_at', '<', now()->subDays((int)$delete_backup_days)) ->cursor() ->each(function ($backup) { nlog("deleting {$backup->filename}"); diff --git a/app/Jobs/Util/QuoteReminderJob.php b/app/Jobs/Util/QuoteReminderJob.php index b2fe8dbc1603..ed1aa5fa722a 100644 --- a/app/Jobs/Util/QuoteReminderJob.php +++ b/app/Jobs/Util/QuoteReminderJob.php @@ -125,22 +125,15 @@ class QuoteReminderJob implements ShouldQueue return; } - $reminder_template = $quote->calculateTemplate('invoice'); + $reminder_template = $quote->calculateTemplate('quote'); nrlog("#{$quote->number} => reminder template = {$reminder_template}"); $quote->service()->touchReminder($reminder_template)->save(); - $fees = $this->calcLateFee($quote, $reminder_template); - - if($quote->isLocked()) { - return $this->addFeeToNewQuote($quote, $reminder_template, $fees); - } - - $quote = $this->setLateFee($quote, $fees[0], $fees[1]); //20-04-2022 fixes for endless reminders - generic template naming was wrong - $enabled_reminder = 'enable_'.$reminder_template; - if ($reminder_template == 'endless_reminder') { - $enabled_reminder = 'enable_reminder_endless'; - } + $enabled_reminder = 'enable_quote_'.$reminder_template; + // if ($reminder_template == 'endless_reminder') { + // $enabled_reminder = 'enable_reminder_endless'; + // } if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) && $quote->client->getSetting($enabled_reminder) && @@ -149,9 +142,9 @@ class QuoteReminderJob implements ShouldQueue $quote->invitations->each(function ($invitation) use ($quote, $reminder_template) { if ($invitation->contact && !$invitation->contact->trashed() && $invitation->contact->email) { EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); - nrlog("Firing reminder email for invoice {$quote->number} - {$reminder_template}"); + nrlog("Firing reminder email for quote {$quote->number} - {$reminder_template}"); $quote->entityEmailEvent($invitation, $reminder_template); - $quote->sendEvent(Webhook::EVENT_REMIND_INVOICE, "client"); + $quote->sendEvent(Webhook::EVENT_REMIND_QUOTE, "client"); } }); } @@ -162,162 +155,4 @@ class QuoteReminderJob implements ShouldQueue } } - private function addFeeToNewQuote(Quote $over_due_quote, string $reminder_template, array $fees) - { - - $amount = $fees[0]; - $percent = $fees[1]; - - $quote = false; - - //2024-06-07 this early return prevented any reminders from sending for users who enabled lock_invoices. - if ($amount > 0 || $percent > 0) { - // return; - - $fee = $amount; - - if ($over_due_quote->partial > 0) { - $fee += round($over_due_quote->partial * $percent / 100, 2); - } else { - $fee += round($over_due_quote->balance * $percent / 100, 2); - } - - /** @var \App\Models\Invoice $quote */ - $quote = InvoiceFactory::create($over_due_quote->company_id, $over_due_quote->user_id); - $quote->client_id = $over_due_quote->client_id; - $quote->date = now()->format('Y-m-d'); - $quote->due_date = now()->format('Y-m-d'); - - $quote_item = new InvoiceItem(); - $quote_item->type_id = '5'; - $quote_item->product_key = trans('texts.fee'); - $quote_item->notes = ctrans('texts.late_fee_added_locked_invoice', ['invoice' => $over_due_quote->number, 'date' => $this->translateDate(now()->startOfDay(), $over_due_invoice->client->date_format(), $over_due_invoice->client->locale())]); - $quote_item->quantity = 1; - $quote_item->cost = $fee; - - $quote_items = []; - $quote_items[] = $quote_item; - - $quote->line_items = $quote_items; - - /**Refresh Invoice values*/ - $quote = $quote->calc()->getInvoice(); - $quote->service() - ->createInvitations() - ->applyNumber() - ->markSent() - ->save(); - } - - if(!$quote) { - $quote = $over_due_quote; - } - - $enabled_reminder = 'enable_'.$reminder_template; - // if ($reminder_template == 'endless_reminder') { - // $enabled_reminder = 'enable_reminder_endless'; - // } - - if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) && - $quote->client->getSetting($enabled_reminder) && - $quote->client->getSetting('send_reminders') && - (Ninja::isSelfHost() || $quote->company->account->isPaidHostedClient())) { - $quote->invitations->each(function ($invitation) use ($quote, $reminder_template) { - if ($invitation->contact && !$invitation->contact->trashed() && $invitation->contact->email) { - EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); - nrlog("Firing reminder email for qipte {$quote->number} - {$reminder_template}"); - event(new QuoteReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); - $quote->sendEvent(Webhook::EVENT_REMIND_QUOTE, "client"); - } - }); - } - - $quote->service()->setReminder()->save(); - - } - - /** - * Calculates the late if - if any - and rebuilds the invoice - * - * @param Invoice $quote - * @param string $template - * @return array - */ - private function calcLateFee($quote, $template): array - { - $late_fee_amount = 0; - $late_fee_percent = 0; - - switch ($template) { - case 'reminder1': - $late_fee_amount = $quote->client->getSetting('late_fee_amount1'); - $late_fee_percent = $quote->client->getSetting('late_fee_percent1'); - break; - case 'reminder2': - $late_fee_amount = $quote->client->getSetting('late_fee_amount2'); - $late_fee_percent = $quote->client->getSetting('late_fee_percent2'); - break; - case 'reminder3': - $late_fee_amount = $quote->client->getSetting('late_fee_amount3'); - $late_fee_percent = $quote->client->getSetting('late_fee_percent3'); - break; - case 'endless_reminder': - $late_fee_amount = $quote->client->getSetting('late_fee_endless_amount'); - $late_fee_percent = $quote->client->getSetting('late_fee_endless_percent'); - break; - default: - $late_fee_amount = 0; - $late_fee_percent = 0; - break; - } - - return [$late_fee_amount, $late_fee_percent]; - } - - /** - * Applies the late fee to the invoice line items - * - * @param Invoice $quote - * @param float $amount The fee amount - * @param float $percent The fee percentage amount - * - * @return Invoice - */ - private function setLateFee($quote, $amount, $percent): Invoice - { - - $temp_invoice_balance = $quote->balance; - - if ($amount <= 0 && $percent <= 0) { - return $quote; - } - - $fee = $amount; - - if ($quote->partial > 0) { - $fee += round($quote->partial * $percent / 100, 2); - } else { - $fee += round($quote->balance * $percent / 100, 2); - } - - $quote_item = new InvoiceItem(); - $quote_item->type_id = '5'; - $quote_item->product_key = trans('texts.fee'); - $quote_item->notes = ctrans('texts.late_fee_added', ['date' => $this->translateDate(now()->startOfDay(), $quote->client->date_format(), $quote->client->locale())]); - $quote_item->quantity = 1; - $quote_item->cost = $fee; - - $quote_items = $quote->line_items; - $quote_items[] = $quote_item; - - $quote->line_items = $quote_items; - - /**Refresh Invoice values*/ - $quote = $quote->calc()->getInvoice(); - - $quote->ledger()->updateInvoiceBalance($quote->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$quote->number}"); - $quote->client->service()->calculateBalance(); - - return $quote; - } } diff --git a/app/Models/Quote.php b/app/Models/Quote.php index b0f7a17f6d53..f751590cc42e 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -397,28 +397,50 @@ class Quote extends BaseModel */ public function calculateTemplate(string $entity_string): string { - return $entity_string; + + $client = $this->client; + + if ($entity_string != 'quote') { + return $entity_string; + } + + if ($this->inReminderWindow( + $client->getSetting('quote_schedule_reminder1'), + $client->getSetting('quote_num_days_reminder1') + ) && ! $this->reminder1_sent) { + return 'reminder1'; + // } elseif ($this->inReminderWindow( + // $client->getSetting('schedule_reminder2'), + // $client->getSetting('num_days_reminder2') + // ) && ! $this->reminder2_sent) { + // return 'reminder2'; + // } elseif ($this->inReminderWindow( + // $client->getSetting('schedule_reminder3'), + // $client->getSetting('num_days_reminder3') + // ) && ! $this->reminder3_sent) { + // return 'reminder3'; + // } elseif ($this->checkEndlessReminder( + // $this->reminder_last_sent, + // $client->getSetting('endless_reminder_frequency_id') + // )) { + // return 'endless_reminder'; + } else { + return $entity_string; + } + } /** - * isPayable - proxy for matching Invoice status as - * to whether the quote is still valid, allows - * reuse of UpdateReminder class - * * @return bool */ - public function isPayable(): bool + public function canRemind(): bool { - if ($this->status_id == self::STATUS_SENT && $this->is_deleted == false && $this->due_date->gte(now()->addSeconds($this->timezone_offset()))) { - return true; - } elseif ($this->status_id == self::STATUS_DRAFT || $this->is_deleted) { + if (in_array($this->status_id, [self::STATUS_DRAFT, self::STATUS_APPROVED, self::STATUS_CONVERTED]) || $this->is_deleted) return false; - } elseif (in_array($this->status_id, [self::STATUS_APPROVED, self::STATUS_CONVERTED])) { - return false; - } else { - return false; - } + + return true; + } } diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index d236054c35f8..051903c73df8 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -684,7 +684,7 @@ class RecurringInvoice extends BaseModel return null; } - return $new_date->addDays($client_payment_terms); //add the number of days in the payment terms to the date + return $new_date->addDays((int)$client_payment_terms); //add the number of days in the payment terms to the date } /** diff --git a/app/Models/RecurringQuote.php b/app/Models/RecurringQuote.php index b2d86d92dc1f..c9093c5276af 100644 --- a/app/Models/RecurringQuote.php +++ b/app/Models/RecurringQuote.php @@ -569,7 +569,7 @@ class RecurringQuote extends BaseModel return null; } - return $new_date->addDays($client_payment_terms); //add the number of days in the payment terms to the date + return $new_date->addDays((int)$client_payment_terms); //add the number of days in the payment terms to the date } /** diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php index 101d7e680e5f..5d32bb7dc594 100644 --- a/app/Repositories/InvoiceRepository.php +++ b/app/Repositories/InvoiceRepository.php @@ -64,6 +64,8 @@ class InvoiceRepository extends BaseRepository */ public function delete($invoice): Invoice { + $invoice = $invoice->fresh(); + if ($invoice->is_deleted) { return $invoice; } diff --git a/app/Services/Chart/ChartQueries.php b/app/Services/Chart/ChartQueries.php index aa970c5e3a24..5826bbb4ea52 100644 --- a/app/Services/Chart/ChartQueries.php +++ b/app/Services/Chart/ChartQueries.php @@ -37,6 +37,50 @@ trait ChartQueries ", ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); } + public function getAggregateExpenseQuery($start_date, $end_date) + { + $user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id; + + return DB::select(" + SELECT + sum(expenses.amount / IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(clients.settings, '$.currency_id')) AS SIGNED), :company_currency2)) as amount, + IFNULL(expenses.currency_id, :company_currency) as currency_id + FROM expenses + JOIN clients + ON expenses.client_id=clients.id + WHERE expenses.is_deleted = 0 + AND expenses.company_id = :company_id + AND (expenses.date BETWEEN :start_date AND :end_date) + {$user_filter} + GROUP BY currency_id + ", ['company_currency2' => $this->company->settings->currency_id, 'company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); + } + + public function getAggregateExpenseChartQuery($start_date, $end_date) + { + + $user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id; + + return DB::select(" + SELECT + sum(expenses.amount / IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(clients.settings, '$.currency_id')) AS SIGNED), :company_currency)) as total, + expenses.date + FROM expenses + JOIN clients + ON expenses.client_id=clients.id + WHERE (expenses.date BETWEEN :start_date AND :end_date) + AND expenses.company_id = :company_id + AND expenses.is_deleted = 0 + {$user_filter} + GROUP BY expenses.date + ", [ + 'company_currency' => $this->company->settings->currency_id, + 'company_id' => $this->company->id, + 'start_date' => $start_date, + 'end_date' => $end_date, + ]); + } + public function getExpenseChartQuery($start_date, $end_date, $currency_id) { @@ -88,6 +132,53 @@ trait ChartQueries ]); } + public function getAggregatePaymentQuery($start_date, $end_date) + { + + $user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id; + + return DB::select(" + SELECT sum(payments.amount / payments.exchange_rate) as amount, + IFNULL(payments.currency_id, :company_currency) as currency_id + FROM payments + WHERE payments.is_deleted = 0 + {$user_filter} + AND payments.company_id = :company_id + AND (payments.date BETWEEN :start_date AND :end_date) + GROUP BY currency_id + ", [ + 'company_currency' => $this->company->settings->currency_id, + 'company_id' => $this->company->id, + 'start_date' => $start_date, + 'end_date' => $end_date, + ]); + } + + public function getAggregatePaymentChartQuery($start_date, $end_date) + { + + $user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id; + + return DB::select(" + SELECT + sum((payments.amount - payments.refunded) / payments.exchange_rate) as total, + payments.date, + IFNULL(payments.currency_id, :company_currency) AS currency_id + FROM payments + WHERE payments.company_id = :company_id + AND payments.is_deleted = 0 + {$user_filter} + AND payments.status_id IN (4,5,6) + AND (payments.date BETWEEN :start_date AND :end_date) + GROUP BY payments.date + ", [ + 'company_currency' => $this->company->settings->currency_id, + 'company_id' => $this->company->id, + 'start_date' => $start_date, + 'end_date' => $end_date, + ]); + } + public function getPaymentChartQuery($start_date, $end_date, $currency_id) { @@ -142,28 +233,54 @@ trait ChartQueries ", ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); } - public function getRevenueQueryX($start_date, $end_date) + public function getAggregateOutstandingQuery($start_date, $end_date) { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; return DB::select(" SELECT - sum(invoices.paid_to_date) as paid_to_date, + sum(invoices.balance / IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(clients.settings, '$.currency_id')) AS SIGNED), :company_currency2)) as amount, + COUNT(invoices.id) as outstanding_count, IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT( clients.settings, '$.currency_id' )) AS SIGNED), :company_currency) AS currency_id FROM clients JOIN invoices on invoices.client_id = clients.id - WHERE invoices.company_id = :company_id + WHERE invoices.status_id IN (2,3) + AND invoices.company_id = :company_id AND clients.is_deleted = 0 {$user_filter} AND invoices.is_deleted = 0 - AND invoices.amount > 0 - AND invoices.status_id IN (3,4) + AND invoices.balance > 0 AND (invoices.date BETWEEN :start_date AND :end_date) - GROUP BY currency_id - ", ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); + ", [ + 'company_currency2' => $this->company->settings->currency_id, + 'company_currency' => $this->company->settings->currency_id, + 'company_id' => $this->company->id, + 'start_date' => $start_date, + 'end_date' => $end_date]); + } + public function getAggregateRevenueQuery($start_date, $end_date) + { + $user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id; + + return DB::select(" + SELECT + sum((payments.amount - payments.refunded) * payments.exchange_rate) as paid_to_date, + payments.currency_id AS currency_id + FROM payments + WHERE payments.company_id = :company_id + AND payments.is_deleted = 0 + {$user_filter} + AND payments.status_id IN (1,4,5,6) + AND (payments.date BETWEEN :start_date AND :end_date) + GROUP BY payments.currency_id + ", ['company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); + } + + public function getRevenueQuery($start_date, $end_date) { $user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id; @@ -182,6 +299,30 @@ trait ChartQueries ", ['company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); } + + public function getAggregateInvoicesQuery($start_date, $end_date) + { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + + return DB::select(" + SELECT + sum(invoices.amount / IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(clients.settings, '$.currency_id')) AS SIGNED), :company_currency2)) as invoiced_amount, + IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT( clients.settings, '$.currency_id' )) AS SIGNED), :company_currency) AS currency_id + FROM clients + JOIN invoices + on invoices.client_id = clients.id + WHERE invoices.status_id IN (2,3,4) + AND invoices.company_id = :company_id + {$user_filter} + AND invoices.amount > 0 + AND clients.is_deleted = 0 + AND invoices.is_deleted = 0 + AND (invoices.date BETWEEN :start_date AND :end_date) + GROUP BY invoices.company_id + ", ['company_currency2' => $this->company->settings->currency_id, 'company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); + } + + public function getInvoicesQuery($start_date, $end_date) { $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; @@ -204,6 +345,32 @@ trait ChartQueries ", ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); } + public function getAggregateOutstandingChartQuery($start_date, $end_date) + { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + + return DB::select(" + SELECT + sum(invoices.balance / IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(clients.settings, '$.currency_id')) AS SIGNED), :company_currency)) as total, + invoices.date + FROM clients + JOIN invoices + on invoices.client_id = clients.id + WHERE invoices.status_id IN (2,3,4) + AND invoices.company_id = :company_id + AND clients.is_deleted = 0 + AND invoices.is_deleted = 0 + {$user_filter} + AND (invoices.date BETWEEN :start_date AND :end_date) + GROUP BY invoices.company_id + ", [ + 'company_currency' => (int) $this->company->settings->currency_id, + 'company_id' => $this->company->id, + 'start_date' => $start_date, + 'end_date' => $end_date, + ]); + } + public function getOutstandingChartQuery($start_date, $end_date, $currency_id) { $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; @@ -234,6 +401,32 @@ trait ChartQueries } + public function getAggregateInvoiceChartQuery($start_date, $end_date) + { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + + return DB::select(" + SELECT + sum(invoices.amount / IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(clients.settings, '$.currency_id')) AS SIGNED), :company_currency)) as total, + invoices.date + FROM clients + JOIN invoices + on invoices.client_id = clients.id + WHERE invoices.company_id = :company_id + AND clients.is_deleted = 0 + AND invoices.is_deleted = 0 + {$user_filter} + AND invoices.status_id IN (2,3,4) + AND (invoices.date BETWEEN :start_date AND :end_date) + GROUP BY invoices.company_id + ", [ + 'company_currency' => (int) $this->company->settings->currency_id, + 'company_id' => $this->company->id, + 'start_date' => $start_date, + 'end_date' => $end_date, + ]); + } + public function getInvoiceChartQuery($start_date, $end_date, $currency_id) { $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; diff --git a/app/Services/Chart/ChartService.php b/app/Services/Chart/ChartService.php index 7bd2ed911a0b..7808be450991 100644 --- a/app/Services/Chart/ChartService.php +++ b/app/Services/Chart/ChartService.php @@ -88,6 +88,12 @@ class ChartService $data[$key]['expenses'] = $this->getExpenseChartQuery($start_date, $end_date, $key); } + + $data[999]['invoices'] = $this->getAggregateInvoiceChartQuery($start_date, $end_date); + $data[999]['outstanding'] = $this->getAggregateOutstandingChartQuery($start_date, $end_date); + $data[999]['payments'] = $this->getAggregatePaymentChartQuery($start_date, $end_date); + $data[999]['expenses'] = $this->getAggregateExpenseChartQuery($start_date, $end_date); + return $data; } @@ -123,6 +129,17 @@ class ChartService } + $aggregate_revenue = $this->getAggregateRevenueQuery($start_date, $end_date); + $aggregate_outstanding = $this->getAggregateOutstandingQuery($start_date, $end_date); + $aggregate_expenses = $this->getAggregateExpenseQuery($start_date, $end_date); + $aggregate_invoices = $this->getAggregateInvoicesQuery($start_date, $end_date); + + $data[999]['invoices'] = $aggregate_invoices !== false ? $aggregate_invoices : new \stdClass(); + $data[999]['expense'] = $aggregate_expenses !== false ? $aggregate_expenses : new \stdClass(); + $data[999]['outstanding'] = $aggregate_outstanding !== false ? $aggregate_outstanding : new \stdClass(); + $data[999]['revenue'] = $aggregate_revenue !== false ? $aggregate_revenue : new \stdClass(); + + return $data; } diff --git a/app/Services/Invoice/UpdateReminder.php b/app/Services/Invoice/UpdateReminder.php index 2d861632d2f8..2a9791ba7ed3 100644 --- a/app/Services/Invoice/UpdateReminder.php +++ b/app/Services/Invoice/UpdateReminder.php @@ -19,7 +19,7 @@ use Carbon\Carbon; class UpdateReminder extends AbstractService { - public function __construct(public Invoice | Quote $invoice, public mixed $settings = null) + public function __construct(public Invoice $invoice, public mixed $settings = null) { } @@ -47,7 +47,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder1_sent) && $this->settings->schedule_reminder1 == 'after_invoice_date') { - $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder1); + $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1); if ($reminder_date->gt(now())) { $date_collection->push($reminder_date); @@ -58,7 +58,7 @@ class UpdateReminder extends AbstractService ($this->invoice->partial_due_date || $this->invoice->due_date) && $this->settings->schedule_reminder1 == 'before_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder1); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder1); // nlog("1. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -71,7 +71,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder1 == 'after_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder1); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1); // nlog("2. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -81,7 +81,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder2_sent) && $this->settings->schedule_reminder2 == 'after_invoice_date') { - $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder2); + $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2); if ($reminder_date->gt(now())) { $date_collection->push($reminder_date); @@ -93,7 +93,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder2 == 'before_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder2); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder2); // nlog("3. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -106,7 +106,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder2 == 'after_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder2); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2); // nlog("4. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -116,7 +116,7 @@ class UpdateReminder extends AbstractService if (is_null($this->invoice->reminder3_sent) && $this->settings->schedule_reminder3 == 'after_invoice_date') { - $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays($this->settings->num_days_reminder3); + $reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3); if ($reminder_date->gt(now())) { $date_collection->push($reminder_date); @@ -128,7 +128,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder3 == 'before_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder3); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder3); // nlog("5. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { @@ -141,7 +141,7 @@ class UpdateReminder extends AbstractService $this->settings->schedule_reminder3 == 'after_due_date') { $partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date; - $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder3); + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3); // nlog("6. {$reminder_date->format('Y-m-d')}"); if ($reminder_date->gt(now())) { diff --git a/app/Services/Quote/MarkSent.php b/app/Services/Quote/MarkSent.php index bcdeace0d51e..0dc3c084793c 100644 --- a/app/Services/Quote/MarkSent.php +++ b/app/Services/Quote/MarkSent.php @@ -35,7 +35,7 @@ class MarkSent if ($this->quote->due_date != '' || $this->client->getSetting('valid_until') == '') { } else { - $this->quote->due_date = Carbon::parse($this->quote->date)->addDays($this->client->getSetting('valid_until')); + $this->quote->due_date = Carbon::parse($this->quote->date)->addDays((int)$this->client->getSetting('valid_until')); } $this->quote diff --git a/app/Services/Quote/QuoteService.php b/app/Services/Quote/QuoteService.php index cdc1b1dccd6c..ce1a5d5da01b 100644 --- a/app/Services/Quote/QuoteService.php +++ b/app/Services/Quote/QuoteService.php @@ -17,7 +17,7 @@ use App\Jobs\EDocument\CreateEDocument; use App\Models\Project; use App\Models\Quote; use App\Repositories\QuoteRepository; -use App\Services\Invoice\UpdateReminder; +use App\Services\Quote\UpdateReminder; use App\Utils\Ninja; use App\Utils\Traits\MakesHash; use Illuminate\Support\Facades\Storage; @@ -266,6 +266,41 @@ class QuoteService return $this; } + + /*When a reminder is sent we want to touch the dates they were sent*/ + public function touchReminder(string $reminder_template) + { + nrlog(now()->format('Y-m-d h:i:s') . " INV #{$this->quote->number} : Touching Reminder => {$reminder_template}"); + switch ($reminder_template) { + case 'reminder1': + $this->quote->reminder1_sent = now(); + $this->quote->reminder_last_sent = now(); + $this->quote->last_sent_date = now(); + break; + case 'reminder2': + $this->quote->reminder2_sent = now(); + $this->quote->reminder_last_sent = now(); + $this->quote->last_sent_date = now(); + break; + case 'reminder3': + $this->quote->reminder3_sent = now(); + $this->quote->reminder_last_sent = now(); + $this->quote->last_sent_date = now(); + break; + case 'endless_reminder': + $this->quote->reminder_last_sent = now(); + $this->invoice->last_sent_date = now(); + break; + default: + $this->quote->reminder1_sent = now(); + $this->quote->reminder_last_sent = now(); + $this->quote->last_sent_date = now(); + break; + } + + return $this; + } + /** * Saves the quote. * @return Quote|null diff --git a/app/Services/Quote/UpdateReminder.php b/app/Services/Quote/UpdateReminder.php new file mode 100644 index 000000000000..9cf39da12871 --- /dev/null +++ b/app/Services/Quote/UpdateReminder.php @@ -0,0 +1,209 @@ +settings) { + $this->settings = $this->quote->client->getMergedSettings(); + } + + if (!$this->quote->canRemind()) { + $this->quote->next_send_date = null; + $this->quote->saveQuietly(); + + return $this->quote; //exit early + } + + if ($this->quote->next_send_date) { + $this->quote->next_send_date = null; + } + + $offset = $this->quote->client->timezone_offset(); + + $date_collection = collect(); + + if (is_null($this->quote->reminder1_sent) && + $this->settings->quote_schedule_reminder1 == 'after_quote_date') { + $reminder_date = Carbon::parse($this->quote->date)->startOfDay()->addDays((int)$this->settings->quote_num_days_reminder1); + + if ($reminder_date->gt(now())) { + $date_collection->push($reminder_date); + } + } + + if (is_null($this->quote->reminder1_sent) && + ($this->quote->partial_due_date || $this->quote->due_date) && + $this->settings->quote_schedule_reminder1 == 'before_valid_until_date') { + $partial_or_due_date = ($this->quote->partial > 0 && isset($this->quote->partial_due_date)) ? $this->quote->partial_due_date : $this->quote->due_date; + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->quote_num_days_reminder1); + // nlog("1. {$reminder_date->format('Y-m-d')}"); + + if ($reminder_date->gt(now())) { + $date_collection->push($reminder_date); + } + } + + if (is_null($this->quote->reminder1_sent) && + ($this->quote->partial_due_date || $this->quote->due_date) && + $this->settings->quote_schedule_reminder1 == 'after_valid_until_date') { + + $partial_or_due_date = ($this->quote->partial > 0 && isset($this->quote->partial_due_date)) ? $this->quote->partial_due_date : $this->quote->due_date; + $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->quote_num_days_reminder1); + // nlog("2. {$reminder_date->format('Y-m-d')}"); + + if ($reminder_date->gt(now())) { + $date_collection->push($reminder_date); + } + } + + // if (is_null($this->quote->reminder2_sent) && + // $this->settings->schedule_reminder2 == 'after_valid_until_date') { + // $reminder_date = Carbon::parse($this->quote->date)->startOfDay()->addDays($this->settings->num_days_reminder2); + + // if ($reminder_date->gt(now())) { + // $date_collection->push($reminder_date); + // } + // } + + // if (is_null($this->quote->reminder2_sent) && + // ($this->quote->partial_due_date || $this->quote->due_date) && + // $this->settings->schedule_reminder2 == 'before_valid_until_date') { + + // $partial_or_due_date = ($this->quote->partial > 0 && isset($this->quote->partial_due_date)) ? $this->quote->partial_due_date : $this->quote->due_date; + // $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder2); + // // nlog("3. {$reminder_date->format('Y-m-d')}"); + + // if ($reminder_date->gt(now())) { + // $date_collection->push($reminder_date); + // } + // } + + // if (is_null($this->quote->reminder2_sent) && + // ($this->quote->partial_due_date || $this->quote->due_date) && + // $this->settings->schedule_reminder2 == 'after_valid_until_date') { + + // $partial_or_due_date = ($this->quote->partial > 0 && isset($this->quote->partial_due_date)) ? $this->quote->partial_due_date : $this->quote->due_date; + // $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder2); + // // nlog("4. {$reminder_date->format('Y-m-d')}"); + + // if ($reminder_date->gt(now())) { + // $date_collection->push($reminder_date); + // } + // } + + // if (is_null($this->quote->reminder3_sent) && + // $this->settings->schedule_reminder3 == 'after_valid_until_date') { + // $reminder_date = Carbon::parse($this->quote->date)->startOfDay()->addDays($this->settings->num_days_reminder3); + + // if ($reminder_date->gt(now())) { + // $date_collection->push($reminder_date); + // } + // } + + // if (is_null($this->quote->reminder3_sent) && + // ($this->quote->partial_due_date || $this->quote->due_date) && + // $this->settings->schedule_reminder3 == 'before_valid_until_date') { + + // $partial_or_due_date = ($this->quote->partial > 0 && isset($this->quote->partial_due_date)) ? $this->quote->partial_due_date : $this->quote->due_date; + // $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays($this->settings->num_days_reminder3); + // // nlog("5. {$reminder_date->format('Y-m-d')}"); + + // if ($reminder_date->gt(now())) { + // $date_collection->push($reminder_date); + // } + // } + + // if (is_null($this->quote->reminder3_sent) && + // ($this->quote->partial_due_date || $this->quote->due_date) && + // $this->settings->schedule_reminder3 == 'after_valid_until_date') { + + // $partial_or_due_date = ($this->quote->partial > 0 && isset($this->quote->partial_due_date)) ? $this->quote->partial_due_date : $this->quote->due_date; + // $reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays($this->settings->num_days_reminder3); + // // nlog("6. {$reminder_date->format('Y-m-d')}"); + + // if ($reminder_date->gt(now())) { + // $date_collection->push($reminder_date); + // } + // } + + // if ($this->quote->last_sent_date && + // $this->settings->enable_reminder_endless && + // ($this->quote->reminder1_sent || $this->settings->schedule_reminder1 == "" || !$this->settings->enable_reminder1) && + // ($this->quote->reminder2_sent || $this->settings->schedule_reminder2 == "" || !$this->settings->enable_reminder2) && + // ($this->quote->reminder3_sent || $this->settings->schedule_reminder3 == "" || !$this->settings->enable_reminder3)) { + // $reminder_date = $this->addTimeInterval($this->quote->last_sent_date, (int) $this->settings->endless_reminder_frequency_id); + + // if ($reminder_date) { + // if ($reminder_date->gt(now())) { + // $date_collection->push($reminder_date); + // } + // } + // } + + if ($date_collection->count() >= 1 && $date_collection->sort()->first()->gte(now())) { + $this->quote->next_send_date = $date_collection->sort()->first()->addSeconds($offset); + } else { + $this->quote->next_send_date = null; + } + + return $this->quote; + } + + private function addTimeInterval($date, $endless_reminder_frequency_id): ?Carbon + { + if (! $date) { + return null; + } + + switch ($endless_reminder_frequency_id) { + case RecurringInvoice::FREQUENCY_DAILY: + return Carbon::parse($date)->addDay()->startOfDay(); + case RecurringInvoice::FREQUENCY_WEEKLY: + return Carbon::parse($date)->addWeek()->startOfDay(); + case RecurringInvoice::FREQUENCY_TWO_WEEKS: + return Carbon::parse($date)->addWeeks(2)->startOfDay(); + case RecurringInvoice::FREQUENCY_FOUR_WEEKS: + return Carbon::parse($date)->addWeeks(4)->startOfDay(); + case RecurringInvoice::FREQUENCY_MONTHLY: + return Carbon::parse($date)->addMonthNoOverflow()->startOfDay(); + case RecurringInvoice::FREQUENCY_TWO_MONTHS: + return Carbon::parse($date)->addMonthsNoOverflow(2)->startOfDay(); + case RecurringInvoice::FREQUENCY_THREE_MONTHS: + return Carbon::parse($date)->addMonthsNoOverflow(3)->startOfDay(); + case RecurringInvoice::FREQUENCY_FOUR_MONTHS: + return Carbon::parse($date)->addMonthsNoOverflow(4)->startOfDay(); + case RecurringInvoice::FREQUENCY_SIX_MONTHS: + return Carbon::parse($date)->addMonthsNoOverflow(6)->startOfDay(); + case RecurringInvoice::FREQUENCY_ANNUALLY: + return Carbon::parse($date)->addYear()->startOfDay(); + case RecurringInvoice::FREQUENCY_TWO_YEARS: + return Carbon::parse($date)->addYears(2)->startOfDay(); + case RecurringInvoice::FREQUENCY_THREE_YEARS: + return Carbon::parse($date)->addYears(3)->startOfDay(); + default: + return null; + } + } +} diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php index 5ad95ba7cb9b..35b76ff0622f 100644 --- a/app/Transformers/UserTransformer.php +++ b/app/Transformers/UserTransformer.php @@ -40,6 +40,11 @@ class UserTransformer extends EntityTransformer public function transform(User $user) { + $ref = new \stdClass; + $ref->free = 0; + $ref->pro = 0; + $ref->enterprise = 0; + return [ 'id' => $this->encodePrimaryKey($user->id), 'first_name' => $user->first_name ?: '', @@ -66,7 +71,7 @@ class UserTransformer extends EntityTransformer 'language_id' => (string) $user->language_id ?: '', 'user_logged_in_notification' => (bool) $user->user_logged_in_notification, 'referral_code' => (string) $user->referral_code, - 'referral_meta' => $user->referral_meta ? (object)$user->referral_meta : new \stdClass, + 'referral_meta' => $user->referral_meta ? (object)$user->referral_meta : $ref, ]; } diff --git a/app/Utils/Traits/MakesReminders.php b/app/Utils/Traits/MakesReminders.php index e6e6460abaed..232c15525d8d 100644 --- a/app/Utils/Traits/MakesReminders.php +++ b/app/Utils/Traits/MakesReminders.php @@ -32,13 +32,13 @@ trait MakesReminders switch ($schedule_reminder) { case 'after_invoice_date': - return Carbon::parse($this->date)->addDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now()); + return Carbon::parse($this->date)->addDays((int)$num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now()); case 'before_due_date': $partial_or_due_date = ($this->partial > 0 && isset($this->partial_due_date)) ? $this->partial_due_date : $this->due_date; - return Carbon::parse($partial_or_due_date)->subDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now()); + return Carbon::parse($partial_or_due_date)->subDays((int)$num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now()); case 'after_due_date': $partial_or_due_date = ($this->partial > 0 && isset($this->partial_due_date)) ? $this->partial_due_date : $this->due_date; - return Carbon::parse($partial_or_due_date)->addDays($num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now()); + return Carbon::parse($partial_or_due_date)->addDays((int)$num_days_reminder)->startOfDay()->addSeconds($offset)->isSameDay(Carbon::now()); default: return null; } diff --git a/config/ninja.php b/config/ninja.php index 0fbc5f9ee1b6..cf16e2781d18 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.10.0'), - 'app_tag' => env('APP_TAG', '5.10.0'), + 'app_version' => env('APP_VERSION', '5.10.1'), + 'app_tag' => env('APP_TAG', '5.10.1'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), diff --git a/phpstan.neon b/phpstan.neon index 718f0385499c..5066456a20bb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -30,4 +30,5 @@ parameters: - '#Socialite#' - '#Access to protected property#' - '#Call to undefined method .*#' - - '#Argument of an invalid type stdClass supplied for foreach, only iterables are supported.#' \ No newline at end of file + - '#Argument of an invalid type stdClass supplied for foreach, only iterables are supported.#' + - '#Comparison operation ">=" between int<1, max> and 1 is always true#' \ No newline at end of file diff --git a/tests/Feature/QuoteReminderTest.php b/tests/Feature/QuoteReminderTest.php new file mode 100644 index 000000000000..d5d1837fcb3f --- /dev/null +++ b/tests/Feature/QuoteReminderTest.php @@ -0,0 +1,218 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + public $company; + + public $user; + + public $payload; + + public $account; + + public $client; + + public $token; + + public $cu; + + public $invoice; + + private function buildData($settings = null) + { + $this->account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000, + ]); + + $this->account->num_users = 3; + $this->account->save(); + + $this->user = User::factory()->create([ + 'account_id' => $this->account->id, + 'confirmation_code' => 'xyz123', + 'email' => $this->faker->unique()->safeEmail(), + ]); + + if(!$settings) { + $settings = CompanySettings::defaults(); + $settings->client_online_payment_notification = false; + $settings->client_manual_payment_notification = false; + } + + $this->company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $this->company->settings = $settings; + $this->company->save(); + + $this->cu = CompanyUserFactory::create($this->user->id, $this->company->id, $this->account->id); + $this->cu->is_owner = true; + $this->cu->is_admin = true; + $this->cu->is_locked = false; + $this->cu->save(); + + $this->token = \Illuminate\Support\Str::random(64); + + $company_token = new CompanyToken; + $company_token->user_id = $this->user->id; + $company_token->company_id = $this->company->id; + $company_token->account_id = $this->account->id; + $company_token->name = 'test token'; + $company_token->token = $this->token; + $company_token->is_system = true; + + $company_token->save(); + + $this->client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + 'name' => 'bob', + 'address1' => '1234', + 'balance' => 100, + 'paid_to_date' => 50, + ]); + + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'first_name' => 'john', + 'last_name' => 'doe', + 'email' => 'john@doe.com', + 'send_email' => true, + ]); + + $this->quote = Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'date' => now()->addSeconds($this->client->timezone_offset())->format('Y-m-d'), + 'next_send_date' => null, + 'due_date' => Carbon::now()->addSeconds($this->client->timezone_offset())->addDays(5)->format('Y-m-d'), + 'last_sent_date' => now()->addSeconds($this->client->timezone_offset()), + 'reminder_last_sent' => null, + 'status_id' => 2, + 'amount' => 10, + 'balance' => 10, + ]); + + } + + + public function testNullReminder() + { + + $settings = $this->company->settings; + $settings->enable_quote_reminder1 = false; + $settings->quote_schedule_reminder1 = ''; + $settings->quote_num_days_reminder1 = 1; + + $this->buildData(($settings)); + + $this->quote->date = now()->subMonths(2)->format('Y-m-d'); + $this->quote->due_date = now()->subMonth()->format('Y-m-d'); + $this->quote->last_sent_date = now(); + $this->quote->next_send_date = null; + + $this->quote->service()->setReminder($settings)->save(); + + $this->quote = $this->quote->fresh(); + + $this->assertNull($this->quote->next_send_date); + + } + + public function testBeforeValidReminder() + { + + $settings = $this->company->settings; + $settings->enable_quote_reminder1 = true; + $settings->quote_schedule_reminder1 = 'before_valid_until_date'; + $settings->quote_num_days_reminder1 = 1; + + $this->buildData(($settings)); + + $this->quote->date = now()->addMonth()->format('Y-m-d'); + $this->quote->partial_due_date = null; + $this->quote->due_date = now()->addMonths(2)->format('Y-m-d'); + $this->quote->last_sent_date = null; + $this->quote->next_send_date = null; + $this->quote->save(); + + + $this->assertTrue($this->quote->canRemind()); + + $this->quote->service()->setReminder($settings)->save(); + + $this->quote = $this->quote->fresh(); + + $this->assertNotNull($this->quote->next_send_date); + + nlog($this->quote->next_send_date); + $this->assertEquals(now()->addMonths(2)->subDay()->format('Y-m-d'), \Carbon\Carbon::parse($this->quote->next_send_date)->addSeconds($this->quote->client->timezone_offset())->format('Y-m-d')); + + } + + +}