hasMany(Expense::class, 'expense_category_id'); } /** * Company the heading belongs to. */ public function company(): BelongsTo { return $this->belongsTo(Company::class, 'company_id'); } /** * Creation date in the company's configured date format. * * Unlike the expense's own dates this one is not translated -- month and * day names come out in English whatever language the application is * running in. */ public function getFormattedCreatedAtAttribute(mixed $value): string { $format = CompanySetting::getSetting('carbon_date_format', $this->company_id); return Carbon::parse($this->created_at)->format($format); } /** * Everything spent under this heading, summed on the spot. * * The figure is the raw `amount` column, so an expense entered in a * foreign currency contributes its face value rather than its worth in the * company's books, and headings mixing currencies add up to a number that * is in no currency at all. */ public function getAmountAttribute(): float { return $this->expenses()->sum('amount'); } /** * Narrow to the company the current request is acting on. * * The company is read from the request header and never from an argument, * so a caller passing one is quietly ignored. */ public function scopeWhereCompany(Builder $query): void { $company = request()->header('company'); $query->where('company_id', $company); } /** * Widen a listing to also take in one specific heading. */ public function scopeWhereCategory(Builder $query, int $category_id): void { $query->orWhere('id', $category_id); } /** * Partial match on the heading name. */ public function scopeWhereSearch(Builder $query, string $search): void { $needle = '%'.$search.'%'; $query->where('name', 'LIKE', $needle); } /** * Run every listed filter that carries a value. * * Order is load-bearing: the heading filter contributes an OR, which makes * whatever follows it part of that alternative. A filter sent as an empty * string, a zero or a null counts as not sent at all. Note that the * company filter only decides *whether* to scope by company -- which * company is taken from the request header, not from the value sent. */ public function scopeApplyFilters(Builder $query, array $filters): void { $clauses = [ 'category_id' => fn ($wanted) => $query->whereCategory($wanted), 'company_id' => fn ($wanted) => $query->whereCompany($wanted), 'search' => fn ($wanted) => $query->whereSearch($wanted), ]; foreach ($clauses as $filter => $clause) { $wanted = $filters[$filter] ?? null; if ($wanted) { $clause($wanted); } } } /** * Return the whole result set for the sentinel limit "all", otherwise a * page of the requested size. * * @return Collection|LengthAwarePaginator */ public function scopePaginateData(Builder $query, string $limit) { return $limit == 'all' ? $query->get() : $query->paginate($limit); } }