'float', 'send_automatically' => 'boolean', ]; } /** * Invoices this schedule has produced so far. */ public function invoices(): HasMany { return $this->hasMany(Invoice::class, 'recurring_invoice_id'); } /** * Taxes carried by the template, at document level. */ public function taxes(): HasMany { return $this->hasMany(Tax::class, 'recurring_invoice_id'); } /** * Line items the generated invoices are built from. */ public function items(): HasMany { return $this->hasMany(InvoiceItem::class, 'recurring_invoice_id'); } /** * Contact every generated invoice is billed to. */ public function customer(): BelongsTo { return $this->belongsTo(Customer::class, 'customer_id'); } /** * Company the schedule was set up under. */ public function company(): BelongsTo { return $this->belongsTo(Company::class, 'company_id'); } /** * Author of the schedule, linked through the creator_id column. */ public function creator(): BelongsTo { return $this->belongsTo(User::class, 'creator_id'); } /** * Currency the template amounts are stated in. */ public function currency(): BelongsTo { return $this->belongsTo(Currency::class, 'currency_id'); } /** * Start of the schedule, written in the company's date format and in the * language the application is running in. */ public function getFormattedStartsAtAttribute() { return Carbon::parse($this->starts_at)->translatedFormat($this->companyDateFormat()); } /** * The moment the next invoice is due, written in the company's date format * and in the language the application is running in. */ public function getFormattedNextInvoiceAtAttribute() { return Carbon::parse($this->next_invoice_at)->translatedFormat($this->companyDateFormat()); } /** * End date of a date-limited schedule, written in the company's date * format. Unlike the two above it is not translated. */ public function getFormattedLimitDateAttribute() { return Carbon::parse($this->limit_date)->format($this->companyDateFormat()); } /** * Creation date, written in the company's date format and untranslated. */ public function getFormattedCreatedAtAttribute() { return Carbon::parse($this->created_at)->format($this->companyDateFormat()); } /** * Narrow to the company the current request is acting on. */ public function scopeWhereCompany($query) { $company = request()->header('company'); return $query->where($this->qualifyColumn('company_id'), $company); } /** * Return the whole result set for the sentinel limit "all", otherwise a * page of the requested size. */ public function scopePaginateData($query, $limit) { return $limit == 'all' ? $query->get() : $query->paginate($limit); } /** * Sort by a caller-supplied column, sanitised before it reaches SQL. */ public function scopeWhereOrder($query, $orderByField, $orderBy) { return SafeOrderBy::apply($query, $orderByField, $orderBy); } /** * Keep only schedules sitting in one lifecycle state. */ public function scopeWhereStatus($query, $status) { return $query->where($this->qualifyColumn('status'), $status); } /** * Keep only the schedules billed to one contact. */ public function scopeWhereCustomer($query, $customer_id) { return $query->where('customer_id', $customer_id); } /** * Keep only schedules whose start date falls inside the inclusive range. */ public function scopeRecurringInvoicesStartBetween($query, $start, $end) { return $query->whereBetween('starts_at', [ $start->format('Y-m-d'), $end->format('Y-m-d'), ]); } /** * Keep only schedules whose contact matches every whitespace-separated * term, a term counting as matched when it appears in the contact's name, * the contact person or the company name. */ public function scopeWhereSearch($query, $search) { $terms = explode(' ', $search); foreach ($terms as $term) { $query->whereHas('customer', function ($customer) use ($term) { $needle = '%'.$term.'%'; $customer->where('name', 'LIKE', $needle) ->orWhere('contact_name', 'LIKE', $needle) ->orWhere('company_name', 'LIKE', $needle); }); } } /** * Run every listed filter that carries a value. */ public function scopeApplyFilters($query, array $filters) { $status = $filters['status'] ?? null; $search = $filters['search'] ?? null; $from = $filters['from_date'] ?? null; $to = $filters['to_date'] ?? null; $customer = $filters['customer_id'] ?? null; if ($status && $status !== 'ALL') { $query->whereStatus($status); } if ($search) { $query->whereSearch($search); } if ($from && $to) { $query->recurringInvoicesStartBetween( Carbon::createFromFormat('Y-m-d', $from), Carbon::createFromFormat('Y-m-d', $to) ); } if ($customer) { $query->whereCustomer($customer); } $sortField = $filters['orderByField'] ?? null; $sortDirection = $filters['orderBy'] ?? null; if ($sortField || $sortDirection) { $query->whereOrder($sortField ?: 'created_at', $sortDirection ?: 'asc'); } } /** * Retire the schedule, so that no further invoice is generated from it. */ public function markStatusAsCompleted(): void { $this->status = static::COMPLETED; $this->save(); } /** * The moment a cron expression next fires, counted from the given start * date rather than from now, in the application's own time zone. */ public static function getNextInvoiceDate(string $frequency, string $starts_at): string { $schedule = new CronExpression($frequency); $zone = config('app.timezone', 'UTC'); return $schedule->getNextRunDate($starts_at, 0, false, $zone)->format('Y-m-d H:i:s'); } /** * Recompute and store the date the next invoice falls due. */ public function updateNextInvoiceDate(): void { $this->next_invoice_at = self::getNextInvoiceDate($this->frequency, $this->starts_at); $this->save(); } /** * The date format the owning company writes dates in. */ private function companyDateFormat() { return CompanySetting::getSetting('carbon_date_format', $this->company_id); } }