Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jazepsbumbiers's avatar

Laravel query not returning correct values

Hi. I am building an web app using Laravel and Vue. I have a view with a table showing expenses by category. However filter is not working.

 async getCategories() {
                this.loading = true;

                const { response } = await this.request('GET', '/get-other-categories', { params: { userId: this.activeUser.id, ...this.filters } });

                this.categories = response.data;
                this.expenses = this.categories.reduce((sum, item) => sum + item.total_expenses, 0);
                
                this.loading = false;
            },
            async getIncomeCategory() {
                this.loading = true;

                const { response } = await this.request('GET', '/get-income-category', { params: { userId: this.activeUser.id, ...this.filters } });

                this.incomeCategories = response.data;
                this.income = this.incomeCategories.reduce((sum, item) => sum + item.total_income, 0);

                this.loading = false;
            },
filterRecordsByAccounts(accounts) {
                const selectedAccounts = [...new Set(accounts)];

                if (!selectedAccounts.length) {
                    this.filters.accounts = [];

                    this.getCategories();
                    this.getIncomeCategory();

                    return;
                }

                this.filters.accounts = selectedAccounts;

                this.getCategories();
                this.getIncomeCategory();
            },

public function getIncomeCategory(Request $request): JsonResponse
    {
        $userId = $request->query('userId');
        $startDate = $request->query('startDate');
        $endDate = $request->query('endDate');
        $accountIds = $request->query('accounts', []);
        $categoryIds = $request->query('categories', []);
        $labels = $request->query('labels', []);
        $types = $request->query('types', []);
        $paymentTypes = $request->query('paymentTypes', []);
        $paymentStatuses = $request->query('paymentStatuses', []);

        Log::info('received-parameters', [
            'userId' => $userId,
            'startDate' => $startDate,
            'endDate' => $endDate,
            'accountIds' => $accountIds,
            'categoryIds' => $categoryIds,
            'labels' => $labels,
            'types' => $types,
            'paymentTypes' => $paymentTypes,
            'paymentStatuses' => $paymentStatuses,
        ]);

        $query = Category::with(['children' => function ($query) use ($userId, $startDate, $endDate, $accountIds, $categoryIds, $labels, $types, $paymentTypes, $paymentStatuses) {
            $query->where('visible', true)
                ->with(['children' => function ($query) use ($userId, $startDate, $endDate, $accountIds, $categoryIds, $labels, $types, $paymentTypes, $paymentStatuses) {
                    $query->where('user_id', $userId)
                        ->where('visible', true)
                        ->with(['records' => function ($query) use ($userId, $startDate, $endDate, $accountIds, $categoryIds, $labels, $types, $paymentTypes, $paymentStatuses) {
                            $query->where('user_id', $userId)
                                ->whereBetween('date', [$startDate, $endDate])
                                ->when($accountIds, function ($query, $accountIds) {
                                    $query->whereIn('account_id', $accountIds);
                                })
                                ->when($categoryIds, function ($query, $categoryIds) {
                                    $query->whereIn('category_id', $categoryIds);
                                })
                                ->whereHas('labels', function ($q) use ($labels) {
                                    $q->whereIn('label_id', $labels);
                                })
                                ->when($types, function ($query, $types) {
                                    $query->whereIn('type', $types);
                                })
                                ->when($paymentTypes, function ($query, $paymentTypes) {
                                    $query->whereIn('payment_type', $paymentTypes);
                                })
                                ->when($paymentStatuses, function ($query, $paymentStatuses) {
                                    $query->whereIn('payment_status', $paymentStatuses);
                                });
                        }]);
                }]);
        }])
        ->whereNull('parent_id')
        ->where('visible', true)
        ->where(function ($query) {
            $query->whereRaw("json_extract(name, '$.en') = 'Income'")
                ->orWhereRaw("json_extract(name, '$.lv') = 'Ienākumi'");
        });

        Log::info('query', [$query->toSql()]);

        $categories = $query->get();
        
        return response()->json(CategoryResource::collection($categories));
    }

 public function getOtherCategories(Request $request): JsonResponse
    {
        $userId = $request->query('userId');
        $startDate = $request->query('startDate');
        $endDate = $request->query('endDate');
        $accountIds = $request->query('accounts', []);
        $categoryIds = $request->query('categories', []);
        $labels = $request->query('labels', []);
        $types = $request->query('types', []);
        $paymentTypes = $request->query('paymentTypes', []);
        $paymentStatuses = $request->query('paymentStatuses', []);

        Log::info('received-parameters', [
            'userId' => $userId,
            'startDate' => $startDate,
            'endDate' => $endDate,
            'accountIds' => $accountIds,
            'categoryIds' => $categoryIds,
            'labels' => $labels,
            'types' => $types,
            'paymentTypes' => $paymentTypes,
            'paymentStatuses' => $paymentStatuses,
        ]);

        $query = Category::with(['children' => function ($query) use ($userId, $startDate, $endDate, $accountIds, $categoryIds, $labels, $types, $paymentTypes, $paymentStatuses) {
            $query->where('visible', true)
                ->with(['children' => function ($query) use ($userId, $startDate, $endDate, $accountIds, $categoryIds, $labels, $types, $paymentTypes, $paymentStatuses) {
                    $query->where('user_id', $userId)
                        ->where('visible', true)
                        ->with(['records' => function ($query) use ($userId, $startDate, $endDate, $accountIds, $categoryIds, $labels, $types, $paymentTypes, $paymentStatuses) {
                            $query->where('user_id', $userId)
                                ->whereBetween('date', [$startDate, $endDate])
                                ->when($accountIds, function ($query, $accountIds) {
                                    $query->whereIn('account_id', $accountIds);
                                })
                                ->when($categoryIds, function ($query, $categoryIds) {
                                    $query->whereIn('category_id', $categoryIds);
                                })
                                ->whereHas('labels', function ($q) use ($labels) {
                                    $q->whereIn('label_id', $labels);
                                })
                                ->when($types, function ($query, $types) {
                                    $query->whereIn('type', $types);
                                })
                                ->when($paymentTypes, function ($query, $paymentTypes) {
                                    $query->whereIn('payment_type', $paymentTypes);
                                })
                                ->when($paymentStatuses, function ($query, $paymentStatuses) {
                                    $query->whereIn('payment_status', $paymentStatuses);
                                });
                        }]);
                }]);
        }])
        ->whereNull('parent_id')
        ->where('visible', true)
        ->where(function ($query) {
            $query->whereRaw("json_extract(name, '$.en') != 'Income'")
                ->orWhereRaw("json_extract(name, '$.lv') != 'Ienākumi'");
        });
        
        Log::info('query', [$query->toSql()]);

        $categories = $query->get();

        return response()->json(CategoryResource::collection($categories));
    }

