From 81f6f22efbe96deec91f7607bc403f0c4b846285 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 22 Feb 2026 07:05:30 +0100 Subject: [PATCH] Introduce undocumented count endpoint. --- .../Search/TransactionController.php | 73 ++++++++++++++++--- app/Api/V1/Requests/Search/CountRequest.php | 64 ++++++++++++++++ .../Journal/JournalRepository.php | 58 +++++++++++---- .../Journal/JournalRepositoryInterface.php | 4 + routes/api.php | 1 + 5 files changed, 176 insertions(+), 24 deletions(-) create mode 100644 app/Api/V1/Requests/Search/CountRequest.php diff --git a/app/Api/V1/Controllers/Search/TransactionController.php b/app/Api/V1/Controllers/Search/TransactionController.php index cc5768fce1..d95ca1a7c7 100644 --- a/app/Api/V1/Controllers/Search/TransactionController.php +++ b/app/Api/V1/Controllers/Search/TransactionController.php @@ -25,11 +25,15 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Search; use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Search\CountRequest; use FireflyIII\Api\V1\Requests\Search\TransactionSearchRequest; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment; use FireflyIII\Support\Search\SearchInterface; use FireflyIII\Transformers\TransactionGroupTransformer; +use FireflyIII\User; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Log; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Resource\Collection; @@ -38,37 +42,84 @@ use League\Fractal\Resource\Collection; */ class TransactionController extends Controller { + private JournalRepositoryInterface $repository; + + public function __construct() + { + parent::__construct(); + $this->middleware(function ($request, $next) { + /** @var User $admin */ + $admin = auth()->user(); + + $this->repository = app(JournalRepositoryInterface::class); + $this->repository->setUser($admin); + + return $next($request); + }); + } + + + public function count(CountRequest $request, SearchInterface $searcher): JsonResponse + { + $count = 0; + $includeDeleted = $request->attributes->get('include_deleted', false); + $externalId = (string)$request->attributes->get('external_identifier'); + $internalRef = (string)$request->attributes->get('internal_reference'); + $notes = (string) $request->attributes->get('notes'); + $description = (string) $request->attributes->get('description'); + Log::debug(sprintf('Include deleted? %s', var_export($includeDeleted, true))); + if ('' !== $externalId) { + $count += $this->repository->countByMeta('external_identifier', $externalId, $includeDeleted); + Log::debug(sprintf('Search for transactions with external_identifier "%s", count is now %d', $externalId, $count)); + } + if ('' !== $internalRef) { + $count += $this->repository->countByMeta('internal_reference', $internalRef, $includeDeleted); + Log::debug(sprintf('Search for transactions with internal_reference "%s", count is now %d', $internalRef, $count)); + } + if ('' !== $notes) { + $count += $this->repository->countByNotes($notes, $includeDeleted); + Log::debug(sprintf('Search for transactions with notes LIKE "%s", count is now %d',$notes, $count)); + } + if ('' !== $description) { + $count += $this->repository->countByDescription($description, $includeDeleted); + Log::debug(sprintf('Search for transactions with description "%s", count is now %d', $description, $count)); + } + + + return response()->json(['count' => $count]); + } + /** * This endpoint is documented at: * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/search/searchTransactions */ public function search(TransactionSearchRequest $request, SearchInterface $searcher): JsonResponse { - $manager = $this->getManager(); - $fullQuery = (string) $request->attributes->get('query'); - $page = $request->attributes->get('page'); - $pageSize = $request->attributes->get('limit'); + $manager = $this->getManager(); + $fullQuery = (string)$request->attributes->get('query'); + $page = $request->attributes->get('page'); + $pageSize = $request->attributes->get('limit'); $searcher->parseQuery($fullQuery); $searcher->setPage($page); $searcher->setLimit($pageSize); - $groups = $searcher->searchTransactions(); - $parameters = ['search' => $fullQuery]; - $url = route('api.v1.search.transactions').'?'.http_build_query($parameters); + $groups = $searcher->searchTransactions(); + $parameters = ['search' => $fullQuery]; + $url = route('api.v1.search.transactions') . '?' . http_build_query($parameters); $groups->setPath($url); // enrich - $enrichment = new TransactionGroupEnrichment(); + $enrichment = new TransactionGroupEnrichment(); $enrichment->setUser(auth()->user()); $transactions = $enrichment->enrich($groups->getCollection()); /** @var TransactionGroupTransformer $transformer */ - $transformer = app(TransactionGroupTransformer::class); + $transformer = app(TransactionGroupTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Collection($transactions, $transformer, 'transactions'); + $resource = new Collection($transactions, $transformer, 'transactions'); $resource->setPaginator(new IlluminatePaginatorAdapter($groups)); - $array = $manager->createData($resource)->toArray(); + $array = $manager->createData($resource)->toArray(); return response()->json($array)->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Api/V1/Requests/Search/CountRequest.php b/app/Api/V1/Requests/Search/CountRequest.php new file mode 100644 index 0000000000..efd34f43e3 --- /dev/null +++ b/app/Api/V1/Requests/Search/CountRequest.php @@ -0,0 +1,64 @@ +. + */ + +namespace FireflyIII\Api\V1\Requests\Search; + +use FireflyIII\Api\V1\Requests\AggregateFormRequest; +use FireflyIII\Rules\IsBoolean; +use Illuminate\Contracts\Validation\Validator; +use Override; + +class CountRequest extends AggregateFormRequest +{ + #[Override] + protected function getRequests(): array + { + return []; + } + + public function rules(): array + { + return [ + 'notes' => 'string|min:1|max:255', + 'external_identifier' => 'string|min:1|max:255', + 'description' => 'string|min:1|max:255', + 'internal_reference' => 'string|min:1|max:255', + 'include_deleted' => new IsBoolean(), + ]; + } + + public function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator): void { + if ($validator->failed()) { + return; + } + $this->attributes->set('include_deleted', $this->convertBoolean($this->input('include_deleted', 'false'))); + $this->attributes->set('notes', $this->convertString('notes')); + $this->attributes->set('external_identifier', $this->convertString('external_identifier')); + $this->attributes->set('description', $this->convertString('description')); + $this->attributes->set('internal_reference', $this->convertString('internal_reference')); + }); + } +} diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 4a3d714440..704efdd3ed 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -77,8 +77,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac ->transactionJournals() ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') ->whereIn('transaction_types.type', $types) - ->get(['transaction_journals.*']) - ; + ->get(['transaction_journals.*']); } /** @@ -89,8 +88,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac return $this->user ->transactionJournals() ->orderBy('date', 'ASC') - ->first(['transaction_journals.*']) - ; + ->first(['transaction_journals.*']); } #[Override] @@ -115,7 +113,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac */ public function getJournalTotal(TransactionJournal $journal): string { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($journal->id); $cache->addProperty('amount-positive'); if ($cache->has()) { @@ -124,7 +122,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac // saves on queries: $amount = $journal->transactions()->where('amount', '>', 0)->get()->sum('amount'); - $amount = (string) $amount; + $amount = (string)$amount; $cache->store($amount); return $amount; @@ -135,8 +133,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac return $this->user ->transactionJournals() ->orderBy('date', 'DESC') - ->first(['transaction_journals.*']) - ; + ->first(['transaction_journals.*']); } public function getLinkNoteText(TransactionJournalLink $link): string @@ -144,7 +141,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac /** @var null|Note $note */ $note = $link->notes()->first(); - return (string) $note?->text; + return (string)$note?->text; } /** @@ -187,8 +184,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac return $this->userGroup ->transactionJournals() ->where('completed', false) - ->get(['transaction_journals.*']) - ; + ->get(['transaction_journals.*']); } #[Override] @@ -212,8 +208,7 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac $query = $this->user ->transactionJournals() ->orderBy('date', 'DESC') - ->orderBy('description', 'ASC') - ; + ->orderBy('description', 'ASC'); if ('' !== $search) { $query->whereLike('description', sprintf('%%%s%%', $search)); } @@ -273,4 +268,41 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac return $journal; } + + #[\Override] + public function countByMeta(string $field, string $value, bool $includeDeleted): int + { + $search = TransactionJournalMeta:: + leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') + ->where('name', $field)->where('data', json_encode($value)) + ->where('transaction_journals.user_id', $this->user->id); + if ($includeDeleted) { + $search->withTrashed(); + } + return $search->count(); + } + + #[\Override] + public function countByNotes(string $value, bool $includeDeleted): int + { + $search = Note:: + where('noteable_type', TransactionJournal::class) + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'notes.noteable_id') + ->where('transaction_journals.user_id', $this->user->id) + ->where('text', 'LIKE', sprintf('%%%s%%', $value)); + if ($includeDeleted) { + $search->withTrashed(); + } + return $search->count(); + } + + #[\Override] + public function countByDescription(string $value, bool $includeDeleted): int + { + $search = $this->user->transactionJournals()->where('description', $value); + if ($includeDeleted) { + $search->withTrashed(); + } + return $search->count(); + } } diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index cd56939b56..deda82c392 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -52,6 +52,10 @@ interface JournalRepositoryInterface */ public function destroyGroup(TransactionGroup $transactionGroup): void; + public function countByMeta(string $field, string $value, bool $includeDeleted): int; + public function countByNotes(string $value, bool $includeDeleted): int; + public function countByDescription(string $value, bool $includeDeleted): int; + /** * Deletes a journal. */ diff --git a/routes/api.php b/routes/api.php index 749e8fbe91..96ccb2adb0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -699,6 +699,7 @@ Route::group( ], static function (): void { Route::get('transactions', ['uses' => 'TransactionController@search', 'as' => 'transactions']); + Route::get('transactions/count', ['uses' => 'TransactionController@count', 'as' => 'count']); Route::get('accounts', ['uses' => 'AccountController@search', 'as' => 'accounts']); } );