diff --git a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php index 9da24cf99b2a..375b076aee24 100644 --- a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php @@ -12,9 +12,12 @@ namespace App\Http\Requests\RecurringQuote; use App\Http\Requests\Request; +use App\Http\ValidationRules\Recurring\UniqueRecurringQuoteNumberRule; +use App\Models\Client; use App\Models\RecurringQuote; use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\MakesHash; +use Illuminate\Http\UploadedFile; class StoreRecurringQuoteRequest extends Request { @@ -33,17 +36,39 @@ class StoreRecurringQuoteRequest extends Request public function rules() { - return [ - 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', - 'client_id' => 'required|exists:clients,id,company_id,'.auth()->user()->company()->id, - ]; + $rules = []; + + if ($this->input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.'.$index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; + + $rules['invitations.*.client_contact_id'] = 'distinct'; + + $rules['frequency_id'] = 'required|integer|digits_between:1,12'; + + $rules['number'] = new UniqueRecurringQuoteNumberRule($this->all()); + + return $rules; } protected function prepareForValidation() { $input = $this->all(); - if ($input['client_id']) { + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { $input['client_id'] = $this->decodePrimaryKey($input['client_id']); } @@ -51,8 +76,56 @@ class StoreRecurringQuoteRequest extends Request $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } + if (isset($input['client_contacts'])) { + foreach ($input['client_contacts'] as $key => $contact) { + if (! array_key_exists('send_email', $contact) || ! array_key_exists('id', $contact)) { + unset($input['client_contacts'][$key]); + } + } + } + + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + } + + if (isset($input['invitations'][$key]['id']) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; - //$input['line_items'] = json_encode($input['line_items']); + + if (isset($input['auto_bill'])) { + $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); + } else { + if ($client = Client::find($input['client_id'])) { + $input['auto_bill'] = $client->getSetting('auto_bill'); + $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); + } + } + $this->replace($input); } + + private function setAutoBillFlag($auto_bill) + { + if ($auto_bill == 'always' || $auto_bill == 'optout') { + return true; + } + + return false; + + } + + public function messages() + { + return []; + } } diff --git a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php index 4a8cc51d87d1..7278d8d1b246 100644 --- a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php @@ -14,12 +14,15 @@ namespace App\Http\Requests\RecurringQuote; use App\Http\Requests\Request; use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\CleanLineItems; +use App\Utils\Traits\MakesHash; +use Illuminate\Http\UploadedFile; use Illuminate\Validation\Rule; class UpdateRecurringQuoteRequest extends Request { use ChecksEntityStatus; use CleanLineItems; + use MakesHash; /** * Determine if the user is authorized to make this request. @@ -33,24 +36,91 @@ class UpdateRecurringQuoteRequest extends Request public function rules() { - return [ - 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', - ]; + $rules = []; + + if ($this->input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.'.$index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + if($this->number) + $rules['number'] = Rule::unique('recurring_quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_quote->id); + + + return $rules; } protected function prepareForValidation() { $input = $this->all(); + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (isset($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } - $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + } - if($this->number) - $rules['number'] = Rule::unique('recurring_quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_quote->id); + if (array_key_exists('id', $input['invitations'][$key]) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + + if (isset($input['line_items'])) { + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + } + + if (isset($input['auto_bill'])) { + $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); + } + + if (array_key_exists('documents', $input)) { + unset($input['documents']); + } + $this->replace($input); } + + /** + * if($auto_bill == '') + * off / optin / optout will reset the status of this field to off to allow + * the client to choose whether to auto_bill or not. + * + * @param enum $auto_bill off/always/optin/optout + * + * @return bool + */ + private function setAutoBillFlag($auto_bill) :bool + { + if ($auto_bill == 'always') { + return true; + } + + // if($auto_bill == '') + // off / optin / optout will reset the status of this field to off to allow + // the client to choose whether to auto_bill or not. + + return false; + } } diff --git a/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php new file mode 100644 index 000000000000..72985f6f48e7 --- /dev/null +++ b/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->recurring_quote); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Http/ValidationRules/Recurring/UniqueRecurringQuoteNumberRule.php b/app/Http/ValidationRules/Recurring/UniqueRecurringQuoteNumberRule.php new file mode 100644 index 000000000000..66fca8061f3f --- /dev/null +++ b/app/Http/ValidationRules/Recurring/UniqueRecurringQuoteNumberRule.php @@ -0,0 +1,67 @@ +input = $input; + } + + /** + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return $this->checkIfQuoteNumberUnique(); //if it exists, return false! + } + + /** + * @return string + */ + public function message() + { + return ctrans('texts.recurring_quote_number_taken', ['number' => $this->input['number']]); + } + + /** + * @return bool + */ + private function checkIfQuoteNumberUnique() : bool + { + if (empty($this->input['number'])) { + return true; + } + + $invoice = RecurringQuote::where('client_id', $this->input['client_id']) + ->where('number', $this->input['number']) + ->withTrashed() + ->exists(); + + if ($invoice) { + return false; + } + + return true; + } +} diff --git a/app/Models/Presenters/RecurringQuotePresenter.php b/app/Models/Presenters/RecurringQuotePresenter.php new file mode 100644 index 000000000000..e69058ce8f5d --- /dev/null +++ b/app/Models/Presenters/RecurringQuotePresenter.php @@ -0,0 +1,31 @@ + 'object', 'line_items' => 'object', 'backup' => 'object', - 'settings' => 'object', 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', ]; - protected $with = [ - // 'client', - // 'company', + protected $appends = [ + 'hashed_id', + 'status', ]; + protected $touches = []; + public function getEntityType() { return self::class; @@ -126,6 +159,16 @@ class RecurringQuote extends BaseModel return $value; } + public function activities() + { + return $this->hasMany(Activity::class)->orderBy('id', 'DESC')->take(50); + } + + public function history() + { + return $this->hasManyThrough(Backup::class, Activity::class); + } + public function company() { return $this->belongsTo(Company::class); @@ -136,6 +179,11 @@ class RecurringQuote extends BaseModel return $this->belongsTo(Client::class)->withTrashed(); } + public function project() + { + return $this->belongsTo(Project::class)->withTrashed(); + } + public function user() { return $this->belongsTo(User::class)->withTrashed(); @@ -146,8 +194,299 @@ class RecurringQuote extends BaseModel return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed(); } + public function quotes() + { + return $this->hasMany(Quote::class, 'recurring_id', 'id')->withTrashed(); + } + public function invitations() { - $this->morphMany(RecurringQuoteInvitation::class); + return $this->hasMany(RecurringQuoteInvitation::class); } + + public function documents() + { + return $this->morphMany(Document::class, 'documentable'); + } + + public function getStatusAttribute() + { + if ($this->status_id == self::STATUS_ACTIVE && Carbon::parse($this->next_send_date)->isFuture()) { + return self::STATUS_PENDING; + } else { + return $this->status_id; + } + } + + public function nextSendDate() :?Carbon + { + if (!$this->next_send_date) { + return null; + } + + $offset = $this->client->timezone_offset(); + + /* + As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need + to add ON a day - a day = 86400 seconds + */ + if($offset < 0) + $offset += 86400; + + switch ($this->frequency_id) { + case self::FREQUENCY_DAILY: + return Carbon::parse($this->next_send_date)->startOfDay()->addDay()->addSeconds($offset); + case self::FREQUENCY_WEEKLY: + return Carbon::parse($this->next_send_date)->startOfDay()->addWeek()->addSeconds($offset); + case self::FREQUENCY_TWO_WEEKS: + return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(2)->addSeconds($offset); + case self::FREQUENCY_FOUR_WEEKS: + return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(4)->addSeconds($offset); + case self::FREQUENCY_MONTHLY: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset); + case self::FREQUENCY_TWO_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset); + case self::FREQUENCY_THREE_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset); + case self::FREQUENCY_FOUR_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset); + case self::FREQUENCY_SIX_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(6)->addSeconds($offset); + case self::FREQUENCY_ANNUALLY: + return Carbon::parse($this->next_send_date)->startOfDay()->addYear()->addSeconds($offset); + case self::FREQUENCY_TWO_YEARS: + return Carbon::parse($this->next_send_date)->startOfDay()->addYears(2)->addSeconds($offset); + case self::FREQUENCY_THREE_YEARS: + return Carbon::parse($this->next_send_date)->startOfDay()->addYears(3)->addSeconds($offset); + default: + return null; + } + } + + public function nextDateByFrequency($date) + { + $offset = $this->client->timezone_offset(); + + switch ($this->frequency_id) { + case self::FREQUENCY_DAILY: + return Carbon::parse($date)->startOfDay()->addDay()->addSeconds($offset); + case self::FREQUENCY_WEEKLY: + return Carbon::parse($date)->startOfDay()->addWeek()->addSeconds($offset); + case self::FREQUENCY_TWO_WEEKS: + return Carbon::parse($date)->startOfDay()->addWeeks(2)->addSeconds($offset); + case self::FREQUENCY_FOUR_WEEKS: + return Carbon::parse($date)->startOfDay()->addWeeks(4)->addSeconds($offset); + case self::FREQUENCY_MONTHLY: + return Carbon::parse($date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset); + case self::FREQUENCY_TWO_MONTHS: + return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset); + case self::FREQUENCY_THREE_MONTHS: + return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset); + case self::FREQUENCY_FOUR_MONTHS: + return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset); + case self::FREQUENCY_SIX_MONTHS: + return Carbon::parse($date)->addMonthsNoOverflow(6)->addSeconds($offset); + case self::FREQUENCY_ANNUALLY: + return Carbon::parse($date)->startOfDay()->addYear()->addSeconds($offset); + case self::FREQUENCY_TWO_YEARS: + return Carbon::parse($date)->startOfDay()->addYears(2)->addSeconds($offset); + case self::FREQUENCY_THREE_YEARS: + return Carbon::parse($date)->startOfDay()->addYears(3)->addSeconds($offset); + default: + return null; + } + } + + public function remainingCycles() : int + { + if ($this->remaining_cycles == 0) { + return 0; + } elseif ($this->remaining_cycles == -1) { + return -1; + } else { + return $this->remaining_cycles - 1; + } + } + + public function setCompleted() : void + { + $this->status_id = self::STATUS_COMPLETED; + $this->next_send_date = null; + $this->remaining_cycles = 0; + $this->save(); + } + + public static function badgeForStatus(int $status) + { + switch ($status) { + case self::STATUS_DRAFT: + return '