Logs:

[2024-07-13 13:18:38] local.INFO: received-parameters {"userId":"1","startDate":null,"endDate":null,"accountIds":["2"],"categoryIds":[],"labels":[],"types":[],"paymentTypes":[],"paymentStatuses":[]} 
[2024-07-13 13:18:38] local.INFO: query ["select * from categories where parent_id is null and visible = ? and (json_extract(name, '$.en') != 'Income' or json_extract(name, '$.lv') != 'Ienākumi') and categories.deleted_at is null"] 
[2024-07-13 13:18:41] local.INFO: received-parameters {"userId":"1","startDate":null,"endDate":null,"accountIds":["2"],"categoryIds":[],"labels":[],"types":[],"paymentTypes":[],"paymentStatuses":[]} 
[2024-07-13 13:18:41] local.INFO: query ["select * from categories where parent_id is null and visible = ? and (json_extract(name, '$.en') = 'Income' or json_extract(name, '$.lv') = 'Ienākumi') and categories.deleted_at is null"] 
0 likes
2 replies
jazepsbumbiers's avatar

Category model:

class Category extends Model
{
    use HasFactory;
    use HasTranslations;
    use SoftDeletes;
    
    /**
     * @var array
     */
    protected $fillable = [
        'icon',
        'name',
        'visible',
        'color',
        'parent_id',
        'user_id',
    ];

    /**
     * @var array
     */
    public $translatable = [
        'name',
    ];

    /**
     * @var array
     */
    protected $appends = [
        'money_spent',
        'total_expenses',
        'money_earned',
        'total_income',
    ];

    /**
     * @return float
     */
    public function getMoneySpentAttribute(): float
    {
        return $this->getExpenseSumWithinRange(request('startDate'), request('endDate'), request('userId'));
    }

    /**
     * @return float
     */
    public function getTotalExpensesAttribute(): float
    {
        return $this->getTotalExpenseSumWithinRange(request('startDate'), request('endDate'), request('userId'));
    }

    /**
     * @return float
     */
    public function getMoneyEarnedAttribute(): float
    {
        return $this->getIncomeSumWithinRange(request('startDate'), request('endDate'), request('userId'));
    }

    /**
     * @return float
     */
    public function getTotalIncomeAttribute(): float
    {
        return $this->getTotalIncomeSumWithinRange(request('startDate'), request('endDate'), request('userId'));
    }

