diff --git a/app/Http/Controllers/Transaction/IndexController.php b/app/Http/Controllers/Transaction/IndexController.php index ad0b2b3931..aaa6c3f0a5 100644 --- a/app/Http/Controllers/Transaction/IndexController.php +++ b/app/Http/Controllers/Transaction/IndexController.php @@ -105,7 +105,7 @@ class IndexController extends Controller $endPeriod = clone $end; $endPeriod->endOfDay(); // limit to 6 years for the time being. - $max = 6; + $max = 6; if (now()->diffInYears($startPeriod, true) > $max) { $startPeriod = now()->subYears($max); } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 8160b679b1..1487d1a86e 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -51,6 +51,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U { Log::debug(sprintf('Collect all statistics where type starts with "%s"', $prefix)); Log::debug(sprintf('Between %s and %s', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); + return $this->userGroup ->periodStatistics() ->where('type', 'LIKE', sprintf('%s%%', $prefix)) diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php index 60e7057dab..e4711bb778 100644 --- a/app/Services/Internal/Support/JournalServiceTrait.php +++ b/app/Services/Internal/Support/JournalServiceTrait.php @@ -40,6 +40,7 @@ use FireflyIII\Support\NullArrayObject; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Facades\Log; use Safe\Exceptions\JsonException; + use function Safe\json_encode; /** @@ -47,10 +48,10 @@ use function Safe\json_encode; */ trait JournalServiceTrait { - private AccountRepositoryInterface $accountRepository; - private BudgetRepositoryInterface $budgetRepository; + private AccountRepositoryInterface $accountRepository; + private BudgetRepositoryInterface $budgetRepository; private CategoryRepositoryInterface $categoryRepository; - private TagFactory $tagFactory; + private TagFactory $tagFactory; /** * @throws FireflyException @@ -67,23 +68,23 @@ trait JournalServiceTrait unset($array); // and now try to find it, based on the type of transaction. - $message = 'Transaction = %s, %s account should be in: %s. Direction is %s.'; + $message = 'Transaction = %s, %s account should be in: %s. Direction is %s.'; Log::debug(sprintf($message, $transactionType, $direction, implode(', ', $expectedTypes[$transactionType] ?? ['UNKNOWN']), $direction)); - $result = $this->findAccountById($data, $expectedTypes[$transactionType], $opposite); - $result = $this->findAccountByIban($result, $data, $expectedTypes[$transactionType], $opposite); - $ibanResult = $result; - $result = $this->findAccountByNumber($result, $data, $expectedTypes[$transactionType], $opposite); - $numberResult = $result; - $result = $this->findAccountByName($result, $data, $expectedTypes[$transactionType], $opposite); - $nameResult = $result; + $result = $this->findAccountById($data, $expectedTypes[$transactionType], $opposite); + $result = $this->findAccountByIban($result, $data, $expectedTypes[$transactionType], $opposite); + $ibanResult = $result; + $result = $this->findAccountByNumber($result, $data, $expectedTypes[$transactionType], $opposite); + $numberResult = $result; + $result = $this->findAccountByName($result, $data, $expectedTypes[$transactionType], $opposite); + $nameResult = $result; // if $result (find by name) is NULL, but IBAN is set, any result of the search by NAME can't overrule // this account. In such a case, the name search must be retried with a new name. - if (null !== $nameResult && null === $numberResult && null === $ibanResult && '' !== (string)$data['iban'] && '' !== (string)$nameResult->iban) { + if (null !== $nameResult && null === $numberResult && null === $ibanResult && '' !== (string) $data['iban'] && '' !== (string) $nameResult->iban) { $data['name'] = sprintf('%s (%s)', $data['name'], $data['iban']); Log::debug(sprintf('Search again using the new name, "%s".', $data['name'])); - $result = $this->findAccountByName(null, $data, $expectedTypes[$transactionType], $opposite); + $result = $this->findAccountByName(null, $data, $expectedTypes[$transactionType], $opposite); } // the account that Firefly III creates must be "creatable", aka select the one we can create from the list just in case @@ -96,7 +97,7 @@ trait JournalServiceTrait Log::debug(sprintf('Account #%d may exist and be of the wrong type, use data to create one of the right type.', $data['id'])); $temp = $this->findAccountById(['id' => $data['id']], []); if (null !== $temp) { - $tempData = ['name' => $temp->name, 'iban' => $temp->iban, 'number' => null, 'bic' => null]; + $tempData = ['name' => $temp->name, 'iban' => $temp->iban, 'number' => null, 'bic' => null]; $result = $this->createAccount(null, $tempData, $creatableType); } } @@ -183,7 +184,7 @@ trait JournalServiceTrait protected function storeNotes(TransactionJournal $journal, ?string $notes): void { - $notes = (string)$notes; + $notes = (string) $notes; $note = $journal->notes()->first(); if ('' !== $notes) { if (null === $note) { @@ -215,7 +216,7 @@ trait JournalServiceTrait } Log::debug('Start of loop.'); foreach ($tags as $string) { - $string = (string)$string; + $string = (string) $string; Log::debug(sprintf('Now at tag "%s"', $string)); if ('' !== $string) { $tag = $this->tagFactory->findOrCreate($string); @@ -227,6 +228,7 @@ trait JournalServiceTrait $set = array_unique($set); Log::debug('End of loop.'); Log::debug(sprintf('Total nr. of tags: %d', count($tags)), $tags); + try { $journal->tags()->sync($set); } catch (UniqueConstraintViolationException $e) { @@ -251,51 +253,51 @@ trait JournalServiceTrait throw new FireflyException(sprintf('TransactionFactory: Cannot create asset account with these values: %s', json_encode($data))); } // fix name of account if only IBAN is given: - if ('' === (string)$data['name'] && '' !== (string)$data['iban']) { + if ('' === (string) $data['name'] && '' !== (string) $data['iban']) { Log::debug(sprintf('Account name is now IBAN ("%s")', $data['iban'])); $data['name'] = $data['iban']; } // fix name of account if only number is given: - if ('' === (string)$data['name'] && '' !== (string)$data['number']) { + if ('' === (string) $data['name'] && '' !== (string) $data['number']) { Log::debug(sprintf('Account name is now account number ("%s")', $data['number'])); $data['name'] = $data['number']; } // if name is still NULL, return NULL. - if ('' === (string)$data['name']) { + if ('' === (string) $data['name']) { Log::debug('Account name is still NULL, return NULL.'); return null; } // 2025-04-19 sanity check on IBAN. $validator = new UniqueIban(null, $preferredType); - if ('' !== (string)$data['iban'] && !$validator->passes('iban', $data['iban'])) { + if ('' !== (string) $data['iban'] && !$validator->passes('iban', $data['iban'])) { Log::warning(sprintf('IBAN "%s" is already in use, quietly ignore it.', $data['iban'])); $data['iban'] = null; } // $data['name'] = $data['name'] ?? '(no name)'; - $account = $this->accountRepository->store([ - 'account_type_id' => null, - 'account_type_name' => $preferredType, - 'name' => $data['name'], - 'virtual_balance' => null, - 'active' => true, - 'iban' => $data['iban'], - 'currency_id' => $data['currency_id'] ?? null, - 'order' => $this->accountRepository->maxOrder($preferredType), - ]); + $account = $this->accountRepository->store([ + 'account_type_id' => null, + 'account_type_name' => $preferredType, + 'name' => $data['name'], + 'virtual_balance' => null, + 'active' => true, + 'iban' => $data['iban'], + 'currency_id' => $data['currency_id'] ?? null, + 'order' => $this->accountRepository->maxOrder($preferredType), + ]); // store BIC if (null !== $data['bic']) { /** @var AccountMetaFactory $metaFactory */ $metaFactory = app(AccountMetaFactory::class); - $metaFactory->create(['account_id' => $account->id, 'name' => 'BIC', 'data' => $data['bic']]); + $metaFactory->create(['account_id' => $account->id, 'name' => 'BIC', 'data' => $data['bic']]); } // store account number if (null !== $data['number']) { /** @var AccountMetaFactory $metaFactory */ $metaFactory = app(AccountMetaFactory::class); - $metaFactory->create(['account_id' => $account->id, 'name' => 'account_number', 'data' => $data['number']]); + $metaFactory->create(['account_id' => $account->id, 'name' => 'account_number', 'data' => $data['number']]); } } @@ -339,7 +341,7 @@ trait JournalServiceTrait { // first attempt, find by ID. if (null !== $data['id']) { - $search = $this->accountRepository->find((int)$data['id']); + $search = $this->accountRepository->find((int) $data['id']); if (null !== $search && in_array($search->accountType->type, $types, true)) { Log::debug(sprintf('Found "account_id" object: #%d, "%s" of type %s (1)', $search->id, $search->name, $search->accountType->type)); @@ -412,10 +414,10 @@ trait JournalServiceTrait return null; } // find by preferred type. - $result = $this->accountRepository->findByAccountNumber((string)$data['number'], [$types[0]]); + $result = $this->accountRepository->findByAccountNumber((string) $data['number'], [$types[0]]); // or any expected type. - $result ??= $this->accountRepository->findByAccountNumber((string)$data['number'], $types); + $result ??= $this->accountRepository->findByAccountNumber((string) $data['number'], $types); if (null !== $result) { Log::debug(sprintf('Found account: #%d, %s', $result->id, $result->name)); @@ -437,7 +439,7 @@ trait JournalServiceTrait private function getCashAccount(?Account $account, array $data, array $types): ?Account { // return cash account. - if (!$account instanceof Account && '' === (string)$data['name'] && in_array(AccountTypeEnum::CASH->value, $types, true)) { + if (!$account instanceof Account && '' === (string) $data['name'] && in_array(AccountTypeEnum::CASH->value, $types, true)) { $account = $this->accountRepository->getCashAccount(); } Log::debug('Cannot return cash account, return input instead.'); diff --git a/app/Support/Calendar/Calculator.php b/app/Support/Calendar/Calculator.php index 5d44cb2363..17576d61ae 100644 --- a/app/Support/Calendar/Calculator.php +++ b/app/Support/Calendar/Calculator.php @@ -49,6 +49,7 @@ class Calculator // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private static array $intervals = []; public function isAvailablePeriodicity(Periodicity $periodicity): bool diff --git a/app/Support/Export/ExportDataGenerator.php b/app/Support/Export/ExportDataGenerator.php index 3c3e4963b8..4cea227fc0 100644 --- a/app/Support/Export/ExportDataGenerator.php +++ b/app/Support/Export/ExportDataGenerator.php @@ -116,6 +116,8 @@ class ExportDataGenerator // @phpstan-ignore-line + // @phpstan-ignore-line + public function __construct() { $this->accounts = new Collection(); diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index c62f6d8526..6c8810702b 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -74,13 +74,13 @@ use Illuminate\Support\Str; */ trait PeriodOverview { - protected AccountRepositoryInterface $accountRepository; - protected CategoryRepositoryInterface $categoryRepository; - protected TagRepositoryInterface $tagRepository; - protected JournalRepositoryInterface $journalRepos; + protected AccountRepositoryInterface $accountRepository; + protected CategoryRepositoryInterface $categoryRepository; + protected TagRepositoryInterface $tagRepository; + protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; - private Collection $statistics; - private array $transactions; + private Collection $statistics; + private array $transactions; /** * This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range) @@ -92,18 +92,18 @@ trait PeriodOverview protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array { Log::debug(sprintf('Now in getAccountPeriodOverview(#%d, %s %s)', $account->id, $start->format('Y-m-d H:i:s.u'), $end->format('Y-m-d H:i:s.u'))); - $this->accountRepository = app(AccountRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); $this->accountRepository->setUser($account->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); - $entries = []; + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleModelPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); @@ -120,18 +120,18 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); $this->categoryRepository->setUser($category->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { @@ -141,6 +141,34 @@ trait PeriodOverview return $entries; } + protected function getGenericPeriod(string $type, string $period, Carbon $start, Carbon $end): array + { + $return = [ + 'title' => Navigation::periodShow($start, $period), + 'route' => route('transactions.index', [$type, $start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => 0, + ]; + $setTypes = [ + 'withdrawal' => 'spent', + 'expenses' => 'spent', + 'deposit' => 'earned', + 'revenue' => 'earned', + 'transfer' => 'transferred', + 'transfers' => 'transferred', + ]; + if (!array_key_exists($type, $setTypes)) { + throw new FireflyException(sprintf('[c] Cannot deal with type "%s"', $type)); + } + $setType = $setTypes[$type]; + + $this->transactions = []; + $set = $this->getSingleGenericPeriodByType($start, $end, $type); + $return['total_transactions'] += $set['count']; + $return[$setType] = $set; + + return $return; + } + /** * Same as above, but for lists that involve transactions without a budget. * @@ -153,13 +181,13 @@ trait PeriodOverview Log::debug(sprintf('Now in getNoModelPeriodOverview(%s, %s %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'))); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $entries = []; - $this->statistics = $this->periodStatisticRepo->allInRangeForPrefix(sprintf('no_%s', $model), $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $entries = []; + $this->statistics = $this->periodStatisticRepo->allInRangeForPrefix(sprintf('no_%s', $model), $start, $end); Log::debug(sprintf('Collected %d stats', $this->statistics->count())); foreach ($dates as $currentDate) { @@ -169,7 +197,6 @@ trait PeriodOverview return $entries; } - protected function getSingleModelPeriod(Model $model, string $period, Carbon $start, Carbon $end): array { Log::debug(sprintf('Now in getSingleModelPeriod(%s #%d, %s %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); @@ -185,7 +212,7 @@ trait PeriodOverview ]; $this->transactions = []; foreach ($types as $type) { - $set = $this->getSingleModelPeriodByType($model, $start, $end, $type); + $set = $this->getSingleModelPeriodByType($model, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -201,18 +228,18 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array { // period overview for tags. - $this->tagRepository = app(TagRepositoryInterface::class); + $this->tagRepository = app(TagRepositoryInterface::class); $this->tagRepository->setUser($tag->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end); Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { @@ -229,12 +256,12 @@ trait PeriodOverview { $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $this->statistics = $this->periodStatisticRepo->allInRangeForPrefix('all_', $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $this->statistics = $this->periodStatisticRepo->allInRangeForPrefix('all_', $start, $end); Log::debug(sprintf('Collected %d statistics', $this->statistics->count())); foreach ($dates as $currentDate) { @@ -244,39 +271,6 @@ trait PeriodOverview return $entries; } - protected function getGenericPeriod(string $type, string $period, Carbon $start, Carbon $end): array - { - $return = [ - 'title' => Navigation::periodShow($start, $period), - 'route' => route('transactions.index', [ - $type, - $start->format('Y-m-d'), - $end->format('Y-m-d'), - ]), - 'total_transactions' => 0, - ]; - $setTypes = [ - 'withdrawal' => 'spent', - 'expenses' => 'spent', - 'deposit' => 'earned', - 'revenue' => 'earned', - 'transfer' => 'transferred', - 'transfers' => 'transferred', - ]; - if (!array_key_exists($type, $setTypes)) { - throw new FireflyException(sprintf('[c] Cannot deal with type "%s"', $type)); - } - $setType = $setTypes[$type]; - - $this->transactions = []; - $set = $this->getSingleGenericPeriodByType($start, $end, $type); - $return['total_transactions'] += $set['count']; - $return[$setType] = $set; - - return $return; - } - - /** * Filter a list of journals by a set of dates, and then group them by currency. */ @@ -303,7 +297,7 @@ trait PeriodOverview } return $this->statistics->filter( - static fn(PeriodStatistic $statistic): bool => ( + static fn (PeriodStatistic $statistic): bool => ( $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix) @@ -319,11 +313,10 @@ trait PeriodOverview return new Collection(); } Log::debug(sprintf('Now in filterStatistics("%s")', $type)); - return $this->statistics->filter( - static function (PeriodStatistic $statistic) use ($start, $end, $type): bool { - return $statistic->start->isSameSecond($start) && $statistic->end->isSameSecond($end) && $statistic->type === $type; - } - ); + + return $this->statistics->filter(static function (PeriodStatistic $statistic) use ($start, $end, $type): bool { + return $statistic->start->isSameSecond($start) && $statistic->end->isSameSecond($end) && $statistic->type === $type; + }); } private function filterTransactionsByType(TransactionTypeEnum $type, Carbon $start, Carbon $end): array @@ -335,7 +328,7 @@ trait PeriodOverview $fits = $item['type'] === $type->value && $date >= $start && $date <= $end; if ($fits) { // if type is withdrawal, negative amount: - if (TransactionTypeEnum::WITHDRAWAL === $type && 1 === bccomp((string)$item['amount'], '0')) { + if (TransactionTypeEnum::WITHDRAWAL === $type && 1 === bccomp((string) $item['amount'], '0')) { $item['amount'] = Steam::negative($item['amount']); } $result[] = $item; @@ -352,12 +345,12 @@ trait PeriodOverview foreach ($this->transactions as $item) { $date = Carbon::parse($item['date']); if ($date >= $start && $date <= $end) { - if ('Transfer' === $item['type'] && 'away' === $direction && -1 === bccomp((string)$item['amount'], '0')) { + if ('Transfer' === $item['type'] && 'away' === $direction && -1 === bccomp((string) $item['amount'], '0')) { $result[] = $item; continue; } - if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string)$item['amount'], '0')) { + if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string) $item['amount'], '0')) { $result[] = $item; } } @@ -385,24 +378,63 @@ trait PeriodOverview return [$start, $end]; } + private function getSingleGenericPeriodByType(Carbon $start, Carbon $end, string $type): array + { + $filterType = sprintf('all_%s', $type); + $statistics = $this->filterStatistics($start, $end, $filterType); + $types = config(sprintf('firefly.transactionTypesByType.%s', $type)); + // nothing found, regenerate them. + if (0 === $statistics->count()) { + if (0 === count($this->transactions)) { + // get collection! + // collect all journals in this period (regardless of type) + $collector = app(GroupCollectorInterface::class); + $collector->setTypes($types)->setRange($start, $end); + $this->transactions = $collector->getExtractedJournals(); + Log::debug(sprintf('Going to group %d found journal(s)', count($types))); + } + + $grouped = $this->groupByCurrency($this->filterJournalsByDate($this->transactions, $start, $end)); + $this->saveGroupedForPrefix('all', $start, $end, $type, $grouped); + + return $grouped; + } + $grouped = ['count' => 0]; + + /** @var PeriodStatistic $statistic */ + foreach ($statistics as $statistic) { + $id = (int) $statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ + 'amount' => (string) $statistic->amount, + 'count' => (int) $statistic->count, + 'currency_id' => $currency->id, + 'currency_name' => $currency->name, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + ]; + $grouped['count'] += (int) $statistic->count; + } + + return $grouped; + } + private function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array { Log::debug(sprintf( - 'Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', - $model::class, - $model->id, - $start->format('Y-m-d H:i:s.u'), - $end->format('Y-m-d H:i:s.u'), - $type - )); + 'Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', + $model::class, + $model->id, + $start->format('Y-m-d H:i:s.u'), + $end->format('Y-m-d H:i:s.u'), + $type + )); $statistics = $this->filterStatistics($start, $end, $type); // nothing found, regenerate them. if (0 === $statistics->count()) { - Log::debug(sprintf('Found nothing between %s and %s for type "%s"', - $start->format('Y-m-d H:i:s.u'), - $end->format('Y-m-d H:i:s.u'), - $type)); + Log::debug(sprintf('Found nothing between %s and %s for type "%s"', $start->format('Y-m-d H:i:s.u'), $end->format('Y-m-d H:i:s.u'), $type)); if (0 === count($this->transactions)) { switch ($model::class) { default: @@ -457,64 +489,22 @@ trait PeriodOverview return $grouped; } - $grouped = ['count' => 0]; + $grouped = ['count' => 0]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ - 'amount' => (string)$statistic->amount, - 'count' => (int)$statistic->count, + $id = (int) $statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ + 'amount' => (string) $statistic->amount, + 'count' => (int) $statistic->count, 'currency_id' => $currency->id, 'currency_name' => $currency->name, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, ]; - $grouped['count'] += (int)$statistic->count; - } - - return $grouped; - } - - private function getSingleGenericPeriodByType(Carbon $start, Carbon $end, string $type): array - { - $filterType = sprintf('all_%s', $type); - $statistics = $this->filterStatistics($start, $end, $filterType); - $types = config(sprintf('firefly.transactionTypesByType.%s', $type)); - // nothing found, regenerate them. - if (0 === $statistics->count()) { - if (0 === count($this->transactions)) { - // get collection! - // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); - $collector->setTypes($types)->setRange($start, $end); - $this->transactions = $collector->getExtractedJournals(); - Log::debug(sprintf('Going to group %d found journal(s)', count($types))); - } - - $grouped = $this->groupByCurrency($this->filterJournalsByDate($this->transactions, $start, $end)); - $this->saveGroupedForPrefix('all', $start, $end, $type, $grouped); - - return $grouped; - } - $grouped = ['count' => 0]; - - /** @var PeriodStatistic $statistic */ - foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ - 'amount' => (string)$statistic->amount, - 'count' => (int)$statistic->count, - 'currency_id' => $currency->id, - 'currency_name' => $currency->name, - 'currency_code' => $currency->code, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - ]; - $grouped['count'] += (int)$statistic->count; + $grouped['count'] += (int) $statistic->count; } return $grouped; @@ -536,7 +526,7 @@ trait PeriodOverview case 'budget': // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); $spent = $collector->getExtractedJournals(); $earned = []; @@ -547,23 +537,23 @@ trait PeriodOverview case 'category': // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earned = $collector->getExtractedJournals(); + $earned = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spent = $collector->getExtractedJournals(); + $spent = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -574,10 +564,10 @@ trait PeriodOverview $groupedSpent = $this->groupByCurrency($spent); $groupedEarned = $this->groupByCurrency($earned); $groupedTransferred = $this->groupByCurrency($transferred); - $entry = ['title' => $title, 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [ + $entry = ['title' => $title, 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [ $start->format('Y-m-d'), $end->format('Y-m-d'), - ]), 'total_transactions' => count($spent), 'spent' => $groupedSpent, 'earned' => $groupedEarned, 'transferred' => $groupedTransferred]; + ]), 'total_transactions' => count($spent), 'spent' => $groupedSpent, 'earned' => $groupedEarned, 'transferred' => $groupedTransferred]; $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'spent', $groupedSpent); $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'earned', $groupedEarned); $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'transferred', $groupedTransferred); @@ -586,30 +576,30 @@ trait PeriodOverview } Log::debug(sprintf('Found %d statistics in period %s - %s.', count($statistics), $start->format('Y-m-d'), $end->format('Y-m-d'))); - $entry = ['title' => $title, 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [ + $entry = ['title' => $title, 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [ $start->format('Y-m-d'), $end->format('Y-m-d'), - ]), 'total_transactions' => 0, 'spent' => [], 'earned' => [], 'transferred' => []]; - $grouped = []; + ]), 'total_transactions' => 0, 'spent' => [], 'earned' => [], 'transferred' => []]; + $grouped = []; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $type = str_replace(sprintf('no_%s_', $model), '', $statistic->type); - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); + $type = str_replace(sprintf('no_%s_', $model), '', $statistic->type); + $id = (int) $statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); $grouped[$type]['count'] ??= 0; - $grouped[$type][$id] = [ - 'amount' => (string)$statistic->amount, - 'count' => (int)$statistic->count, + $grouped[$type][$id] = [ + 'amount' => (string) $statistic->amount, + 'count' => (int) $statistic->count, 'currency_id' => $currency->id, 'currency_name' => $currency->name, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, ]; - $grouped[$type]['count'] += (int)$statistic->count; + $grouped[$type]['count'] += (int) $statistic->count; } - $types = ['spent', 'earned', 'transferred']; + $types = ['spent', 'earned', 'transferred']; foreach ($types as $type) { if (array_key_exists($type, $grouped)) { $entry['total_transactions'] += $grouped[$type]['count']; @@ -631,16 +621,16 @@ trait PeriodOverview /** @var array $journal */ foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $currencyCode = $journal['currency_code']; - $currencyName = $journal['currency_name']; - $currencySymbol = $journal['currency_symbol']; - $currencyDecimalPlaces = $journal['currency_decimal_places']; - $foreignCurrencyId = $journal['foreign_currency_id']; - $amount = (string)($journal['amount'] ?? '0'); + $currencyId = (int) $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + $foreignCurrencyId = $journal['foreign_currency_id']; + $amount = (string) ($journal['amount'] ?? '0'); if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId !== $this->primaryCurrency->id) { - $amount = (string)($journal['pc_amount'] ?? '0'); + $amount = (string) ($journal['pc_amount'] ?? '0'); $currencyId = $this->primaryCurrency->id; $currencyCode = $this->primaryCurrency->code; $currencyName = $this->primaryCurrency->name; @@ -648,12 +638,12 @@ trait PeriodOverview $currencyDecimalPlaces = $this->primaryCurrency->decimal_places; } if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId === $this->primaryCurrency->id) { - $currencyId = (int)$foreignCurrencyId; + $currencyId = (int) $foreignCurrencyId; $currencyCode = $journal['foreign_currency_code']; $currencyName = $journal['foreign_currency_name']; $currencySymbol = $journal['foreign_currency_symbol']; $currencyDecimalPlaces = $journal['foreign_currency_decimal_places']; - $amount = (string)($journal['foreign_amount'] ?? '0'); + $amount = (string) ($journal['foreign_amount'] ?? '0'); } $return[$currencyId] ??= [ 'amount' => '0', @@ -665,7 +655,7 @@ trait PeriodOverview 'currency_decimal_places' => $currencyDecimalPlaces, ]; - $return[$currencyId]['amount'] = bcadd((string)$return[$currencyId]['amount'], $amount); + $return[$currencyId]['amount'] = bcadd((string) $return[$currencyId]['amount'], $amount); ++$return[$currencyId]['count']; ++$return['count']; } @@ -677,14 +667,14 @@ trait PeriodOverview { unset($array['count']); Log::debug(sprintf( - 'saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', - $model::class, - $model->id, - $start->format('Y-m-d'), - $end->format('Y-m-d'), - $type, - count($array) - )); + 'saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', + $model::class, + $model->id, + $start->format('Y-m-d'), + $end->format('Y-m-d'), + $type, + count($array) + )); foreach ($array as $entry) { $this->periodStatisticRepo->saveStatistic($model, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); } @@ -698,13 +688,13 @@ trait PeriodOverview { unset($array['count']); Log::debug(sprintf( - 'saveGroupedForPrefix("%s", %s, %s, "%s", array(%d))', - $prefix, - $start->format('Y-m-d'), - $end->format('Y-m-d'), - $type, - count($array) - )); + 'saveGroupedForPrefix("%s", %s, %s, "%s", array(%d))', + $prefix, + $start->format('Y-m-d'), + $end->format('Y-m-d'), + $type, + count($array) + )); foreach ($array as $entry) { $this->periodStatisticRepo->savePrefixedStatistic($prefix, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); } diff --git a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php index 3179f42645..024bb82307 100644 --- a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php @@ -54,6 +54,7 @@ class AvailableBudgetEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private readonly bool $convertToPrimary; // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line @@ -68,6 +69,7 @@ class AvailableBudgetEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private array $currencies = []; private array $currencyIds = []; private array $ids = []; diff --git a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php index 8045b82968..49daae8b0c 100644 --- a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php @@ -54,6 +54,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private array $currencies = []; private array $currencyIds = []; private Carbon $end; diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php index e661d0dcca..dc981971fc 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php @@ -56,6 +56,7 @@ class PiggyBankEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private array $accounts = []; // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line @@ -70,6 +71,7 @@ class PiggyBankEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private array $amounts = []; private Collection $collection; private array $currencies = []; diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php index ba1892b9ba..0912aa0302 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php @@ -51,6 +51,7 @@ class PiggyBankEventEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private array $accountIds = []; // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line @@ -65,6 +66,7 @@ class PiggyBankEventEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private Collection $collection; private array $currencies = []; private array $groupIds = []; diff --git a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php index a78d829d87..f1fe723d20 100644 --- a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php +++ b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php @@ -60,6 +60,7 @@ class SubscriptionEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private readonly bool $convertToPrimary; private ?Carbon $end = null; private array $mappedObjects = []; diff --git a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php index 6c99908217..bcfc13f8a0 100644 --- a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php +++ b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php @@ -81,6 +81,8 @@ class TransactionGroupEnrichment implements EnrichmentInterface // @phpstan-ignore-line + // @phpstan-ignore-line + public function __construct() { $this->dateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date']; diff --git a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php index 93f54eaa36..de7606dbf8 100644 --- a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php +++ b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php @@ -56,6 +56,7 @@ class WebhookEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private array $ids = []; // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line @@ -70,6 +71,7 @@ class WebhookEnrichment implements EnrichmentInterface // @phpstan-ignore-line // @phpstan-ignore-line // @phpstan-ignore-line + // @phpstan-ignore-line private array $responses = []; private array $triggers = []; private array $webhookDeliveries = []; diff --git a/config/firefly.php b/config/firefly.php index f081a38f25..eee487f66d 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => (bool)envNonEmpty('USE_RUNNING_BALANCE', true), // this is only the default value, is not used. // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2026-02-13', - 'build_time' => 1770965855, + 'version' => 'develop/2026-02-14', + 'build_time' => 1771057393, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 28, // field is no longer used. diff --git a/package-lock.json b/package-lock.json index 63947d507d..ec467a1181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7118,9 +7118,9 @@ } }, "node_modules/i18next": { - "version": "25.8.6", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.6.tgz", - "integrity": "sha512-HsS6p2yr/Vo5EPljWuBJ9OxKVFok2Q/Oa6PvFTpv2bMcDt2sQMOnKDQ7FTDDdME+3d1YULQjKj7aVSZP1bCouQ==", + "version": "25.8.7", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.7.tgz", + "integrity": "sha512-ttxxc5+67S/0hhoeVdEgc1lRklZhdfcUSEPp1//uUG2NB88X3667gRsDar+ZWQFdysnOsnb32bcoMsa4mtzhkQ==", "funding": [ { "type": "individual", diff --git a/resources/assets/v1/src/locales/es.json b/resources/assets/v1/src/locales/es.json index 4c13f5d122..4e3a8f56e9 100644 --- a/resources/assets/v1/src/locales/es.json +++ b/resources/assets/v1/src/locales/es.json @@ -107,18 +107,18 @@ "multi_account_warning_withdrawal": "Tenga en cuenta que la cuenta de origen de las divisiones posteriores ser\u00e1 anulada por lo que se defina en la primera divisi\u00f3n del gasto.", "multi_account_warning_deposit": "Tenga en cuenta que la cuenta de destino de las divisiones posteriores ser\u00e1 anulada por lo que se defina en la primera divisi\u00f3n del retiro.", "multi_account_warning_transfer": "Tenga en cuenta que la cuenta de origen + destino de divisiones posteriores ser\u00e1 anulada por lo que se defina en la primera divisi\u00f3n de la transferencia.", - "webhook_trigger_ANY": "After any event", + "webhook_trigger_ANY": "Despu\u00e9s de cualquier evento", "webhook_trigger_STORE_TRANSACTION": "Despu\u00e9s de crear la transacci\u00f3n", "webhook_trigger_UPDATE_TRANSACTION": "Despu\u00e9s de actualizar la transacci\u00f3n", "webhook_trigger_DESTROY_TRANSACTION": "Despu\u00e9s de eliminar la transacci\u00f3n", - "webhook_trigger_STORE_BUDGET": "After budget creation", + "webhook_trigger_STORE_BUDGET": "Despu\u00e9s de crear un presupuesto", "webhook_trigger_UPDATE_BUDGET": "After budget update", "webhook_trigger_DESTROY_BUDGET": "After budget delete", "webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "After budgeted amount change", "webhook_response_TRANSACTIONS": "Detalles de la transacci\u00f3n", "webhook_response_RELEVANT": "Relevant details", "webhook_response_ACCOUNTS": "Detalles de la cuenta", - "webhook_response_NONE": "No details", + "webhook_response_NONE": "Sin detalles", "webhook_delivery_JSON": "JSON", "actions": "Acciones", "meta_data": "Meta Datos",