diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index e9dfcd55..ddfdf4fe 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -48,15 +48,29 @@ class TransactionController extends Controller 'search' => $validated['search'] ?? null, ], fn ($value) => $value !== null); - $transactions = Transaction::query() + $query = Transaction::query() ->where('user_id', $user->id) ->with(['account.bank', 'category', 'labels']) - ->applyFilters($filters) - ->orderBy($sortColumn, $sortDirection) + ->applyFilters($filters); + + $nullableSortColumns = ['creditor_name', 'debtor_name']; + + if (in_array($sortColumn, $nullableSortColumns, true)) { + $sortAlias = $sortColumn.'_sort'; + $query->select('transactions.*') + ->selectRaw("COALESCE({$sortColumn}, '') as {$sortAlias}") + ->orderBy($sortAlias, $sortDirection); + } else { + $query->orderBy($sortColumn, $sortDirection); + } + + $transactions = $query ->orderBy('id', 'desc') ->cursorPaginate($perPage) ->withQueryString(); + $transactions->getCollection()->each(fn (Transaction $transaction) => $transaction->makeHidden(['creditor_name_sort', 'debtor_name_sort'])); + $appliedFilters = [ 'date_from' => $validated['date_from'] ?? null, 'date_to' => $validated['date_to'] ?? null, diff --git a/tests/Feature/TransactionFilterTest.php b/tests/Feature/TransactionFilterTest.php index 0b2a2e92..ac366985 100644 --- a/tests/Feature/TransactionFilterTest.php +++ b/tests/Feature/TransactionFilterTest.php @@ -614,3 +614,47 @@ test('filter by multiple categories including uncategorized', function () { ->has('transactions.data', 2) ); }); + +test('paginates across pages when sorting by a nullable column with null values', function () { + Transaction::factory()->plaintext()->count(12)->create([ + 'user_id' => $this->user->id, + 'account_id' => $this->account->id, + 'creditor_name' => null, + ]); + + Transaction::factory()->plaintext()->count(5)->create([ + 'user_id' => $this->user->id, + 'account_id' => $this->account->id, + 'creditor_name' => 'ACME', + ]); + + $firstPage = actingAs($this->user)->get(route('transactions.index', [ + 'sort' => 'creditor_name', + 'per_page' => 10, + ])); + + $firstPage->assertSuccessful(); + + $nextPageUrl = $firstPage->getOriginalContent()->getData()['page']['props']['transactions']['next_page_url']; + + expect($nextPageUrl)->not->toBeNull(); + + actingAs($this->user)->get($nextPageUrl)->assertSuccessful(); +}); + +test('does not expose the sort alias attribute when sorting by a nullable column', function () { + Transaction::factory()->plaintext()->create([ + 'user_id' => $this->user->id, + 'account_id' => $this->account->id, + 'creditor_name' => null, + ]); + + $response = actingAs($this->user)->get(route('transactions.index', [ + 'sort' => 'creditor_name', + ])); + + $response->assertInertia(fn ($page) => $page + ->has('transactions.data', 1) + ->missing('transactions.data.0.creditor_name_sort') + ); +});