    /**
     * @param string|null $startDate
     * @param string|null $endDate
     * @param int|null $userId
     * 
     * @return float
     */
    public function getExpenseSumWithinRange(string $startDate = null, string $endDate = null, int $userId = null): float
    {
        return $this->records()
                    ->where('type', Record::TYPE_EXPENSE)
                    ->when($userId, function ($query) use ($userId) {
                        $query->where('user_id', $userId);
                    })
                    ->when($startDate && $endDate, function ($query) use ($startDate, $endDate) {
                        $query->whereBetween('date', [$startDate, $endDate]);
                    })
                    ->sum('amount');
    }

    /**
     * @param string|null $startDate
     * @param string|null $endDate
     * @param int|null $userId
     * 
     * @return float
     */
    public function getTotalExpenseSumWithinRange(string $startDate = null, string $endDate = null, int $userId = null): float
    {
        $totalExpenses = $this->allExpenseRecords()
                            ->when($userId, function ($query) use ($userId) {
                                $query->where('records.user_id', $userId);
                            })
                            ->when($startDate && $endDate, function ($query) use ($startDate, $endDate) {
                                $query->whereBetween('records.date', [$startDate, $endDate]);
                            })
                            ->whereHas('account', function ($query) {
                                $query->where('exclude_from_statistics', false);
                            })
                            ->sum('amount');

        foreach ($this->descendants as $child) {
            $totalExpenses += $child->allExpenseRecords()
                                    ->when($userId, function ($query) use ($userId) {
                                        $query->where('records.user_id', $userId);
                                    })
                                    ->when($startDate && $endDate, function ($query) use ($startDate, $endDate) {
                                        $query->whereBetween('records.date', [$startDate, $endDate]);
                                    })
                                    ->whereHas('account', function ($query) {
                                        $query->where('exclude_from_statistics', false);
                                    })
                                    ->sum('amount');
        }

        return $totalExpenses;
    }

    /**
     * @param string|null $startDate
     * @param string|null $endDate
     * @param int|null $userId
     * 
     * @return float
     */
    public function getIncomeSumWithinRange(string $startDate = null, string $endDate = null, int $userId = null): float
    {
        return $this->records()
                    ->where('type', Record::TYPE_INCOME)
                    ->when($userId, function ($query) use ($userId) {
                        $query->where('user_id', $userId);
                    })
                    ->when($startDate && $endDate, function ($query) use ($startDate, $endDate) {
                        $query->whereBetween('date', [$startDate, $endDate]);
                    })
                    ->sum('amount');
    }

    /**
     * @param string|null $startDate
     * @param string|null $endDate
     * @param int|null $userId
     * 
     * @return float
     */
    public function getTotalIncomeSumWithinRange(string $startDate = null, string $endDate = null, int $userId = null): float
    {
        $totalIncome = $this->allIncomeRecords()
                            ->when($userId, function ($query) use ($userId) {
                                $query->where('records.user_id', $userId);
                            })
                            ->when($startDate && $endDate, function ($query) use ($startDate, $endDate) {
                                $query->whereBetween('records.date', [$startDate, $endDate]);
                            })
                            ->whereHas('account', function ($query) {
                                $query->where('exclude_from_statistics', false);
                            })
                            ->sum('amount');

        foreach ($this->descendants as $child) {
            $totalIncome += $child->allIncomeRecords()
                                ->when($userId, function ($query) use ($userId) {
                                    $query->where('records.user_id', $userId);
                                })
                                ->when($startDate && $endDate, function ($query) use ($startDate, $endDate) {
                                    $query->whereBetween('records.date', [$startDate, $endDate]);
                                })
                                ->whereHas('account', function ($query) {
                                    $query->where('exclude_from_statistics', false);
                                })
                                ->sum('amount');
        }

        return $totalIncome;
    }
    
    /**
     * @param string|null $startDate
     * @param string|null $endDate
     * @param int|null $userId
     * 
     * @return array
     */
    public static function calculateTotalsForDateRange(string $startDate = null, string $endDate = null, int $userId = null): array
    {
        $categories = self::with('descendants')->get();

        $totalExpenses = 0;
        $totalIncome = 0;

        foreach ($categories as $category) {
            $totalExpenses += $category->getTotalExpenseSumWithinRange($startDate, $endDate, $userId);
            $totalIncome += $category->getTotalIncomeSumWithinRange($startDate, $endDate, $userId);
        }

        return [
            'total_expenses' => $totalExpenses,
            'total_income' => $totalIncome,
        ];
    }

    public function allExpenseRecords()
    {
        return $this->hasManyThrough(Record::class, Category::class, 'parent_id', 'category_id')
                    ->where('records.type', Record::TYPE_EXPENSE);
    }

    public function allIncomeRecords()
    {
        return $this->hasManyThrough(Record::class, Category::class, 'parent_id', 'category_id')
                    ->where('records.type', Record::TYPE_INCOME);
    }

    public function descendants()
    {
        return $this->hasMany(Category::class, 'parent_id', 'id')->with('descendants');
    }

    /**
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
    
    /**
     * @return HasMany
     */
    public function children(): HasMany
    {
        return $this->hasMany(Category::class, 'parent_id', 'id');
    }

    /**
     * @return BelongsTo
     */
    public function parent(): BelongsTo
    {
        return $this->belongsTo(Category::class, 'parent_id', 'id');
    }

    /**
     * @return HasMany
     */
    public function records(): HasMany
    {
        return $this->hasMany(Record::class);
    }
}
jazepsbumbiers's avatar

Record model:

class Record extends Model
{
    use HasFactory;
    use SoftDeletes;

    CONST TYPE_EXPENSE = 1;
    CONST TYPE_INCOME = 2;
    CONST TYPE_TRANSFER = 3;

    const PAYMENT_TYPE_CASH = 1;
    const PAYMENT_TYPE_DEBIT_CARD = 2;
    const PAYMENT_TYPE_CREDIT_CARD = 3;
    const PAYMENT_TYPE_TRANSFER = 4;
    const PAYMENT_TYPE_VOUCHER = 5;
    const PAYMENT_TYPE_MOBILE_PAYMENT = 6;
    const PAYMENT_TYPE_WEB_PAYMENT = 7;

    const PAYMENT_STATUS_CLEARED = 1;
    const PAYMENT_STATUS_RECONCILED = 2;
    const PAYMENT_STATUS_UNCLEARED = 3;

    public static $paymentTypes = [];
    public static $paymentStatuses = [];
    public static $types = [];

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'type',
        'account_id',
        'amount',
        'currency',
        'transfer_from_account_id',
        'transfer_to_account_id',
        'category_id',
        'date',
        'time',
        'payer',
        'location',
        'payment_type',
        'payment_status',
        'note',
        'user_id',
    ];

    public static function initializePaymentTypes()
    {
        self::$paymentTypes = [
            [
                'id' => self::PAYMENT_TYPE_CASH,
                'name' => __('payment-types.cash'),
            ],
            [
                'id' => self::PAYMENT_TYPE_DEBIT_CARD,
                'name' => __('payment-types.debit-card'),
            ],
            [
                'id' => self::PAYMENT_TYPE_CREDIT_CARD,
                'name' => __('payment-types.credit-card'),
            ],
            [
                'id' => self::PAYMENT_TYPE_TRANSFER,
                'name' => __('payment-types.transfer'),
            ],
            [
                'id' => self::PAYMENT_TYPE_VOUCHER,
                'name' => __('payment-types.voucher'),
            ],
            [
                'id' => self::PAYMENT_TYPE_MOBILE_PAYMENT,
                'name' => __('payment-types.mobile-payment'),
            ],
            [
                'id' => self::PAYMENT_TYPE_WEB_PAYMENT,
                'name' => __('payment-types.web-payment'),
            ],
        ];
    }

    public static function initializePaymentStatuses()
    {
        self::$paymentStatuses = [
            [
                'id' => self::PAYMENT_STATUS_CLEARED,
                'name' => __('payment-statuses.cleared'),
            ],
            [
                'id' => self::PAYMENT_STATUS_RECONCILED,
                'name' => __('payment-statuses.reconciled'),
            ],
            [
                'id' => self::PAYMENT_STATUS_UNCLEARED,
                'name' => __('payment-statuses.uncleared'),
            ],
        ];
    }

    public static function initializeTypes()
    {
        self::$types = [
            [
                'id' => self::TYPE_EXPENSE,
                'name' => __('record-types.expense'),
            ],
            [
                'id' => self::TYPE_INCOME,
                'name' => __('record-types.income'),
            ],
        ];
    }

    /**
     * @return BelongsTo
     */
    public function account(): BelongsTo
    {
        return $this->belongsTo(BankAccount::class);
    }

    /**
     * @return BelongsTo
     */
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * @return BelongsTo
     */
    public function accountType(): BelongsTo
    {
        return $this->belongsTo(AccountType::class);
    }

    /**
     * @return BelongsToMany
     */
    public function labels(): BelongsToMany
    {
        return $this->belongsToMany(Label::class);
    }

    /**
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

Please or to participate in this conversation.