diff --git a/app/Http/Controllers/Account/ShowController.php b/app/Http/Controllers/Account/ShowController.php index 4b77635b4b..8c9ac76727 100644 --- a/app/Http/Controllers/Account/ShowController.php +++ b/app/Http/Controllers/Account/ShowController.php @@ -102,7 +102,7 @@ class ShowController extends Controller // make sure dates are end of day and start of day: $start->startOfDay(); - $end->endOfDay(); + $end->endOfDay()->milli(0); $location = $this->repository->getLocation($account); $attachments = $this->repository->getAttachments($account); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index 030735a00f..194073bc88 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -22,7 +22,8 @@ class PeriodStatistic extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', - 'date' => SeparateTimezoneCaster::class, + 'start' => SeparateTimezoneCaster::class, + 'end' => SeparateTimezoneCaster::class, ]; } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index ae8914c0c0..1762329f12 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -66,4 +66,9 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface return $stat; } + + public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection + { + return $model->primaryPeriodStatistics()->where('start','>=', $start)->where('end','<=', $end)->get(); + } } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php index 0b9a6bfbc0..d26d85d101 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -35,4 +35,6 @@ interface PeriodStatisticRepositoryInterface public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection; public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic; + + public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection; } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index c3d82f0caf..4b59c3a4d9 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -41,280 +41,6 @@ use NumberFormatter; */ class Amount { - /** - * This method will properly format the given number, in color or "black and white", - * as a currency, given two things: the currency required and the current locale. - * - * @throws FireflyException - */ - public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string - { - return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured); - } - - /** - * This method will properly format the given number, in color or "black and white", - * as a currency, given two things: the currency required and the current locale. - * - * @throws FireflyException - */ - public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string - { - $locale = Steam::getLocale(); - $rounded = Steam::bcround($amount, $decimalPlaces); - $coloured ??= true; - - $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); - $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol); - $fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces); - $fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces); - $result = (string)$fmt->format((float)$rounded); // intentional float - - if (true === $coloured) { - if (1 === bccomp($rounded, '0')) { - return sprintf('%s', $result); - } - if (-1 === bccomp($rounded, '0')) { - return sprintf('%s', $result); - } - - return sprintf('%s', $result); - } - - return $result; - } - - public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string - { - $format = $this->getTransactionCurrencyById($currencyId); - - return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured); - } - - public function getAllCurrencies(): Collection - { - return TransactionCurrency::orderBy('code', 'ASC')->get(); - } - - /** - * Experimental function to see if we can quickly and quietly get the amount from a journal. - * This depends on the user's default currency and the wish to have it converted. - */ - public function getAmountFromJournal(array $journal): string - { - $convertToPrimary = $this->convertToPrimary(); - $currency = $this->getPrimaryCurrency(); - $field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount'; - $amount = $journal[$field] ?? '0'; - // Log::debug(sprintf('Field is %s, amount is %s', $field, $amount)); - // fallback, the transaction has a foreign amount in $currency. - if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) { - $amount = $journal['foreign_amount']; - // Log::debug(sprintf('Overruled, amount is now %s', $amount)); - } - - return (string)$amount; - } - - public function getTransactionCurrencyById(int $currencyId): TransactionCurrency - { - $instance = PreferencesSingleton::getInstance(); - $key = sprintf('transaction_currency_%d', $currencyId); - - /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); - if (null !== $pref) { - return $pref; - } - $currency = TransactionCurrency::find($currencyId); - if (null === $currency) { - $message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__); - Log::error($message); - - throw new FireflyException($message); - } - $instance->setPreference($key, $currency); - - return $currency; - } - - public function getTransactionCurrencyByCode(string $code): TransactionCurrency - { - $instance = PreferencesSingleton::getInstance(); - $key = sprintf('transaction_currency_%s', $code); - - /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); - if (null !== $pref) { - return $pref; - } - $currency = TransactionCurrency::whereCode($code)->first(); - if (null === $currency) { - $message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__); - Log::error($message); - - throw new FireflyException($message); - } - $instance->setPreference($key, $currency); - - return $currency; - } - - public function convertToPrimary(?User $user = null): bool - { - $instance = PreferencesSingleton::getInstance(); - if (!$user instanceof User) { - $pref = $instance->getPreference('convert_to_primary_no_user'); - if (null === $pref) { - $res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled'); - $instance->setPreference('convert_to_primary_no_user', $res); - - return $res; - } - - return $pref; - } - $key = sprintf('convert_to_primary_%d', $user->id); - $pref = $instance->getPreference($key); - if (null === $pref) { - $res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled'); - $instance->setPreference($key, $res); - - return $res; - } - - return $pref; - } - - public function getPrimaryCurrency(): TransactionCurrency - { - if (auth()->check()) { - /** @var User $user */ - $user = auth()->user(); - if (null !== $user->userGroup) { - return $this->getPrimaryCurrencyByUserGroup($user->userGroup); - } - } - - return $this->getSystemCurrency(); - } - - public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency - { - $cache = new CacheProperties(); - $cache->addProperty('getPrimaryCurrencyByGroup'); - $cache->addProperty($userGroup->id); - if ($cache->has()) { - return $cache->get(); - } - - /** @var null|TransactionCurrency $primary */ - $primary = $userGroup->currencies()->where('group_default', true)->first(); - if (null === $primary) { - $primary = $this->getSystemCurrency(); - // could be the user group has no default right now. - $userGroup->currencies()->sync([$primary->id => ['group_default' => true]]); - } - $cache->store($primary); - - return $primary; - } - - public function getSystemCurrency(): TransactionCurrency - { - return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first(); - } - - /** - * Experimental function to see if we can quickly and quietly get the amount from a journal. - * This depends on the user's default currency and the wish to have it converted. - */ - public function getAmountFromJournalObject(TransactionJournal $journal): string - { - $convertToPrimary = $this->convertToPrimary(); - $currency = $this->getPrimaryCurrency(); - $field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount'; - - /** @var null|Transaction $sourceTransaction */ - $sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first(); - if (null === $sourceTransaction) { - return '0'; - } - $amount = $sourceTransaction->{$field} ?? '0'; - if ((int)$sourceTransaction->foreign_currency_id === $currency->id) { - // use foreign amount instead! - $amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount. - } - - return $amount; - } - - public function getCurrencies(): Collection - { - /** @var User $user */ - $user = auth()->user(); - - return $user->currencies()->orderBy('code', 'ASC')->get(); - } - - /** - * This method returns the correct format rules required by accounting.js, - * the library used to format amounts in charts. - * - * Used only in one place. - * - * @throws FireflyException - */ - public function getJsConfig(): array - { - $config = $this->getLocaleInfo(); - $negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']); - $positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']); - - return [ - 'mon_decimal_point' => $config['mon_decimal_point'], - 'mon_thousands_sep' => $config['mon_thousands_sep'], - 'format' => [ - 'pos' => $positive, - 'neg' => $negative, - 'zero' => $positive, - ], - ]; - } - - /** - * @throws FireflyException - */ - private function getLocaleInfo(): array - { - // get config from preference, not from translation: - $locale = Steam::getLocale(); - $array = Steam::getLocaleArray($locale); - - setlocale(LC_MONETARY, $array); - $info = localeconv(); - - // correct variables - $info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes'); - $info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes'); - - $info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space'); - $info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space'); - - $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); - - $info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL); - $info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL); - - return $info; - } - - private function getLocaleField(array $info, string $field): bool - { - return (is_bool($info[$field]) && true === $info[$field]) - || (is_int($info[$field]) && 1 === $info[$field]); - } - /** * bool $sepBySpace is $localeconv['n_sep_by_space'] * int $signPosn = $localeconv['n_sign_posn'] @@ -333,11 +59,11 @@ class Amount // there are five possible positions for the "+" or "-" sign (if it is even used) // pos_a and pos_e could be the ( and ) symbol. - $posA = ''; // before everything - $posB = ''; // before currency symbol - $posC = ''; // after currency symbol - $posD = ''; // before amount - $posE = ''; // after everything + $posA = ''; // before everything + $posB = ''; // before currency symbol + $posC = ''; // after currency symbol + $posD = ''; // before amount + $posE = ''; // after everything // format would be (currency before amount) // AB%sC_D%vE @@ -379,9 +105,283 @@ class Amount } if ($csPrecedes) { - return $posA.$posB.'%s'.$posC.$space.$posD.'%v'.$posE; + return $posA . $posB . '%s' . $posC . $space . $posD . '%v' . $posE; } - return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE; + return $posA . $posD . '%v' . $space . $posB . '%s' . $posC . $posE; + } + + public function convertToPrimary(?User $user = null): bool + { + $instance = PreferencesSingleton::getInstance(); + if (!$user instanceof User) { + $pref = $instance->getPreference('convert_to_primary_no_user'); + if (null === $pref) { + $res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled'); + $instance->setPreference('convert_to_primary_no_user', $res); + + return $res; + } + + return $pref; + } + $key = sprintf('convert_to_primary_%d', $user->id); + $pref = $instance->getPreference($key); + if (null === $pref) { + $res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled'); + $instance->setPreference($key, $res); + + return $res; + } + + return $pref; + } + + /** + * This method will properly format the given number, in color or "black and white", + * as a currency, given two things: the currency required and the current locale. + * + * @throws FireflyException + */ + public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string + { + return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured); + } + + public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string + { + $format = $this->getTransactionCurrencyById($currencyId); + + return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured); + } + + /** + * This method will properly format the given number, in color or "black and white", + * as a currency, given two things: the currency required and the current locale. + * + * @throws FireflyException + */ + public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string + { + $locale = Steam::getLocale(); + $rounded = Steam::bcround($amount, $decimalPlaces); + $coloured ??= true; + + $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); + $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol); + $fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces); + $fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces); + $result = (string)$fmt->format((float)$rounded); // intentional float + + if (true === $coloured) { + if (1 === bccomp($rounded, '0')) { + return sprintf('%s', $result); + } + if (-1 === bccomp($rounded, '0')) { + return sprintf('%s', $result); + } + + return sprintf('%s', $result); + } + + return $result; + } + + public function getAllCurrencies(): Collection + { + return TransactionCurrency::orderBy('code', 'ASC')->get(); + } + + /** + * Experimental function to see if we can quickly and quietly get the amount from a journal. + * This depends on the user's default currency and the wish to have it converted. + */ + public function getAmountFromJournal(array $journal): string + { + $convertToPrimary = $this->convertToPrimary(); + $currency = $this->getPrimaryCurrency(); + $field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount'; + $amount = $journal[$field] ?? '0'; + // Log::debug(sprintf('Field is %s, amount is %s', $field, $amount)); + // fallback, the transaction has a foreign amount in $currency. + if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) { + $amount = $journal['foreign_amount']; + // Log::debug(sprintf('Overruled, amount is now %s', $amount)); + } + + return (string)$amount; + } + + /** + * Experimental function to see if we can quickly and quietly get the amount from a journal. + * This depends on the user's default currency and the wish to have it converted. + */ + public function getAmountFromJournalObject(TransactionJournal $journal): string + { + $convertToPrimary = $this->convertToPrimary(); + $currency = $this->getPrimaryCurrency(); + $field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount'; + + /** @var null|Transaction $sourceTransaction */ + $sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first(); + if (null === $sourceTransaction) { + return '0'; + } + $amount = $sourceTransaction->{$field} ?? '0'; + if ((int)$sourceTransaction->foreign_currency_id === $currency->id) { + // use foreign amount instead! + $amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount. + } + + return $amount; + } + + public function getCurrencies(): Collection + { + /** @var User $user */ + $user = auth()->user(); + + return $user->currencies()->orderBy('code', 'ASC')->get(); + } + + /** + * This method returns the correct format rules required by accounting.js, + * the library used to format amounts in charts. + * + * Used only in one place. + * + * @throws FireflyException + */ + public function getJsConfig(): array + { + $config = $this->getLocaleInfo(); + $negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']); + $positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']); + + return [ + 'mon_decimal_point' => $config['mon_decimal_point'], + 'mon_thousands_sep' => $config['mon_thousands_sep'], + 'format' => [ + 'pos' => $positive, + 'neg' => $negative, + 'zero' => $positive, + ], + ]; + } + + public function getPrimaryCurrency(): TransactionCurrency + { + if (auth()->check()) { + /** @var User $user */ + $user = auth()->user(); + if (null !== $user->userGroup) { + return $this->getPrimaryCurrencyByUserGroup($user->userGroup); + } + } + + return $this->getSystemCurrency(); + } + + public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency + { + $cache = new CacheProperties(); + $cache->addProperty('getPrimaryCurrencyByGroup'); + $cache->addProperty($userGroup->id); + if ($cache->has()) { + return $cache->get(); + } + + /** @var null|TransactionCurrency $primary */ + $primary = $userGroup->currencies()->where('group_default', true)->first(); + if (null === $primary) { + $primary = $this->getSystemCurrency(); + // could be the user group has no default right now. + $userGroup->currencies()->sync([$primary->id => ['group_default' => true]]); + } + $cache->store($primary); + + return $primary; + } + + public function getSystemCurrency(): TransactionCurrency + { + return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first(); + } + + public function getTransactionCurrencyByCode(string $code): TransactionCurrency + { + $instance = PreferencesSingleton::getInstance(); + $key = sprintf('transaction_currency_%s', $code); + + /** @var null|TransactionCurrency $pref */ + $pref = $instance->getPreference($key); + if (null !== $pref) { + return $pref; + } + $currency = TransactionCurrency::whereCode($code)->first(); + if (null === $currency) { + $message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__); + Log::error($message); + + throw new FireflyException($message); + } + $instance->setPreference($key, $currency); + + return $currency; + } + + public function getTransactionCurrencyById(int $currencyId): TransactionCurrency + { + $instance = PreferencesSingleton::getInstance(); + $key = sprintf('transaction_currency_%d', $currencyId); + + /** @var null|TransactionCurrency $pref */ + $pref = $instance->getPreference($key); + if (null !== $pref) { + return $pref; + } + $currency = TransactionCurrency::find($currencyId); + if (null === $currency) { + $message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__); + Log::error($message); + + throw new FireflyException($message); + } + $instance->setPreference($key, $currency); + + return $currency; + } + + private function getLocaleField(array $info, string $field): bool + { + return (is_bool($info[$field]) && true === $info[$field]) + || (is_int($info[$field]) && 1 === $info[$field]); + } + + /** + * @throws FireflyException + */ + private function getLocaleInfo(): array + { + // get config from preference, not from translation: + $locale = Steam::getLocale(); + $array = Steam::getLocaleArray($locale); + + setlocale(LC_MONETARY, $array); + $info = localeconv(); + + // correct variables + $info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes'); + $info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes'); + + $info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space'); + $info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space'); + + $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); + + $info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL); + $info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL); + + return $info; } } diff --git a/app/Support/Authentication/RemoteUserGuard.php b/app/Support/Authentication/RemoteUserGuard.php index c2e534184b..353765af94 100644 --- a/app/Support/Authentication/RemoteUserGuard.php +++ b/app/Support/Authentication/RemoteUserGuard.php @@ -48,7 +48,7 @@ class RemoteUserGuard implements Guard public function __construct(protected UserProvider $provider, Application $app) { /** @var null|Request $request */ - $request = $app->get('request'); + $request = $app->get('request'); Log::debug(sprintf('Created RemoteUserGuard for %s "%s"', $request?->getMethod(), $request?->getRequestUri())); $this->application = $app; $this->user = null; @@ -63,8 +63,8 @@ class RemoteUserGuard implements Guard return; } // Get the user identifier from $_SERVER or apache filtered headers - $header = config('auth.guard_header', 'REMOTE_USER'); - $userID = request()->server($header) ?? null; + $header = config('auth.guard_header', 'REMOTE_USER'); + $userID = request()->server($header) ?? null; if (function_exists('apache_request_headers')) { Log::debug('Use apache_request_headers to find user ID.'); @@ -83,10 +83,10 @@ class RemoteUserGuard implements Guard $retrievedUser = $this->provider->retrieveById($userID); // store email address if present in header and not already set. - $header = config('auth.guard_email'); + $header = config('auth.guard_email'); if (null !== $header) { - $emailAddress = (string) (request()->server($header) ?? apache_request_headers()[$header] ?? null); + $emailAddress = (string)(request()->server($header) ?? apache_request_headers()[$header] ?? null); $preference = Preferences::getForUser($retrievedUser, 'remote_guard_alt_email'); if ('' !== $emailAddress && null === $preference && $emailAddress !== $userID) { @@ -99,7 +99,14 @@ class RemoteUserGuard implements Guard } Log::debug(sprintf('Result of getting user from provider: %s', $retrievedUser->email)); - $this->user = $retrievedUser; + $this->user = $retrievedUser; + } + + public function check(): bool + { + Log::debug(sprintf('Now at %s', __METHOD__)); + + return $this->user() instanceof User; } public function guest(): bool @@ -109,11 +116,32 @@ class RemoteUserGuard implements Guard return !$this->check(); } - public function check(): bool + public function hasUser(): bool { Log::debug(sprintf('Now at %s', __METHOD__)); - return $this->user() instanceof User; + throw new FireflyException('Did not implement RemoteUserGuard::hasUser()'); + } + + /** + * @SuppressWarnings("PHPMD.ShortMethodName") + */ + public function id(): int | string | null + { + Log::debug(sprintf('Now at %s', __METHOD__)); + + return $this->user?->id; + } + + public function setUser(Authenticatable | User | null $user): void // @phpstan-ignore-line + { + Log::debug(sprintf('Now at %s', __METHOD__)); + if ($user instanceof User) { + $this->user = $user; + + return; + } + Log::error(sprintf('Did not set user at %s', __METHOD__)); } public function user(): ?User @@ -129,34 +157,6 @@ class RemoteUserGuard implements Guard return $user; } - public function hasUser(): bool - { - Log::debug(sprintf('Now at %s', __METHOD__)); - - throw new FireflyException('Did not implement RemoteUserGuard::hasUser()'); - } - - /** - * @SuppressWarnings("PHPMD.ShortMethodName") - */ - public function id(): int|string|null - { - Log::debug(sprintf('Now at %s', __METHOD__)); - - return $this->user?->id; - } - - public function setUser(Authenticatable|User|null $user): void // @phpstan-ignore-line - { - Log::debug(sprintf('Now at %s', __METHOD__)); - if ($user instanceof User) { - $this->user = $user; - - return; - } - Log::error(sprintf('Did not set user at %s', __METHOD__)); - } - /** * @throws FireflyException * diff --git a/app/Support/Balance.php b/app/Support/Balance.php index 6b97a04628..f9d684c5ef 100644 --- a/app/Support/Balance.php +++ b/app/Support/Balance.php @@ -48,19 +48,18 @@ class Balance return $cache->get(); } - $query = Transaction::whereIn('transactions.account_id', $accounts->pluck('id')->toArray()) - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->orderBy('transaction_journals.date', 'desc') - ->orderBy('transaction_journals.order', 'asc') - ->orderBy('transaction_journals.description', 'desc') - ->orderBy('transactions.amount', 'desc') - ->where('transaction_journals.date', '<=', $date) - ; + $query = Transaction::whereIn('transactions.account_id', $accounts->pluck('id')->toArray()) + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->orderBy('transaction_journals.date', 'desc') + ->orderBy('transaction_journals.order', 'asc') + ->orderBy('transaction_journals.description', 'desc') + ->orderBy('transactions.amount', 'desc') + ->where('transaction_journals.date', '<=', $date); - $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']); + $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']); foreach ($result as $entry) { - $accountId = (int) $entry->account_id; - $currencyId = (int) $entry->transaction_currency_id; + $accountId = (int)$entry->account_id; + $currencyId = (int)$entry->transaction_currency_id; $currencies[$currencyId] ??= Amount::getTransactionCurrencyById($currencyId); $return[$accountId] ??= []; if (array_key_exists($currencyId, $return[$accountId])) { diff --git a/app/Support/Binder/AccountList.php b/app/Support/Binder/AccountList.php index 314a0c025f..3d6c48728e 100644 --- a/app/Support/Binder/AccountList.php +++ b/app/Support/Binder/AccountList.php @@ -43,23 +43,21 @@ class AccountList implements BinderInterface if ('allAssetAccounts' === $value) { /** @var Collection $collection */ $collection = auth()->user()->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('account_types.type', [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]) - ->orderBy('accounts.name', 'ASC') - ->get(['accounts.*']) - ; + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]) + ->orderBy('accounts.name', 'ASC') + ->get(['accounts.*']); } if ('allAssetAccounts' !== $value) { - $incoming = array_map('\intval', explode(',', $value)); - $list = array_merge(array_unique($incoming), [0]); + $incoming = array_map('\intval', explode(',', $value)); + $list = array_merge(array_unique($incoming), [0]); /** @var Collection $collection */ $collection = auth()->user()->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('accounts.id', $list) - ->orderBy('accounts.name', 'ASC') - ->get(['accounts.*']) - ; + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('accounts.id', $list) + ->orderBy('accounts.name', 'ASC') + ->get(['accounts.*']); } if ($collection->count() > 0) { diff --git a/app/Support/Binder/BudgetList.php b/app/Support/Binder/BudgetList.php index 6526ebd38a..917885a7d0 100644 --- a/app/Support/Binder/BudgetList.php +++ b/app/Support/Binder/BudgetList.php @@ -41,13 +41,12 @@ class BudgetList implements BinderInterface if (auth()->check()) { if ('allBudgets' === $value) { return auth()->user()->budgets()->where('active', true) - ->orderBy('order', 'ASC') - ->orderBy('name', 'ASC') - ->get() - ; + ->orderBy('order', 'ASC') + ->orderBy('name', 'ASC') + ->get(); } - $list = array_unique(array_map('\intval', explode(',', $value))); + $list = array_unique(array_map('\intval', explode(',', $value))); if (0 === count($list)) { // @phpstan-ignore-line app('log')->warning('Budget list count is zero, return 404.'); @@ -57,10 +56,9 @@ class BudgetList implements BinderInterface /** @var Collection $collection */ $collection = auth()->user()->budgets() - ->where('active', true) - ->whereIn('id', $list) - ->get() - ; + ->where('active', true) + ->whereIn('id', $list) + ->get(); // add empty budget if applicable. if (in_array(0, $list, true)) { diff --git a/app/Support/Binder/CategoryList.php b/app/Support/Binder/CategoryList.php index 1275481fa3..cde58f228f 100644 --- a/app/Support/Binder/CategoryList.php +++ b/app/Support/Binder/CategoryList.php @@ -41,21 +41,19 @@ class CategoryList implements BinderInterface if (auth()->check()) { if ('allCategories' === $value) { return auth()->user()->categories() - ->orderBy('name', 'ASC') - ->get() - ; + ->orderBy('name', 'ASC') + ->get(); } - $list = array_unique(array_map('\intval', explode(',', $value))); + $list = array_unique(array_map('\intval', explode(',', $value))); if (0 === count($list)) { // @phpstan-ignore-line throw new NotFoundHttpException(); } /** @var Collection $collection */ $collection = auth()->user()->categories() - ->whereIn('id', $list) - ->get() - ; + ->whereIn('id', $list) + ->get(); // add empty category if applicable. if (in_array(0, $list, true)) { diff --git a/app/Support/Binder/Date.php b/app/Support/Binder/Date.php index 99c0ce4c17..4dcfb314c8 100644 --- a/app/Support/Binder/Date.php +++ b/app/Support/Binder/Date.php @@ -43,16 +43,16 @@ class Date implements BinderInterface /** @var FiscalHelperInterface $fiscalHelper */ $fiscalHelper = app(FiscalHelperInterface::class); - $magicWords = [ - 'currentMonthStart' => today(config('app.timezone'))->startOfMonth(), - 'currentMonthEnd' => today(config('app.timezone'))->endOfMonth(), - 'currentYearStart' => today(config('app.timezone'))->startOfYear(), - 'currentYearEnd' => today(config('app.timezone'))->endOfYear(), + $magicWords = [ + 'currentMonthStart' => today(config('app.timezone'))->startOfMonth(), + 'currentMonthEnd' => today(config('app.timezone'))->endOfMonth(), + 'currentYearStart' => today(config('app.timezone'))->startOfYear(), + 'currentYearEnd' => today(config('app.timezone'))->endOfYear(), - 'previousMonthStart' => today(config('app.timezone'))->startOfMonth()->subDay()->startOfMonth(), - 'previousMonthEnd' => today(config('app.timezone'))->startOfMonth()->subDay()->endOfMonth(), - 'previousYearStart' => today(config('app.timezone'))->startOfYear()->subDay()->startOfYear(), - 'previousYearEnd' => today(config('app.timezone'))->startOfYear()->subDay()->endOfYear(), + 'previousMonthStart' => today(config('app.timezone'))->startOfMonth()->subDay()->startOfMonth(), + 'previousMonthEnd' => today(config('app.timezone'))->startOfMonth()->subDay()->endOfMonth(), + 'previousYearStart' => today(config('app.timezone'))->startOfYear()->subDay()->startOfYear(), + 'previousYearEnd' => today(config('app.timezone'))->startOfYear()->subDay()->endOfYear(), 'currentFiscalYearStart' => $fiscalHelper->startOfFiscalYear(today(config('app.timezone'))), 'currentFiscalYearEnd' => $fiscalHelper->endOfFiscalYear(today(config('app.timezone'))), @@ -68,7 +68,7 @@ class Date implements BinderInterface try { $result = new Carbon($value); - } catch (InvalidDateException|InvalidFormatException $e) { // @phpstan-ignore-line + } catch (InvalidDateException | InvalidFormatException $e) { // @phpstan-ignore-line $message = sprintf('Could not parse date "%s" for user #%d: %s', $value, auth()->user()->id, $e->getMessage()); app('log')->error($message); diff --git a/app/Support/Binder/JournalList.php b/app/Support/Binder/JournalList.php index 5eadcc587a..217dd565ed 100644 --- a/app/Support/Binder/JournalList.php +++ b/app/Support/Binder/JournalList.php @@ -39,7 +39,7 @@ class JournalList implements BinderInterface public static function routeBinder(string $value, Route $route): array { if (auth()->check()) { - $list = self::parseList($value); + $list = self::parseList($value); // get the journals by using the collector. /** @var GroupCollectorInterface $collector */ @@ -47,7 +47,7 @@ class JournalList implements BinderInterface $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value, TransactionTypeEnum::RECONCILIATION->value]); $collector->withCategoryInformation()->withBudgetInformation()->withTagInformation()->withAccountInformation(); $collector->setJournalIds($list); - $result = $collector->getExtractedJournals(); + $result = $collector->getExtractedJournals(); if (0 === count($result)) { throw new NotFoundHttpException(); } diff --git a/app/Support/Binder/TagList.php b/app/Support/Binder/TagList.php index 3dd4835f54..685087da75 100644 --- a/app/Support/Binder/TagList.php +++ b/app/Support/Binder/TagList.php @@ -43,11 +43,10 @@ class TagList implements BinderInterface if (auth()->check()) { if ('allTags' === $value) { return auth()->user()->tags() - ->orderBy('tag', 'ASC') - ->get() - ; + ->orderBy('tag', 'ASC') + ->get(); } - $list = array_unique(array_map('\strtolower', explode(',', $value))); + $list = array_unique(array_map('\strtolower', explode(',', $value))); app('log')->debug('List of tags is', $list); if (0 === count($list)) { // @phpstan-ignore-line @@ -59,7 +58,7 @@ class TagList implements BinderInterface /** @var TagRepositoryInterface $repository */ $repository = app(TagRepositoryInterface::class); $repository->setUser(auth()->user()); - $allTags = $repository->get(); + $allTags = $repository->get(); $collection = $allTags->filter( static function (Tag $tag) use ($list) { @@ -68,7 +67,7 @@ class TagList implements BinderInterface return true; } - if (in_array((string) $tag->id, $list, true)) { + if (in_array((string)$tag->id, $list, true)) { Log::debug(sprintf('TagList: (id) found tag #%d ("%s") in list.', $tag->id, $tag->tag)); return true; diff --git a/app/Support/Binder/TagOrId.php b/app/Support/Binder/TagOrId.php index bc511e5018..ad3a866e1a 100644 --- a/app/Support/Binder/TagOrId.php +++ b/app/Support/Binder/TagOrId.php @@ -40,9 +40,9 @@ class TagOrId implements BinderInterface $repository = app(TagRepositoryInterface::class); $repository->setUser(auth()->user()); - $result = $repository->findByTag($value); + $result = $repository->findByTag($value); if (null === $result) { - $result = $repository->find((int) $value); + $result = $repository->find((int)$value); } if (null !== $result) { return $result; diff --git a/app/Support/Binder/UserGroupAccount.php b/app/Support/Binder/UserGroupAccount.php index 12d7eff4a2..47a7af5541 100644 --- a/app/Support/Binder/UserGroupAccount.php +++ b/app/Support/Binder/UserGroupAccount.php @@ -41,10 +41,9 @@ class UserGroupAccount implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $account = Account::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $account = Account::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $account) { return $account; } diff --git a/app/Support/Binder/UserGroupBill.php b/app/Support/Binder/UserGroupBill.php index 551846d693..05eff73b6e 100644 --- a/app/Support/Binder/UserGroupBill.php +++ b/app/Support/Binder/UserGroupBill.php @@ -41,10 +41,9 @@ class UserGroupBill implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $currency = Bill::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $currency = Bill::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $currency) { return $currency; } diff --git a/app/Support/Binder/UserGroupExchangeRate.php b/app/Support/Binder/UserGroupExchangeRate.php index 74a65c9348..1bb8fcc374 100644 --- a/app/Support/Binder/UserGroupExchangeRate.php +++ b/app/Support/Binder/UserGroupExchangeRate.php @@ -38,10 +38,9 @@ class UserGroupExchangeRate implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $rate = CurrencyExchangeRate::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $rate = CurrencyExchangeRate::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $rate) { return $rate; } diff --git a/app/Support/Binder/UserGroupTransaction.php b/app/Support/Binder/UserGroupTransaction.php index d9131400f3..61add59c73 100644 --- a/app/Support/Binder/UserGroupTransaction.php +++ b/app/Support/Binder/UserGroupTransaction.php @@ -38,10 +38,9 @@ class UserGroupTransaction implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $group = TransactionGroup::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $group = TransactionGroup::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $group) { return $group; } diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php index b81f040467..38f2863e92 100644 --- a/app/Support/CacheProperties.php +++ b/app/Support/CacheProperties.php @@ -27,7 +27,6 @@ use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use JsonException; - use function Safe\json_encode; /** @@ -78,20 +77,6 @@ class CacheProperties return Cache::has($this->hash); } - private function hash(): void - { - $content = ''; - foreach ($this->properties as $property) { - try { - $content = sprintf('%s%s', $content, json_encode($property, JSON_THROW_ON_ERROR)); - } catch (JsonException) { - // @ignoreException - $content = sprintf('%s%s', $content, hash('sha256', (string) Carbon::now()->getTimestamp())); - } - } - $this->hash = substr(hash('sha256', $content), 0, 16); - } - /** * @param mixed $data */ @@ -99,4 +84,18 @@ class CacheProperties { Cache::forever($this->hash, $data); } + + private function hash(): void + { + $content = ''; + foreach ($this->properties as $property) { + try { + $content = sprintf('%s%s', $content, json_encode($property, JSON_THROW_ON_ERROR)); + } catch (JsonException) { + // @ignoreException + $content = sprintf('%s%s', $content, hash('sha256', (string)Carbon::now()->getTimestamp())); + } + } + $this->hash = substr(hash('sha256', $content), 0, 16); + } } diff --git a/app/Support/Calendar/Calculator.php b/app/Support/Calendar/Calculator.php index 3dff9dff07..b6ee2ceebb 100644 --- a/app/Support/Calendar/Calculator.php +++ b/app/Support/Calendar/Calculator.php @@ -33,31 +33,10 @@ use SplObjectStorage; */ class Calculator { - public const int DEFAULT_INTERVAL = 1; + public const int DEFAULT_INTERVAL = 1; private static ?SplObjectStorage $intervalMap = null; // @phpstan-ignore-line private static array $intervals = []; - /** - * @throws IntervalException - */ - public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon - { - if (!self::isAvailablePeriodicity($periodicity)) { - throw IntervalException::unavailable($periodicity, self::$intervals); - } - - /** @var Periodicity\Interval $periodicity */ - $periodicity = self::$intervalMap->offsetGet($periodicity); - $interval = $this->skipInterval($skipInterval); - - return $periodicity->nextDate($epoch->clone(), $interval); - } - - public function isAvailablePeriodicity(Periodicity $periodicity): bool - { - return self::containsInterval($periodicity); - } - private static function containsInterval(Periodicity $periodicity): bool { return self::loadIntervalMap()->contains($periodicity); @@ -78,6 +57,27 @@ class Calculator return self::$intervalMap; } + public function isAvailablePeriodicity(Periodicity $periodicity): bool + { + return self::containsInterval($periodicity); + } + + /** + * @throws IntervalException + */ + public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon + { + if (!self::isAvailablePeriodicity($periodicity)) { + throw IntervalException::unavailable($periodicity, self::$intervals); + } + + /** @var Periodicity\Interval $periodicity */ + $periodicity = self::$intervalMap->offsetGet($periodicity); + $interval = $this->skipInterval($skipInterval); + + return $periodicity->nextDate($epoch->clone(), $interval); + } + private function skipInterval(int $skip): int { return self::DEFAULT_INTERVAL + $skip; diff --git a/app/Support/Chart/Budget/FrontpageChartGenerator.php b/app/Support/Chart/Budget/FrontpageChartGenerator.php index 9e351afc78..e48cc65e42 100644 --- a/app/Support/Chart/Budget/FrontpageChartGenerator.php +++ b/app/Support/Chart/Budget/FrontpageChartGenerator.php @@ -69,9 +69,9 @@ class FrontpageChartGenerator Log::debug('Now in generate for budget chart.'); $budgets = $this->budgetRepository->getActiveBudgets(); $data = [ - ['label' => (string) trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'], - ['label' => (string) trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'], - ['label' => (string) trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'], + ['label' => (string)trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'], + ['label' => (string)trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'], + ['label' => (string)trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'], ]; // loop al budgets: @@ -84,6 +84,64 @@ class FrontpageChartGenerator return $data; } + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + public function setStart(Carbon $start): void + { + $this->start = $start; + } + + /** + * A basic setter for the user. Also updates the repositories with the right user. + */ + public function setUser(User $user): void + { + $this->budgetRepository->setUser($user); + $this->blRepository->setUser($user); + $this->opsRepository->setUser($user); + + $locale = app('steam')->getLocale(); + $this->monthAndDayFormat = (string)trans('config.month_and_day_js', [], $locale); + } + + /** + * If a budget has budget limit, each limit is processed individually. + */ + private function budgetLimits(array $data, Budget $budget, Collection $limits): array + { + Log::debug('Start processing budget limits.'); + + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $data = $this->processLimit($data, $budget, $limit); + } + Log::debug('Done processing budget limits.'); + + return $data; + } + + /** + * When no limits are present, the expenses of the whole period are collected and grouped. + * This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty. + */ + private function noBudgetLimits(array $data, Budget $budget): array + { + $spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget)); + + /** @var array $entry */ + foreach ($spent as $entry) { + $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + $data[0]['entries'][$title] = bcmul((string)$entry['sum'], '-1'); // spent + $data[1]['entries'][$title] = 0; // left to spend + $data[2]['entries'][$title] = 0; // overspent + } + + return $data; + } + /** * For each budget, gets all budget limits for the current time range. * When no limits are present, the time range is used to collect information on money spent. @@ -108,41 +166,6 @@ class FrontpageChartGenerator return $result; } - /** - * When no limits are present, the expenses of the whole period are collected and grouped. - * This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty. - */ - private function noBudgetLimits(array $data, Budget $budget): array - { - $spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget)); - - /** @var array $entry */ - foreach ($spent as $entry) { - $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); - $data[0]['entries'][$title] = bcmul((string) $entry['sum'], '-1'); // spent - $data[1]['entries'][$title] = 0; // left to spend - $data[2]['entries'][$title] = 0; // overspent - } - - return $data; - } - - /** - * If a budget has budget limit, each limit is processed individually. - */ - private function budgetLimits(array $data, Budget $budget, Collection $limits): array - { - Log::debug('Start processing budget limits.'); - - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $data = $this->processLimit($data, $budget, $limit); - } - Log::debug('Done processing budget limits.'); - - return $data; - } - /** * For each limit, the expenses from the time range of the limit are collected. Each row from the result is * processed individually. @@ -158,7 +181,7 @@ class FrontpageChartGenerator Log::debug(sprintf('Processing limit #%d with %s %s', $limit->id, $limit->transactionCurrency->code, $limit->amount)); } - $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency); + $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency); Log::debug(sprintf('Spent array has %d entries.', count($spent))); /** @var array $entry */ @@ -185,7 +208,7 @@ class FrontpageChartGenerator */ private function processRow(array $data, Budget $budget, BudgetLimit $limit, array $entry): array { - $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); Log::debug(sprintf('Title is "%s"', $title)); if ($limit->start_date->startOfDay()->ne($this->start->startOfDay()) || $limit->end_date->startOfDay()->ne($this->end->startOfDay())) { $title = sprintf( @@ -196,22 +219,22 @@ class FrontpageChartGenerator $limit->end_date->isoFormat($this->monthAndDayFormat) ); } - $usePrimary = $this->convertToPrimary && $this->default->id !== $limit->transaction_currency_id; - $amount = $limit->amount; + $usePrimary = $this->convertToPrimary && $this->default->id !== $limit->transaction_currency_id; + $amount = $limit->amount; Log::debug(sprintf('Amount is "%s".', $amount)); if ($usePrimary && $limit->transaction_currency_id !== $this->default->id) { $amount = $limit->native_amount; Log::debug(sprintf('Amount is now "%s".', $amount)); } $amount ??= '0'; - $sumSpent = bcmul((string) $entry['sum'], '-1'); // spent + $sumSpent = bcmul((string)$entry['sum'], '-1'); // spent $data[0]['entries'][$title] ??= '0'; $data[1]['entries'][$title] ??= '0'; $data[2]['entries'][$title] ??= '0'; - $data[0]['entries'][$title] = bcadd((string) $data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent - $data[1]['entries'][$title] = bcadd((string) $data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string) $entry['sum'], $amount) : '0'); // left to spent - $data[2]['entries'][$title] = bcadd((string) $data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string) $entry['sum'], $amount), '-1')); // overspent + $data[0]['entries'][$title] = bcadd((string)$data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent + $data[1]['entries'][$title] = bcadd((string)$data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string)$entry['sum'], $amount) : '0'); // left to spent + $data[2]['entries'][$title] = bcadd((string)$data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string)$entry['sum'], $amount), '-1')); // overspent Log::debug(sprintf('Amount [spent] is now %s.', $data[0]['entries'][$title])); Log::debug(sprintf('Amount [left] is now %s.', $data[1]['entries'][$title])); @@ -219,27 +242,4 @@ class FrontpageChartGenerator return $data; } - - public function setEnd(Carbon $end): void - { - $this->end = $end; - } - - public function setStart(Carbon $start): void - { - $this->start = $start; - } - - /** - * A basic setter for the user. Also updates the repositories with the right user. - */ - public function setUser(User $user): void - { - $this->budgetRepository->setUser($user); - $this->blRepository->setUser($user); - $this->opsRepository->setUser($user); - - $locale = app('steam')->getLocale(); - $this->monthAndDayFormat = (string) trans('config.month_and_day_js', [], $locale); - } } diff --git a/app/Support/Chart/Category/FrontpageChartGenerator.php b/app/Support/Chart/Category/FrontpageChartGenerator.php index c27f13c686..b106deafce 100644 --- a/app/Support/Chart/Category/FrontpageChartGenerator.php +++ b/app/Support/Chart/Category/FrontpageChartGenerator.php @@ -26,7 +26,6 @@ namespace FireflyIII\Support\Chart\Category; use Carbon\Carbon; use FireflyIII\Enums\AccountTypeEnum; -use FireflyIII\Models\Category; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; @@ -66,16 +65,16 @@ class FrontpageChartGenerator public function generate(): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $categories = $this->repository->getCategories(); - $accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); - $collection = $this->collectExpensesAll($categories, $accounts); + $categories = $this->repository->getCategories(); + $accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); + $collection = $this->collectExpensesAll($categories, $accounts); // collect for no-category: - $noCategory = $this->collectNoCatExpenses($accounts); - $collection = array_merge($collection, $noCategory); + $noCategory = $this->collectNoCatExpenses($accounts); + $collection = array_merge($collection, $noCategory); // sort temp array by amount. - $amounts = array_column($collection, 'sum_float'); + $amounts = array_column($collection, 'sum_float'); array_multisort($amounts, SORT_ASC, $collection); $currencyData = $this->createCurrencyGroups($collection); @@ -96,6 +95,30 @@ class FrontpageChartGenerator ]; } + private function collectExpensesAll(Collection $categories, Collection $accounts): array + { + Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories))); + $spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories); + $tempData = []; + foreach ($categories as $category) { + $sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary); + if (0 === count($sums)) { + continue; + } + foreach ($sums as $currency) { + $this->addCurrency($currency); + $tempData[] = [ + 'name' => $category->name, + 'sum' => $currency['sum'], + 'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']), + 'currency_id' => (int)$currency['currency_id'], + ]; + } + } + + return $tempData; + } + private function collectNoCatExpenses(Collection $accounts): array { $noCatExp = $this->noCatRepos->sumExpenses($this->start, $this->end, $accounts); @@ -147,28 +170,4 @@ class FrontpageChartGenerator return $currencyData; } - - private function collectExpensesAll(Collection $categories, Collection $accounts): array - { - Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories))); - $spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories); - $tempData = []; - foreach ($categories as $category) { - $sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary); - if (0 === count($sums)) { - continue; - } - foreach ($sums as $currency) { - $this->addCurrency($currency); - $tempData[] = [ - 'name' => $category->name, - 'sum' => $currency['sum'], - 'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']), - 'currency_id' => (int)$currency['currency_id'], - ]; - } - } - - return $tempData; - } } diff --git a/app/Support/Chart/Category/WholePeriodChartGenerator.php b/app/Support/Chart/Category/WholePeriodChartGenerator.php index 2a28cb4d62..044b7f28a2 100644 --- a/app/Support/Chart/Category/WholePeriodChartGenerator.php +++ b/app/Support/Chart/Category/WholePeriodChartGenerator.php @@ -40,22 +40,22 @@ class WholePeriodChartGenerator public function generate(Category $category, Carbon $start, Carbon $end): array { - $collection = new Collection()->push($category); + $collection = new Collection()->push($category); /** @var OperationsRepositoryInterface $opsRepository */ - $opsRepository = app(OperationsRepositoryInterface::class); + $opsRepository = app(OperationsRepositoryInterface::class); /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); - $types = [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; - $accounts = $accountRepository->getAccountsByType($types); - $step = $this->calculateStep($start, $end); - $chartData = []; - $spent = []; - $earned = []; + $types = [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; + $accounts = $accountRepository->getAccountsByType($types); + $step = $this->calculateStep($start, $end); + $chartData = []; + $spent = []; + $earned = []; - $current = clone $start; + $current = clone $start; while ($current <= $end) { $key = $current->format('Y-m-d'); @@ -65,33 +65,33 @@ class WholePeriodChartGenerator $current = app('navigation')->addPeriod($current, $step, 0); } - $currencies = $this->extractCurrencies($spent) + $this->extractCurrencies($earned); + $currencies = $this->extractCurrencies($spent) + $this->extractCurrencies($earned); // generate chart data (for each currency) /** @var array $currency */ foreach ($currencies as $currency) { - $code = $currency['currency_code']; - $name = $currency['currency_name']; - $chartData[sprintf('spent-in-%s', $code)] = [ - 'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $name]), + $code = $currency['currency_code']; + $name = $currency['currency_name']; + $chartData[sprintf('spent-in-%s', $code)] = [ + 'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $name]), 'entries' => [], 'type' => 'bar', 'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red ]; $chartData[sprintf('earned-in-%s', $code)] = [ - 'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $name]), + 'label' => (string)trans('firefly.box_earned_in_currency', ['currency' => $name]), 'entries' => [], 'type' => 'bar', 'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green ]; } - $current = clone $start; + $current = clone $start; while ($current <= $end) { - $key = $current->format('Y-m-d'); - $label = app('navigation')->periodShow($current, $step); + $key = $current->format('Y-m-d'); + $label = app('navigation')->periodShow($current, $step); /** @var array $currency */ foreach ($currencies as $currency) { diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php index ab03e3dd31..8d58c6a304 100644 --- a/app/Support/Chart/ChartData.php +++ b/app/Support/Chart/ChartData.php @@ -44,12 +44,12 @@ class ChartData public function add(array $data): void { if (array_key_exists('currency_id', $data)) { - $data['currency_id'] = (string) $data['currency_id']; + $data['currency_id'] = (string)$data['currency_id']; } if (array_key_exists('primary_currency_id', $data)) { - $data['primary_currency_id'] = (string) $data['primary_currency_id']; + $data['primary_currency_id'] = (string)$data['primary_currency_id']; } - $required = ['start', 'date', 'end', 'entries']; + $required = ['start', 'date', 'end', 'entries']; foreach ($required as $field) { if (!array_key_exists($field, $data)) { throw new FireflyException(sprintf('Data-set is missing the "%s"-variable.', $field)); diff --git a/app/Support/ChartColour.php b/app/Support/ChartColour.php index 9e938b9946..f08de5c258 100644 --- a/app/Support/ChartColour.php +++ b/app/Support/ChartColour.php @@ -55,7 +55,7 @@ class ChartColour public static function getColour(int $index): string { $index %= count(self::$colours); - $row = self::$colours[$index]; + $row = self::$colours[$index]; return sprintf('rgba(%d, %d, %d, 0.7)', $row[0], $row[1], $row[2]); } diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php index 9a2a5a0d66..6855884d66 100644 --- a/app/Support/Cronjobs/AutoBudgetCronjob.php +++ b/app/Support/Cronjobs/AutoBudgetCronjob.php @@ -39,7 +39,7 @@ class AutoBudgetCronjob extends AbstractCronjob { /** @var Configuration $config */ $config = FireflyConfig::get('last_ab_job', 0); - $lastTime = (int) $config->data; + $lastTime = (int)$config->data; $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { @@ -70,7 +70,7 @@ class AutoBudgetCronjob extends AbstractCronjob Log::info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d'))); /** @var CreateAutoBudgetLimits $job */ - $job = app(CreateAutoBudgetLimits::class, [$this->date]); + $job = app(CreateAutoBudgetLimits::class, [$this->date]); $job->setDate($this->date); $job->handle(); @@ -80,7 +80,7 @@ class AutoBudgetCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Auto-budget cron job fired successfully.'; - FireflyConfig::set('last_ab_job', (int) $this->date->format('U')); + FireflyConfig::set('last_ab_job', (int)$this->date->format('U')); Log::info('Done with auto budget cron job task.'); } } diff --git a/app/Support/Cronjobs/BillWarningCronjob.php b/app/Support/Cronjobs/BillWarningCronjob.php index 8a30bb9c0a..f192aa1224 100644 --- a/app/Support/Cronjobs/BillWarningCronjob.php +++ b/app/Support/Cronjobs/BillWarningCronjob.php @@ -45,7 +45,7 @@ class BillWarningCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_bw_job', 0); - $lastTime = (int) $config->data; + $lastTime = (int)$config->data; $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); @@ -82,7 +82,7 @@ class BillWarningCronjob extends AbstractCronjob Log::info(sprintf('Will now fire bill notification job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); /** @var WarnAboutBills $job */ - $job = app(WarnAboutBills::class); + $job = app(WarnAboutBills::class); $job->setDate($this->date); $job->setForce($this->force); $job->handle(); @@ -93,8 +93,8 @@ class BillWarningCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Bill notification cron job fired successfully.'; - FireflyConfig::set('last_bw_job', (int) $this->date->format('U')); - Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); + FireflyConfig::set('last_bw_job', (int)$this->date->format('U')); + Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U'))); Log::info('Done with bill notification cron job task.'); } } diff --git a/app/Support/Cronjobs/ExchangeRatesCronjob.php b/app/Support/Cronjobs/ExchangeRatesCronjob.php index 71c9a8e587..57cb788bc7 100644 --- a/app/Support/Cronjobs/ExchangeRatesCronjob.php +++ b/app/Support/Cronjobs/ExchangeRatesCronjob.php @@ -39,7 +39,7 @@ class ExchangeRatesCronjob extends AbstractCronjob { /** @var Configuration $config */ $config = FireflyConfig::get('last_cer_job', 0); - $lastTime = (int) $config->data; + $lastTime = (int)$config->data; $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { @@ -71,7 +71,7 @@ class ExchangeRatesCronjob extends AbstractCronjob Log::info(sprintf('Will now fire exchange rates cron job task for date "%s".', $this->date->format('Y-m-d'))); /** @var DownloadExchangeRates $job */ - $job = app(DownloadExchangeRates::class); + $job = app(DownloadExchangeRates::class); $job->setDate($this->date); $job->handle(); @@ -81,7 +81,7 @@ class ExchangeRatesCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Exchange rates cron job fired successfully.'; - FireflyConfig::set('last_cer_job', (int) $this->date->format('U')); + FireflyConfig::set('last_cer_job', (int)$this->date->format('U')); Log::info('Done with exchange rates job task.'); } } diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index c722733253..1f8654b9a7 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -45,7 +45,7 @@ class RecurringCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_rt_job', 0); - $lastTime = (int) $config->data; + $lastTime = (int)$config->data; $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); @@ -80,7 +80,7 @@ class RecurringCronjob extends AbstractCronjob { Log::info(sprintf('Will now fire recurring cron job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); - $job = new CreateRecurringTransactions($this->date); + $job = new CreateRecurringTransactions($this->date); $job->setForce($this->force); $job->handle(); @@ -90,8 +90,8 @@ class RecurringCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Recurring transactions cron job fired successfully.'; - FireflyConfig::set('last_rt_job', (int) $this->date->format('U')); - Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); + FireflyConfig::set('last_rt_job', (int)$this->date->format('U')); + Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U'))); Log::info('Done with recurring cron job task.'); } } diff --git a/app/Support/Cronjobs/UpdateCheckCronjob.php b/app/Support/Cronjobs/UpdateCheckCronjob.php index 6d3cea13ab..c7681037dd 100644 --- a/app/Support/Cronjobs/UpdateCheckCronjob.php +++ b/app/Support/Cronjobs/UpdateCheckCronjob.php @@ -41,8 +41,8 @@ class UpdateCheckCronjob extends AbstractCronjob Log::debug('Now in checkForUpdates()'); // should not check for updates: - $permission = FireflyConfig::get('permission_update_check', -1); - $value = (int) $permission->data; + $permission = FireflyConfig::get('permission_update_check', -1); + $value = (int)$permission->data; if (1 !== $value) { Log::debug('Update check is not enabled.'); // get stuff from job: @@ -56,9 +56,9 @@ class UpdateCheckCronjob extends AbstractCronjob // TODO this is duplicate. /** @var Configuration $lastCheckTime */ - $lastCheckTime = FireflyConfig::get('last_update_check', Carbon::now()->getTimestamp()); - $now = Carbon::now()->getTimestamp(); - $diff = $now - $lastCheckTime->data; + $lastCheckTime = FireflyConfig::get('last_update_check', Carbon::now()->getTimestamp()); + $now = Carbon::now()->getTimestamp(); + $diff = $now - $lastCheckTime->data; Log::debug(sprintf('Last check time is %d, current time is %d, difference is %d', $lastCheckTime->data, $now, $diff)); if ($diff < 604800 && false === $this->force) { // get stuff from job: @@ -71,7 +71,7 @@ class UpdateCheckCronjob extends AbstractCronjob } // last check time was more than a week ago. Log::debug('Have not checked for a new version in a week!'); - $release = $this->getLatestRelease(); + $release = $this->getLatestRelease(); if ('error' === $release['level']) { // get stuff from job: $this->jobFired = true; diff --git a/app/Support/Cronjobs/WebhookCronjob.php b/app/Support/Cronjobs/WebhookCronjob.php index 0cd1899380..84ea6676f5 100644 --- a/app/Support/Cronjobs/WebhookCronjob.php +++ b/app/Support/Cronjobs/WebhookCronjob.php @@ -45,7 +45,7 @@ class WebhookCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_webhook_job', 0); - $lastTime = (int) $config->data; + $lastTime = (int)$config->data; $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); @@ -90,8 +90,8 @@ class WebhookCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Send webhook messages cron job fired successfully.'; - FireflyConfig::set('last_webhook_job', (int) $this->date->format('U')); - Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); + FireflyConfig::set('last_webhook_job', (int)$this->date->format('U')); + Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U'))); Log::info('Done with webhook cron job task.'); } } diff --git a/app/Support/Debug/Timer.php b/app/Support/Debug/Timer.php index 94e48187b7..b23e941a0c 100644 --- a/app/Support/Debug/Timer.php +++ b/app/Support/Debug/Timer.php @@ -28,8 +28,8 @@ use Illuminate\Support\Facades\Log; class Timer { - private array $times = []; private static ?Timer $instance = null; + private array $times = []; private function __construct() { diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index e460353ac2..1dbaeb7d8e 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -23,9 +23,9 @@ declare(strict_types=1); namespace FireflyIII\Support; -use Illuminate\Database\Eloquent\Model; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Support\Form\FormSupport; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Throwable; @@ -43,7 +43,7 @@ class ExpandedForm */ public function amountNoCurrency(string $name, $value = null, ?array $options = null): string { - $options ??= []; + $options ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); @@ -74,8 +74,8 @@ class ExpandedForm */ public function checkbox(string $name, ?int $value = null, $checked = null, ?array $options = null): string { - $options ??= []; - $value ??= 1; + $options ??= []; + $value ??= 1; $options['checked'] = true === $checked; if (app('session')->has('preFilled')) { @@ -83,10 +83,10 @@ class ExpandedForm $options['checked'] = $preFilled[$name] ?? $options['checked']; } - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); unset($options['placeholder'], $options['autocomplete'], $options['class']); @@ -157,10 +157,10 @@ class ExpandedForm public function integer(string $name, $value = null, ?array $options = null): string { $options ??= []; - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); $options['step'] ??= '1'; try { @@ -209,9 +209,9 @@ class ExpandedForm /** @var Model $entry */ foreach ($set as $entry) { // All Eloquent models have an ID - $entryId = $entry->id; - $current = $entry->toArray(); - $title = null; + $entryId = $entry->id; + $current = $entry->toArray(); + $title = null; foreach ($fields as $field) { if (array_key_exists($field, $current) && null === $title) { $title = $current[$field]; diff --git a/app/Support/Export/ExportDataGenerator.php b/app/Support/Export/ExportDataGenerator.php index d4a44e4e6a..b70f2a6615 100644 --- a/app/Support/Export/ExportDataGenerator.php +++ b/app/Support/Export/ExportDataGenerator.php @@ -85,12 +85,12 @@ class ExportDataGenerator private bool $exportTransactions; private Carbon $start; private User $user; - private UserGroup $userGroup; // @phpstan-ignore-line + private UserGroup $userGroup; // @phpstan-ignore-line public function __construct() { - $this->accounts = new Collection(); - $this->start = today(config('app.timezone')); + $this->accounts = new Collection(); + $this->start = today(config('app.timezone')); $this->start->subYear(); $this->end = today(config('app.timezone')); $this->exportTransactions = false; @@ -141,453 +141,6 @@ class ExportDataGenerator return $return; } - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportAccounts(): string - { - $header = [ - 'user_id', - 'account_id', - 'created_at', - 'updated_at', - 'type', - 'name', - 'virtual_balance', - 'iban', - 'number', - 'active', - 'currency_code', - 'role', - 'cc_type', - 'cc_payment_date', - 'in_net_worth', - 'interest', - 'interest_period', - ]; - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - $allAccounts = $repository->getAccountsByType([]); - $records = []; - - /** @var Account $account */ - foreach ($allAccounts as $account) { - $currency = $repository->getAccountCurrency($account); - $records[] = [ - $this->user->id, - $account->id, - $account->created_at->toAtomString(), - $account->updated_at->toAtomString(), - $account->accountType->type, - $account->name, - $account->virtual_balance, - $account->iban, - $account->account_number, - $account->active, - $currency?->code, - $repository->getMetaValue($account, 'account_role'), - $repository->getMetaValue($account, 'cc_type'), - $repository->getMetaValue($account, 'cc_monthly_payment_date'), - $repository->getMetaValue($account, 'include_net_worth'), - $repository->getMetaValue($account, 'interest'), - $repository->getMetaValue($account, 'interest_period'), - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - public function setUser(User $user): void - { - $this->user = $user; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportBills(): string - { - /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class); - $repository->setUser($this->user); - $bills = $repository->getBills(); - $header = [ - 'user_id', - 'bill_id', - 'created_at', - 'updated_at', - 'currency_code', - 'name', - 'amount_min', - 'amount_max', - 'date', - 'repeat_freq', - 'skip', - 'active', - ]; - $records = []; - - /** @var Bill $bill */ - foreach ($bills as $bill) { - $records[] = [ - $this->user->id, - $bill->id, - $bill->created_at->toAtomString(), - $bill->updated_at->toAtomString(), - $bill->transactionCurrency->code, - $bill->name, - $bill->amount_min, - $bill->amount_max, - $bill->date->format('Y-m-d'), - $bill->repeat_freq, - $bill->skip, - $bill->active, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportBudgets(): string - { - $header = [ - 'user_id', - 'budget_id', - 'name', - 'active', - 'order', - 'start_date', - 'end_date', - 'currency_code', - 'amount', - ]; - - $budgetRepos = app(BudgetRepositoryInterface::class); - $budgetRepos->setUser($this->user); - $limitRepos = app(BudgetLimitRepositoryInterface::class); - $budgets = $budgetRepos->getBudgets(); - $records = []; - - /** @var Budget $budget */ - foreach ($budgets as $budget) { - $limits = $limitRepos->getBudgetLimits($budget); - - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $records[] = [ - $this->user->id, - $budget->id, - $budget->name, - $budget->active, - $budget->order, - $limit->start_date->format('Y-m-d'), - $limit->end_date->format('Y-m-d'), - $limit->transactionCurrency->code, - $limit->amount, - ]; - } - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportCategories(): string - { - $header = ['user_id', 'category_id', 'created_at', 'updated_at', 'name']; - - /** @var CategoryRepositoryInterface $catRepos */ - $catRepos = app(CategoryRepositoryInterface::class); - $catRepos->setUser($this->user); - - $records = []; - $categories = $catRepos->getCategories(); - - /** @var Category $category */ - foreach ($categories as $category) { - $records[] = [ - $this->user->id, - $category->id, - $category->created_at->toAtomString(), - $category->updated_at->toAtomString(), - $category->name, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportPiggies(): string - { - /** @var PiggyBankRepositoryInterface $piggyRepos */ - $piggyRepos = app(PiggyBankRepositoryInterface::class); - $piggyRepos->setUser($this->user); - - /** @var AccountRepositoryInterface $accountRepos */ - $accountRepos = app(AccountRepositoryInterface::class); - $accountRepos->setUser($this->user); - - $header = [ - 'user_id', - 'piggy_bank_id', - 'created_at', - 'updated_at', - 'account_name', - 'account_type', - 'name', - 'currency_code', - 'target_amount', - 'current_amount', - 'start_date', - 'target_date', - 'order', - 'active', - ]; - $records = []; - $piggies = $piggyRepos->getPiggyBanks(); - - /** @var PiggyBank $piggy */ - foreach ($piggies as $piggy) { - $repetition = $piggyRepos->getRepetition($piggy); - $currency = $accountRepos->getAccountCurrency($piggy->account); - $records[] = [ - $this->user->id, - $piggy->id, - $piggy->created_at->toAtomString(), - $piggy->updated_at->toAtomString(), - $piggy->account->name, - $piggy->account->accountType->type, - $piggy->name, - $currency?->code, - $piggy->target_amount, - $repetition?->current_amount, - $piggy->start_date?->format('Y-m-d'), - $piggy->target_date?->format('Y-m-d'), - $piggy->order, - $piggy->active, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportRecurring(): string - { - /** @var RecurringRepositoryInterface $recurringRepos */ - $recurringRepos = app(RecurringRepositoryInterface::class); - $recurringRepos->setUser($this->user); - $header = [ - // recurrence: - 'user_id', 'recurrence_id', 'row_contains', 'created_at', 'updated_at', 'type', 'title', 'description', 'first_date', 'repeat_until', 'latest_date', 'repetitions', 'apply_rules', 'active', - - // repetition info: - 'type', 'moment', 'skip', 'weekend', - // transactions + meta: - 'currency_code', 'foreign_currency_code', 'source_name', 'source_type', 'destination_name', 'destination_type', 'amount', 'foreign_amount', 'category', 'budget', 'piggy_bank', 'tags', - ]; - $records = []; - $recurrences = $recurringRepos->get(); - - /** @var Recurrence $recurrence */ - foreach ($recurrences as $recurrence) { - // add recurrence: - $records[] = [ - $this->user->id, $recurrence->id, - 'recurrence', - $recurrence->created_at->toAtomString(), $recurrence->updated_at->toAtomString(), $recurrence->transactionType->type, $recurrence->title, $recurrence->description, $recurrence->first_date?->format('Y-m-d'), $recurrence->repeat_until?->format('Y-m-d'), $recurrence->latest_date?->format('Y-m-d'), $recurrence->repetitions, $recurrence->apply_rules, $recurrence->active, - ]; - - // add new row for each repetition - /** @var RecurrenceRepetition $repetition */ - foreach ($recurrence->recurrenceRepetitions as $repetition) { - $records[] = [ - // recurrence - $this->user->id, - $recurrence->id, - 'repetition', - null, null, null, null, null, null, null, null, null, null, null, - - // repetition: - $repetition->repetition_type, $repetition->repetition_moment, $repetition->repetition_skip, $repetition->weekend, - ]; - } - - /** @var RecurrenceTransaction $transaction */ - foreach ($recurrence->recurrenceTransactions as $transaction) { - $categoryName = $recurringRepos->getCategoryName($transaction); - $budgetId = $recurringRepos->getBudget($transaction); - $piggyBankId = $recurringRepos->getPiggyBank($transaction); - $tags = $recurringRepos->getTags($transaction); - - $records[] = [ - // recurrence - $this->user->id, - $recurrence->id, - 'transaction', - null, null, null, null, null, null, null, null, null, null, null, - - // repetition: - null, null, null, null, - - // transaction: - $transaction->transactionCurrency->code, $transaction->foreignCurrency?->code, $transaction->sourceAccount->name, $transaction->sourceAccount->accountType->type, $transaction->destinationAccount->name, $transaction->destinationAccount->accountType->type, $transaction->amount, $transaction->foreign_amount, $categoryName, $budgetId, $piggyBankId, implode(',', $tags), - ]; - } - } - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - /** * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ @@ -596,256 +149,6 @@ class ExportDataGenerator return null; } - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportRules(): string - { - $header = [ - 'user_id', 'rule_id', 'row_contains', - 'created_at', 'updated_at', 'group_id', 'title', 'description', 'order', 'active', 'stop_processing', 'strict', - 'trigger_type', 'trigger_value', 'trigger_order', 'trigger_active', 'trigger_stop_processing', - 'action_type', 'action_value', 'action_order', 'action_active', 'action_stop_processing']; - $ruleRepos = app(RuleRepositoryInterface::class); - $ruleRepos->setUser($this->user); - $rules = $ruleRepos->getAll(); - $records = []; - - /** @var Rule $rule */ - foreach ($rules as $rule) { - $entry = [ - $this->user->id, $rule->id, - 'rule', - $rule->created_at->toAtomString(), $rule->updated_at->toAtomString(), $rule->ruleGroup->id, $rule->ruleGroup->title, $rule->title, $rule->description, $rule->order, $rule->active, $rule->stop_processing, $rule->strict, - null, null, null, null, null, null, null, null, null, - ]; - $records[] = $entry; - - /** @var RuleTrigger $trigger */ - foreach ($rule->ruleTriggers as $trigger) { - $entry = [ - $this->user->id, - $rule->id, - 'trigger', - null, null, null, null, null, null, null, null, null, - $trigger->trigger_type, $trigger->trigger_value, $trigger->order, $trigger->active, $trigger->stop_processing, - null, null, null, null, null, - ]; - $records[] = $entry; - } - - /** @var RuleAction $action */ - foreach ($rule->ruleActions as $action) { - $entry = [ - $this->user->id, - $rule->id, - 'action', - null, null, null, null, null, null, null, null, null, null, null, null, null, null, - $action->action_type, $action->action_value, $action->order, $action->active, $action->stop_processing, - ]; - $records[] = $entry; - } - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportTags(): string - { - $header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description', 'latitude', 'longitude', 'zoom_level']; - - $tagRepos = app(TagRepositoryInterface::class); - $tagRepos->setUser($this->user); - $tags = $tagRepos->get(); - $records = []; - - /** @var Tag $tag */ - foreach ($tags as $tag) { - $records[] = [ - $this->user->id, - $tag->id, - $tag->created_at->toAtomString(), - $tag->updated_at->toAtomString(), - $tag->tag, - $tag->date?->format('Y-m-d'), - $tag->description, - $tag->latitude, - $tag->longitude, - $tag->zoomLevel, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportTransactions(): string - { - Log::debug('Will now export transactions.'); - // TODO better place for keys? - $header = ['user_id', 'group_id', 'journal_id', 'created_at', 'updated_at', 'group_title', 'type', 'currency_code', 'amount', 'foreign_currency_code', 'foreign_amount', 'primary_currency_code', 'pc_amount', 'pc_foreign_amount', 'description', 'date', 'source_name', 'source_iban', 'source_type', 'destination_name', 'destination_iban', 'destination_type', 'reconciled', 'category', 'budget', 'bill', 'tags', 'notes']; - - $metaFields = config('firefly.journal_meta_fields'); - $header = array_merge($header, $metaFields); - $primary = Amount::getPrimaryCurrency(); - - $collector = app(GroupCollectorInterface::class); - $collector->setUser($this->user); - $collector->setRange($this->start, $this->end)->withAccountInformation()->withCategoryInformation()->withBillInformation()->withBudgetInformation()->withTagInformation()->withNotes(); - if (0 !== $this->accounts->count()) { - $collector->setAccounts($this->accounts); - } - - $journals = $collector->getExtractedJournals(); - - // get repository for meta data: - $repository = app(TransactionGroupRepositoryInterface::class); - $repository->setUser($this->user); - - $records = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - $metaData = $repository->getMetaFields($journal['transaction_journal_id'], $metaFields); - $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); - $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); - $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); - $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); - - if (TransactionTypeEnum::WITHDRAWAL->value !== $journal['transaction_type_type']) { - $amount = Steam::bcround(Steam::positive($journal['amount']), $journal['currency_decimal_places']); - $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); - $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_amount']), $primary->decimal_places); - $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_foreign_amount']), $primary->decimal_places); - } - - // opening balance depends on source account type. - if (TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] && AccountTypeEnum::ASSET->value === $journal['source_account_type']) { - $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); - $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); - $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); - $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); - } - - $records[] = [ - $journal['user_id'], $journal['transaction_group_id'], $journal['transaction_journal_id'], $journal['created_at']->toAtomString(), $journal['updated_at']->toAtomString(), $journal['transaction_group_title'], $journal['transaction_type_type'], - // amounts and currencies - $journal['currency_code'], $amount, $journal['foreign_currency_code'], $foreignAmount, $primary->code, $pcAmount, $pcForeignAmount, - - // more fields - $journal['description'], $journal['date']->toAtomString(), $journal['source_account_name'], $journal['source_account_iban'], $journal['source_account_type'], $journal['destination_account_name'], $journal['destination_account_iban'], $journal['destination_account_type'], $journal['reconciled'], $journal['category_name'], $journal['budget_name'], $journal['bill_name'], - $this->mergeTags($journal['tags']), - $this->clearStringKeepNewlines($journal['notes']), - - // sepa - $metaData['sepa_cc'], $metaData['sepa_ct_op'], $metaData['sepa_ct_id'], $metaData['sepa_db'], $metaData['sepa_country'], $metaData['sepa_ep'], $metaData['sepa_ci'], $metaData['sepa_batch_id'], $metaData['external_url'], - - // dates - $metaData['interest_date'], $metaData['book_date'], $metaData['process_date'], $metaData['due_date'], $metaData['payment_date'], $metaData['invoice_date'], - - // others - $metaData['recurrence_id'], $metaData['internal_reference'], $metaData['bunq_payment_id'], $metaData['import_hash'], $metaData['import_hash_v2'], $metaData['external_id'], $metaData['original_source'], - - // recurring transactions - $metaData['recurrence_total'], $metaData['recurrence_count'], - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - public function setAccounts(Collection $accounts): void - { - $this->accounts = $accounts; - } - - private function mergeTags(array $tags): string - { - if (0 === count($tags)) { - return ''; - } - $smol = []; - foreach ($tags as $tag) { - $smol[] = $tag['name']; - } - - return implode(',', $smol); - } - /** * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ @@ -854,6 +157,11 @@ class ExportDataGenerator return null; } + public function setAccounts(Collection $accounts): void + { + $this->accounts = $accounts; + } + public function setEnd(Carbon $end): void { $this->end = $end; @@ -909,8 +217,700 @@ class ExportDataGenerator $this->start = $start; } + public function setUser(User $user): void + { + $this->user = $user; + } + public function setUserGroup(UserGroup $userGroup): void { $this->userGroup = $userGroup; } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportAccounts(): string + { + $header = [ + 'user_id', + 'account_id', + 'created_at', + 'updated_at', + 'type', + 'name', + 'virtual_balance', + 'iban', + 'number', + 'active', + 'currency_code', + 'role', + 'cc_type', + 'cc_payment_date', + 'in_net_worth', + 'interest', + 'interest_period', + ]; + + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); + $allAccounts = $repository->getAccountsByType([]); + $records = []; + + /** @var Account $account */ + foreach ($allAccounts as $account) { + $currency = $repository->getAccountCurrency($account); + $records[] = [ + $this->user->id, + $account->id, + $account->created_at->toAtomString(), + $account->updated_at->toAtomString(), + $account->accountType->type, + $account->name, + $account->virtual_balance, + $account->iban, + $account->account_number, + $account->active, + $currency?->code, + $repository->getMetaValue($account, 'account_role'), + $repository->getMetaValue($account, 'cc_type'), + $repository->getMetaValue($account, 'cc_monthly_payment_date'), + $repository->getMetaValue($account, 'include_net_worth'), + $repository->getMetaValue($account, 'interest'), + $repository->getMetaValue($account, 'interest_period'), + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportBills(): string + { + /** @var BillRepositoryInterface $repository */ + $repository = app(BillRepositoryInterface::class); + $repository->setUser($this->user); + $bills = $repository->getBills(); + $header = [ + 'user_id', + 'bill_id', + 'created_at', + 'updated_at', + 'currency_code', + 'name', + 'amount_min', + 'amount_max', + 'date', + 'repeat_freq', + 'skip', + 'active', + ]; + $records = []; + + /** @var Bill $bill */ + foreach ($bills as $bill) { + $records[] = [ + $this->user->id, + $bill->id, + $bill->created_at->toAtomString(), + $bill->updated_at->toAtomString(), + $bill->transactionCurrency->code, + $bill->name, + $bill->amount_min, + $bill->amount_max, + $bill->date->format('Y-m-d'), + $bill->repeat_freq, + $bill->skip, + $bill->active, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportBudgets(): string + { + $header = [ + 'user_id', + 'budget_id', + 'name', + 'active', + 'order', + 'start_date', + 'end_date', + 'currency_code', + 'amount', + ]; + + $budgetRepos = app(BudgetRepositoryInterface::class); + $budgetRepos->setUser($this->user); + $limitRepos = app(BudgetLimitRepositoryInterface::class); + $budgets = $budgetRepos->getBudgets(); + $records = []; + + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $limits = $limitRepos->getBudgetLimits($budget); + + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $records[] = [ + $this->user->id, + $budget->id, + $budget->name, + $budget->active, + $budget->order, + $limit->start_date->format('Y-m-d'), + $limit->end_date->format('Y-m-d'), + $limit->transactionCurrency->code, + $limit->amount, + ]; + } + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportCategories(): string + { + $header = ['user_id', 'category_id', 'created_at', 'updated_at', 'name']; + + /** @var CategoryRepositoryInterface $catRepos */ + $catRepos = app(CategoryRepositoryInterface::class); + $catRepos->setUser($this->user); + + $records = []; + $categories = $catRepos->getCategories(); + + /** @var Category $category */ + foreach ($categories as $category) { + $records[] = [ + $this->user->id, + $category->id, + $category->created_at->toAtomString(), + $category->updated_at->toAtomString(), + $category->name, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportPiggies(): string + { + /** @var PiggyBankRepositoryInterface $piggyRepos */ + $piggyRepos = app(PiggyBankRepositoryInterface::class); + $piggyRepos->setUser($this->user); + + /** @var AccountRepositoryInterface $accountRepos */ + $accountRepos = app(AccountRepositoryInterface::class); + $accountRepos->setUser($this->user); + + $header = [ + 'user_id', + 'piggy_bank_id', + 'created_at', + 'updated_at', + 'account_name', + 'account_type', + 'name', + 'currency_code', + 'target_amount', + 'current_amount', + 'start_date', + 'target_date', + 'order', + 'active', + ]; + $records = []; + $piggies = $piggyRepos->getPiggyBanks(); + + /** @var PiggyBank $piggy */ + foreach ($piggies as $piggy) { + $repetition = $piggyRepos->getRepetition($piggy); + $currency = $accountRepos->getAccountCurrency($piggy->account); + $records[] = [ + $this->user->id, + $piggy->id, + $piggy->created_at->toAtomString(), + $piggy->updated_at->toAtomString(), + $piggy->account->name, + $piggy->account->accountType->type, + $piggy->name, + $currency?->code, + $piggy->target_amount, + $repetition?->current_amount, + $piggy->start_date?->format('Y-m-d'), + $piggy->target_date?->format('Y-m-d'), + $piggy->order, + $piggy->active, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportRecurring(): string + { + /** @var RecurringRepositoryInterface $recurringRepos */ + $recurringRepos = app(RecurringRepositoryInterface::class); + $recurringRepos->setUser($this->user); + $header = [ + // recurrence: + 'user_id', 'recurrence_id', 'row_contains', 'created_at', 'updated_at', 'type', 'title', 'description', 'first_date', 'repeat_until', 'latest_date', 'repetitions', 'apply_rules', 'active', + + // repetition info: + 'type', 'moment', 'skip', 'weekend', + // transactions + meta: + 'currency_code', 'foreign_currency_code', 'source_name', 'source_type', 'destination_name', 'destination_type', 'amount', 'foreign_amount', 'category', 'budget', 'piggy_bank', 'tags', + ]; + $records = []; + $recurrences = $recurringRepos->get(); + + /** @var Recurrence $recurrence */ + foreach ($recurrences as $recurrence) { + // add recurrence: + $records[] = [ + $this->user->id, $recurrence->id, + 'recurrence', + $recurrence->created_at->toAtomString(), $recurrence->updated_at->toAtomString(), $recurrence->transactionType->type, $recurrence->title, $recurrence->description, $recurrence->first_date?->format('Y-m-d'), $recurrence->repeat_until?->format('Y-m-d'), $recurrence->latest_date?->format('Y-m-d'), $recurrence->repetitions, $recurrence->apply_rules, $recurrence->active, + ]; + + // add new row for each repetition + /** @var RecurrenceRepetition $repetition */ + foreach ($recurrence->recurrenceRepetitions as $repetition) { + $records[] = [ + // recurrence + $this->user->id, + $recurrence->id, + 'repetition', + null, null, null, null, null, null, null, null, null, null, null, + + // repetition: + $repetition->repetition_type, $repetition->repetition_moment, $repetition->repetition_skip, $repetition->weekend, + ]; + } + + /** @var RecurrenceTransaction $transaction */ + foreach ($recurrence->recurrenceTransactions as $transaction) { + $categoryName = $recurringRepos->getCategoryName($transaction); + $budgetId = $recurringRepos->getBudget($transaction); + $piggyBankId = $recurringRepos->getPiggyBank($transaction); + $tags = $recurringRepos->getTags($transaction); + + $records[] = [ + // recurrence + $this->user->id, + $recurrence->id, + 'transaction', + null, null, null, null, null, null, null, null, null, null, null, + + // repetition: + null, null, null, null, + + // transaction: + $transaction->transactionCurrency->code, $transaction->foreignCurrency?->code, $transaction->sourceAccount->name, $transaction->sourceAccount->accountType->type, $transaction->destinationAccount->name, $transaction->destinationAccount->accountType->type, $transaction->amount, $transaction->foreign_amount, $categoryName, $budgetId, $piggyBankId, implode(',', $tags), + ]; + } + } + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportRules(): string + { + $header = [ + 'user_id', 'rule_id', 'row_contains', + 'created_at', 'updated_at', 'group_id', 'title', 'description', 'order', 'active', 'stop_processing', 'strict', + 'trigger_type', 'trigger_value', 'trigger_order', 'trigger_active', 'trigger_stop_processing', + 'action_type', 'action_value', 'action_order', 'action_active', 'action_stop_processing']; + $ruleRepos = app(RuleRepositoryInterface::class); + $ruleRepos->setUser($this->user); + $rules = $ruleRepos->getAll(); + $records = []; + + /** @var Rule $rule */ + foreach ($rules as $rule) { + $entry = [ + $this->user->id, $rule->id, + 'rule', + $rule->created_at->toAtomString(), $rule->updated_at->toAtomString(), $rule->ruleGroup->id, $rule->ruleGroup->title, $rule->title, $rule->description, $rule->order, $rule->active, $rule->stop_processing, $rule->strict, + null, null, null, null, null, null, null, null, null, + ]; + $records[] = $entry; + + /** @var RuleTrigger $trigger */ + foreach ($rule->ruleTriggers as $trigger) { + $entry = [ + $this->user->id, + $rule->id, + 'trigger', + null, null, null, null, null, null, null, null, null, + $trigger->trigger_type, $trigger->trigger_value, $trigger->order, $trigger->active, $trigger->stop_processing, + null, null, null, null, null, + ]; + $records[] = $entry; + } + + /** @var RuleAction $action */ + foreach ($rule->ruleActions as $action) { + $entry = [ + $this->user->id, + $rule->id, + 'action', + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + $action->action_type, $action->action_value, $action->order, $action->active, $action->stop_processing, + ]; + $records[] = $entry; + } + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportTags(): string + { + $header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description', 'latitude', 'longitude', 'zoom_level']; + + $tagRepos = app(TagRepositoryInterface::class); + $tagRepos->setUser($this->user); + $tags = $tagRepos->get(); + $records = []; + + /** @var Tag $tag */ + foreach ($tags as $tag) { + $records[] = [ + $this->user->id, + $tag->id, + $tag->created_at->toAtomString(), + $tag->updated_at->toAtomString(), + $tag->tag, + $tag->date?->format('Y-m-d'), + $tag->description, + $tag->latitude, + $tag->longitude, + $tag->zoomLevel, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportTransactions(): string + { + Log::debug('Will now export transactions.'); + // TODO better place for keys? + $header = ['user_id', 'group_id', 'journal_id', 'created_at', 'updated_at', 'group_title', 'type', 'currency_code', 'amount', 'foreign_currency_code', 'foreign_amount', 'primary_currency_code', 'pc_amount', 'pc_foreign_amount', 'description', 'date', 'source_name', 'source_iban', 'source_type', 'destination_name', 'destination_iban', 'destination_type', 'reconciled', 'category', 'budget', 'bill', 'tags', 'notes']; + + $metaFields = config('firefly.journal_meta_fields'); + $header = array_merge($header, $metaFields); + $primary = Amount::getPrimaryCurrency(); + + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($this->start, $this->end)->withAccountInformation()->withCategoryInformation()->withBillInformation()->withBudgetInformation()->withTagInformation()->withNotes(); + if (0 !== $this->accounts->count()) { + $collector->setAccounts($this->accounts); + } + + $journals = $collector->getExtractedJournals(); + + // get repository for meta data: + $repository = app(TransactionGroupRepositoryInterface::class); + $repository->setUser($this->user); + + $records = []; + + /** @var array $journal */ + foreach ($journals as $journal) { + $metaData = $repository->getMetaFields($journal['transaction_journal_id'], $metaFields); + $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); + $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); + $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); + $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); + + if (TransactionTypeEnum::WITHDRAWAL->value !== $journal['transaction_type_type']) { + $amount = Steam::bcround(Steam::positive($journal['amount']), $journal['currency_decimal_places']); + $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); + $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_amount']), $primary->decimal_places); + $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_foreign_amount']), $primary->decimal_places); + } + + // opening balance depends on source account type. + if (TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] && AccountTypeEnum::ASSET->value === $journal['source_account_type']) { + $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); + $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); + $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); + $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); + } + + $records[] = [ + $journal['user_id'], $journal['transaction_group_id'], $journal['transaction_journal_id'], $journal['created_at']->toAtomString(), $journal['updated_at']->toAtomString(), $journal['transaction_group_title'], $journal['transaction_type_type'], + // amounts and currencies + $journal['currency_code'], $amount, $journal['foreign_currency_code'], $foreignAmount, $primary->code, $pcAmount, $pcForeignAmount, + + // more fields + $journal['description'], $journal['date']->toAtomString(), $journal['source_account_name'], $journal['source_account_iban'], $journal['source_account_type'], $journal['destination_account_name'], $journal['destination_account_iban'], $journal['destination_account_type'], $journal['reconciled'], $journal['category_name'], $journal['budget_name'], $journal['bill_name'], + $this->mergeTags($journal['tags']), + $this->clearStringKeepNewlines($journal['notes']), + + // sepa + $metaData['sepa_cc'], $metaData['sepa_ct_op'], $metaData['sepa_ct_id'], $metaData['sepa_db'], $metaData['sepa_country'], $metaData['sepa_ep'], $metaData['sepa_ci'], $metaData['sepa_batch_id'], $metaData['external_url'], + + // dates + $metaData['interest_date'], $metaData['book_date'], $metaData['process_date'], $metaData['due_date'], $metaData['payment_date'], $metaData['invoice_date'], + + // others + $metaData['recurrence_id'], $metaData['internal_reference'], $metaData['bunq_payment_id'], $metaData['import_hash'], $metaData['import_hash_v2'], $metaData['external_id'], $metaData['original_source'], + + // recurring transactions + $metaData['recurrence_total'], $metaData['recurrence_count'], + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + private function mergeTags(array $tags): string + { + if (0 === count($tags)) { + return ''; + } + $smol = []; + foreach ($tags as $tag) { + $smol[] = $tag['name']; + } + + return implode(',', $smol); + } } diff --git a/app/Support/FireflyConfig.php b/app/Support/FireflyConfig.php index 499b123602..b79967f8cb 100644 --- a/app/Support/FireflyConfig.php +++ b/app/Support/FireflyConfig.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Support; +use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Configuration; use Illuminate\Contracts\Encryption\DecryptException; @@ -30,7 +31,6 @@ use Illuminate\Contracts\Encryption\EncryptException; use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; -use Exception; /** * Class FireflyConfig. @@ -39,16 +39,43 @@ class FireflyConfig { public function delete(string $name): void { - $fullName = 'ff3-config-'.$name; + $fullName = 'ff3-config-' . $name; if (Cache::has($fullName)) { Cache::forget($fullName); } Configuration::where('name', $name)->forceDelete(); } - public function has(string $name): bool + /** + * @param null|bool|int|string $default + * + * @throws FireflyException + */ + public function get(string $name, mixed $default = null): ?Configuration { - return 1 === Configuration::where('name', $name)->count(); + $fullName = 'ff3-config-' . $name; + if (Cache::has($fullName)) { + return Cache::get($fullName); + } + + try { + /** @var null|Configuration $config */ + $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); + } catch (Exception | QueryException $e) { + throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e); + } + + if (null !== $config) { + Cache::forever($fullName, $config); + + return $config; + } + // no preference found and default is null: + if (null === $default) { + return null; + } + + return $this->set($name, $default); } public function getEncrypted(string $name, mixed $default = null): ?Configuration @@ -74,28 +101,10 @@ class FireflyConfig return $result; } - /** - * @param null|bool|int|string $default - * - * @throws FireflyException - */ - public function get(string $name, mixed $default = null): ?Configuration + public function getFresh(string $name, mixed $default = null): ?Configuration { - $fullName = 'ff3-config-'.$name; - if (Cache::has($fullName)) { - return Cache::get($fullName); - } - - try { - /** @var null|Configuration $config */ - $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); - } catch (Exception|QueryException $e) { - throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e); - } - + $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); if (null !== $config) { - Cache::forever($fullName, $config); - return $config; } // no preference found and default is null: @@ -106,6 +115,19 @@ class FireflyConfig return $this->set($name, $default); } + public function has(string $name): bool + { + return 1 === Configuration::where('name', $name)->count(); + } + + /** + * @param mixed $value + */ + public function put(string $name, $value): Configuration + { + return $this->set($name, $value); + } + public function set(string $name, mixed $value): Configuration { try { @@ -124,39 +146,17 @@ class FireflyConfig $item->name = $name; $item->data = $value; $item->save(); - Cache::forget('ff3-config-'.$name); + Cache::forget('ff3-config-' . $name); return $item; } $config->data = $value; $config->save(); - Cache::forget('ff3-config-'.$name); + Cache::forget('ff3-config-' . $name); return $config; } - public function getFresh(string $name, mixed $default = null): ?Configuration - { - $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); - if (null !== $config) { - return $config; - } - // no preference found and default is null: - if (null === $default) { - return null; - } - - return $this->set($name, $default); - } - - /** - * @param mixed $value - */ - public function put(string $name, $value): Configuration - { - return $this->set($name, $value); - } - public function setEncrypted(string $name, mixed $value): Configuration { try { diff --git a/app/Support/Form/AccountForm.php b/app/Support/Form/AccountForm.php index 19a043c76d..c7a7685061 100644 --- a/app/Support/Form/AccountForm.php +++ b/app/Support/Form/AccountForm.php @@ -51,55 +51,24 @@ class AccountForm $repository = $this->getAccountRepository(); $grouped = $this->getAccountsGrouped($types, $repository); $cash = $repository->getCashAccount(); - $key = (string) trans('firefly.cash_account_type'); - $grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash')); + $key = (string)trans('firefly.cash_account_type'); + $grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash')); return $this->select($name, $grouped, $value, $options); } - private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array - { - if (!$repository instanceof AccountRepositoryInterface) { - $repository = $this->getAccountRepository(); - } - $accountList = $repository->getActiveAccountsByType($types); - $liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value]; - $grouped = []; - - /** @var Account $account */ - foreach ($accountList as $account) { - $role = (string) $repository->getMetaValue($account, 'account_role'); - if (in_array($account->accountType->type, $liabilityTypes, true)) { - $role = sprintf('l_%s', $account->accountType->type); - } - if ('' === $role) { - $role = 'no_account_type'; - if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) { - $role = 'expense_account'; - } - if (AccountTypeEnum::REVENUE->value === $account->accountType->type) { - $role = 'revenue_account'; - } - } - $key = (string) trans(sprintf('firefly.opt_group_%s', $role)); - $grouped[$key][$account->id] = $account->name; - } - - return $grouped; - } - /** * Grouped dropdown list of all accounts that are valid as the destination of a withdrawal. */ public function activeWithdrawalDestinations(string $name, mixed $value = null, ?array $options = null): string { - $types = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::EXPENSE->value]; - $repository = $this->getAccountRepository(); - $grouped = $this->getAccountsGrouped($types, $repository); + $types = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::EXPENSE->value]; + $repository = $this->getAccountRepository(); + $grouped = $this->getAccountsGrouped($types, $repository); $cash = $repository->getCashAccount(); - $key = (string) trans('firefly.cash_account_type'); - $grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash')); + $key = (string)trans('firefly.cash_account_type'); + $grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash')); return $this->select($name, $grouped, $value, $options); } @@ -111,15 +80,15 @@ class AccountForm */ public function assetAccountCheckList(string $name, ?array $options = null): string { - $options ??= []; + $options ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); $selected = request()->old($name) ?? []; // get all asset accounts: - $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value]; - $grouped = $this->getAccountsGrouped($types); + $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value]; + $grouped = $this->getAccountsGrouped($types); unset($options['class']); @@ -173,4 +142,35 @@ class AccountForm return $this->select($name, $grouped, $value, $options); } + + private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array + { + if (!$repository instanceof AccountRepositoryInterface) { + $repository = $this->getAccountRepository(); + } + $accountList = $repository->getActiveAccountsByType($types); + $liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value]; + $grouped = []; + + /** @var Account $account */ + foreach ($accountList as $account) { + $role = (string)$repository->getMetaValue($account, 'account_role'); + if (in_array($account->accountType->type, $liabilityTypes, true)) { + $role = sprintf('l_%s', $account->accountType->type); + } + if ('' === $role) { + $role = 'no_account_type'; + if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) { + $role = 'expense_account'; + } + if (AccountTypeEnum::REVENUE->value === $account->accountType->type) { + $role = 'revenue_account'; + } + } + $key = (string)trans(sprintf('firefly.opt_group_%s', $role)); + $grouped[$key][$account->id] = $account->name; + } + + return $grouped; + } } diff --git a/app/Support/Form/CurrencyForm.php b/app/Support/Form/CurrencyForm.php index 88816667d4..6d24780f19 100644 --- a/app/Support/Form/CurrencyForm.php +++ b/app/Support/Form/CurrencyForm.php @@ -49,60 +49,6 @@ class CurrencyForm return $this->currencyField($name, 'amount', $value, $options); } - /** - * @phpstan-param view-string $view - * - * @throws FireflyException - */ - protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string - { - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); - $options['step'] = 'any'; - $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency(); - - /** @var Collection $currencies */ - $currencies = app('amount')->getCurrencies(); - unset($options['currency'], $options['placeholder']); - // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount) - $preFilled = session('preFilled'); - if (!is_array($preFilled)) { - $preFilled = []; - } - $key = 'amount_currency_id_'.$name; - $sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id; - - app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId)); - - // find this currency in set of currencies: - foreach ($currencies as $currency) { - if ($currency->id === $sentCurrencyId) { - $primaryCurrency = $currency; - app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code)); - - break; - } - } - - // make sure value is formatted nicely: - if (null !== $value && '' !== $value) { - $value = app('steam')->bcround($value, $primaryCurrency->decimal_places); - } - - try { - $html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); - } catch (Throwable $e) { - app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage())); - $html = 'Could not render currencyField.'; - - throw new FireflyException($html, 0, $e); - } - - return $html; - } - /** * TODO describe and cleanup. * @@ -115,6 +61,52 @@ class CurrencyForm return $this->allCurrencyField($name, 'balance', $value, $options); } + /** + * TODO cleanup and describe + * + * @param mixed $value + */ + public function currencyList(string $name, $value = null, ?array $options = null): string + { + /** @var CurrencyRepositoryInterface $currencyRepos */ + $currencyRepos = app(CurrencyRepositoryInterface::class); + + // get all currencies: + $list = $currencyRepos->get(); + $array = []; + + /** @var TransactionCurrency $currency */ + foreach ($list as $currency) { + $array[$currency->id] = $currency->name . ' (' . $currency->symbol . ')'; + } + + return $this->select($name, $array, $value, $options); + } + + /** + * TODO cleanup and describe + * + * @param mixed $value + */ + public function currencyListEmpty(string $name, $value = null, ?array $options = null): string + { + /** @var CurrencyRepositoryInterface $currencyRepos */ + $currencyRepos = app(CurrencyRepositoryInterface::class); + + // get all currencies: + $list = $currencyRepos->get(); + $array = [ + 0 => (string)trans('firefly.no_currency'), + ]; + + /** @var TransactionCurrency $currency */ + foreach ($list as $currency) { + $array[$currency->id] = $currency->name . ' (' . $currency->symbol . ')'; + } + + return $this->select($name, $array, $value, $options); + } + /** * TODO describe and cleanup * @@ -132,16 +124,16 @@ class CurrencyForm $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency(); /** @var Collection $currencies */ - $currencies = app('amount')->getAllCurrencies(); + $currencies = app('amount')->getAllCurrencies(); unset($options['currency'], $options['placeholder']); // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount) - $preFilled = session('preFilled'); + $preFilled = session('preFilled'); if (!is_array($preFilled)) { $preFilled = []; } - $key = 'amount_currency_id_'.$name; - $sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id; + $key = 'amount_currency_id_' . $name; + $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id; app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId)); @@ -161,7 +153,7 @@ class CurrencyForm } try { - $html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); + $html = view('form.' . $view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); } catch (Throwable $e) { app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage())); $html = 'Could not render currencyField.'; @@ -173,48 +165,56 @@ class CurrencyForm } /** - * TODO cleanup and describe + * @phpstan-param view-string $view * - * @param mixed $value + * @throws FireflyException */ - public function currencyList(string $name, $value = null, ?array $options = null): string + protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string { - /** @var CurrencyRepositoryInterface $currencyRepos */ - $currencyRepos = app(CurrencyRepositoryInterface::class); + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); + $options['step'] = 'any'; + $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency(); - // get all currencies: - $list = $currencyRepos->get(); - $array = []; + /** @var Collection $currencies */ + $currencies = app('amount')->getCurrencies(); + unset($options['currency'], $options['placeholder']); + // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount) + $preFilled = session('preFilled'); + if (!is_array($preFilled)) { + $preFilled = []; + } + $key = 'amount_currency_id_' . $name; + $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id; - /** @var TransactionCurrency $currency */ - foreach ($list as $currency) { - $array[$currency->id] = $currency->name.' ('.$currency->symbol.')'; + app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId)); + + // find this currency in set of currencies: + foreach ($currencies as $currency) { + if ($currency->id === $sentCurrencyId) { + $primaryCurrency = $currency; + app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code)); + + break; + } } - return $this->select($name, $array, $value, $options); - } - - /** - * TODO cleanup and describe - * - * @param mixed $value - */ - public function currencyListEmpty(string $name, $value = null, ?array $options = null): string - { - /** @var CurrencyRepositoryInterface $currencyRepos */ - $currencyRepos = app(CurrencyRepositoryInterface::class); - - // get all currencies: - $list = $currencyRepos->get(); - $array = [ - 0 => (string) trans('firefly.no_currency'), - ]; - - /** @var TransactionCurrency $currency */ - foreach ($list as $currency) { - $array[$currency->id] = $currency->name.' ('.$currency->symbol.')'; + // make sure value is formatted nicely: + if (null !== $value && '' !== $value) { + $value = app('steam')->bcround($value, $primaryCurrency->decimal_places); } - return $this->select($name, $array, $value, $options); + try { + $html = view('form.' . $view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); + } catch (Throwable $e) { + app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage())); + $html = 'Could not render currencyField.'; + + throw new FireflyException($html, 0, $e); + } + + return $html; } } diff --git a/app/Support/Form/FormSupport.php b/app/Support/Form/FormSupport.php index 4bcc0fcb87..e41c785f00 100644 --- a/app/Support/Form/FormSupport.php +++ b/app/Support/Form/FormSupport.php @@ -36,7 +36,7 @@ trait FormSupport { public function multiSelect(string $name, ?array $list = null, mixed $selected = null, ?array $options = null): string { - $list ??= []; + $list ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); @@ -54,15 +54,26 @@ trait FormSupport return $html; } - protected function label(string $name, ?array $options = null): string + /** + * @param mixed $selected + */ + public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string { - $options ??= []; - if (array_key_exists('label', $options)) { - return $options['label']; - } - $name = str_replace('[]', '', $name); + $list ??= []; + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $selected = $this->fillFieldValue($name, $selected); + unset($options['autocomplete'], $options['placeholder']); - return (string)trans('form.'.$name); + try { + $html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); + } catch (Throwable $e) { + app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage())); + $html = 'Could not render select.'; + } + + return $html; } /** @@ -70,29 +81,16 @@ trait FormSupport */ protected function expandOptionArray(string $name, $label, ?array $options = null): array { - $options ??= []; + $options ??= []; $name = str_replace('[]', '', $name); $options['class'] = 'form-control'; - $options['id'] = 'ffInput_'.$name; + $options['id'] = 'ffInput_' . $name; $options['autocomplete'] = 'off'; $options['placeholder'] = ucfirst((string)$label); return $options; } - protected function getHolderClasses(string $name): string - { - // Get errors from session: - /** @var null|MessageBag $errors */ - $errors = session('errors'); - - if (null !== $errors && $errors->has($name)) { - return 'form-group has-error has-feedback'; - } - - return 'form-group'; - } - /** * @param null|mixed $value * @@ -116,28 +114,6 @@ trait FormSupport return $value; } - /** - * @param mixed $selected - */ - public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string - { - $list ??= []; - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $selected = $this->fillFieldValue($name, $selected); - unset($options['autocomplete'], $options['placeholder']); - - try { - $html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); - } catch (Throwable $e) { - app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage())); - $html = 'Could not render select.'; - } - - return $html; - } - protected function getAccountRepository(): AccountRepositoryInterface { return app(AccountRepositoryInterface::class); @@ -147,4 +123,28 @@ trait FormSupport { return today(config('app.timezone')); } + + protected function getHolderClasses(string $name): string + { + // Get errors from session: + /** @var null|MessageBag $errors */ + $errors = session('errors'); + + if (null !== $errors && $errors->has($name)) { + return 'form-group has-error has-feedback'; + } + + return 'form-group'; + } + + protected function label(string $name, ?array $options = null): string + { + $options ??= []; + if (array_key_exists('label', $options)) { + return $options['label']; + } + $name = str_replace('[]', '', $name); + + return (string)trans('form.' . $name); + } } diff --git a/app/Support/Form/PiggyBankForm.php b/app/Support/Form/PiggyBankForm.php index 78919b30bf..b6233fd0d6 100644 --- a/app/Support/Form/PiggyBankForm.php +++ b/app/Support/Form/PiggyBankForm.php @@ -47,7 +47,7 @@ class PiggyBankForm /** @var PiggyBankRepositoryInterface $repository */ $repository = app(PiggyBankRepositoryInterface::class); $piggyBanks = $repository->getPiggyBanksWithAmount(); - $title = (string) trans('firefly.default_group_title_name'); + $title = (string)trans('firefly.default_group_title_name'); $array = []; $subList = [ 0 => [ @@ -55,21 +55,21 @@ class PiggyBankForm 'title' => $title, ], 'piggies' => [ - (string) trans('firefly.none_in_select_list'), + (string)trans('firefly.none_in_select_list'), ], ], ]; /** @var PiggyBank $piggy */ foreach ($piggyBanks as $piggy) { - $group = $piggy->objectGroups->first(); - $groupTitle = null; - $groupOrder = 0; + $group = $piggy->objectGroups->first(); + $groupTitle = null; + $groupOrder = 0; if (null !== $group) { $groupTitle = $group->title; $groupOrder = $group->order; } - $subList[$groupOrder] ??= [ + $subList[$groupOrder] ??= [ 'group' => [ 'title' => $groupTitle, ], diff --git a/app/Support/Form/RuleForm.php b/app/Support/Form/RuleForm.php index 6baee553be..f635ed9b86 100644 --- a/app/Support/Form/RuleForm.php +++ b/app/Support/Form/RuleForm.php @@ -41,8 +41,8 @@ class RuleForm $groupRepos = app(RuleGroupRepositoryInterface::class); // get all currencies: - $list = $groupRepos->get(); - $array = []; + $list = $groupRepos->get(); + $array = []; /** @var RuleGroup $group */ foreach ($list as $group) { @@ -57,21 +57,21 @@ class RuleForm */ public function ruleGroupListWithEmpty(string $name, $value = null, ?array $options = null): string { - $options ??= []; + $options ??= []; $options['class'] = 'form-control'; /** @var RuleGroupRepositoryInterface $groupRepos */ - $groupRepos = app(RuleGroupRepositoryInterface::class); + $groupRepos = app(RuleGroupRepositoryInterface::class); // get all currencies: - $list = $groupRepos->get(); - $array = [ - 0 => (string) trans('firefly.none_in_select_list'), + $list = $groupRepos->get(); + $array = [ + 0 => (string)trans('firefly.none_in_select_list'), ]; /** @var RuleGroup $group */ foreach ($list as $group) { - if (array_key_exists('hidden', $options) && (int) $options['hidden'] !== $group->id) { + if (array_key_exists('hidden', $options) && (int)$options['hidden'] !== $group->id) { $array[$group->id] = $group->title; } } diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index d1f7915d23..4c60e00870 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -146,114 +146,6 @@ class AccountBalanceGrouped $converter->summarize(); } - private function processJournal(array $journal): void - { - // format the date according to the period - $period = $journal['date']->format($this->carbonFormat); - $currencyId = (int)$journal['currency_id']; - $currency = $this->findCurrency($currencyId); - - // set the array with monetary info, if it does not exist. - $this->createDefaultDataEntry($journal); - // set the array (in monetary info) with spent/earned in this $period, if it does not exist. - $this->createDefaultPeriodEntry($journal); - - // is this journal's amount in- our outgoing? - $key = $this->getDataKey($journal); - $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']); - - // get conversion rate - $rate = $this->getRate($currency, $journal['date']); - $amountConverted = bcmul($amount, $rate); - - // perhaps transaction already has the foreign amount in the primary currency. - if ((int)$journal['foreign_currency_id'] === $this->primary->id) { - $amountConverted = $journal['foreign_amount'] ?? '0'; - $amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted); - } - - // add normal entry - $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount); - - // add converted entry - $convertedKey = sprintf('pc_%s', $key); - $this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted); - } - - private function findCurrency(int $currencyId): TransactionCurrency - { - if (array_key_exists($currencyId, $this->currencies)) { - return $this->currencies[$currencyId]; - } - $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId); - - return $this->currencies[$currencyId]; - } - - private function createDefaultDataEntry(array $journal): void - { - $currencyId = (int)$journal['currency_id']; - $this->data[$currencyId] ??= [ - 'currency_id' => (string)$currencyId, - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_name' => $journal['currency_name'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - // primary currency info (could be the same) - 'primary_currency_id' => (string)$this->primary->id, - 'primary_currency_code' => $this->primary->code, - 'primary_currency_symbol' => $this->primary->symbol, - 'primary_currency_decimal_places' => $this->primary->decimal_places, - ]; - } - - private function createDefaultPeriodEntry(array $journal): void - { - $currencyId = (int)$journal['currency_id']; - $period = $journal['date']->format($this->carbonFormat); - $this->data[$currencyId][$period] ??= [ - 'period' => $period, - 'spent' => '0', - 'earned' => '0', - 'pc_spent' => '0', - 'pc_earned' => '0', - ]; - } - - private function getDataKey(array $journal): string - { - // deposit = incoming - // transfer or reconcile or opening balance, and these accounts are the destination. - if ( - TransactionTypeEnum::DEPOSIT->value === $journal['transaction_type_type'] - - || ( - ( - TransactionTypeEnum::TRANSFER->value === $journal['transaction_type_type'] - || TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type'] - || TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] - ) - && in_array($journal['destination_account_id'], $this->accountIds, true) - ) - ) { - return 'earned'; - } - - return 'spent'; - } - - private function getRate(TransactionCurrency $currency, Carbon $date): string - { - try { - $rate = $this->converter->getCurrencyRate($currency, $this->primary, $date); - } catch (FireflyException $e) { - app('log')->error($e->getMessage()); - $rate = '1'; - } - - return $rate; - } - public function setAccounts(Collection $accounts): void { $this->accountIds = $accounts->pluck('id')->toArray(); @@ -298,4 +190,112 @@ class AccountBalanceGrouped { $this->start = $start; } + + private function createDefaultDataEntry(array $journal): void + { + $currencyId = (int)$journal['currency_id']; + $this->data[$currencyId] ??= [ + 'currency_id' => (string)$currencyId, + 'currency_symbol' => $journal['currency_symbol'], + 'currency_code' => $journal['currency_code'], + 'currency_name' => $journal['currency_name'], + 'currency_decimal_places' => $journal['currency_decimal_places'], + // primary currency info (could be the same) + 'primary_currency_id' => (string)$this->primary->id, + 'primary_currency_code' => $this->primary->code, + 'primary_currency_symbol' => $this->primary->symbol, + 'primary_currency_decimal_places' => $this->primary->decimal_places, + ]; + } + + private function createDefaultPeriodEntry(array $journal): void + { + $currencyId = (int)$journal['currency_id']; + $period = $journal['date']->format($this->carbonFormat); + $this->data[$currencyId][$period] ??= [ + 'period' => $period, + 'spent' => '0', + 'earned' => '0', + 'pc_spent' => '0', + 'pc_earned' => '0', + ]; + } + + private function findCurrency(int $currencyId): TransactionCurrency + { + if (array_key_exists($currencyId, $this->currencies)) { + return $this->currencies[$currencyId]; + } + $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId); + + return $this->currencies[$currencyId]; + } + + private function getDataKey(array $journal): string + { + // deposit = incoming + // transfer or reconcile or opening balance, and these accounts are the destination. + if ( + TransactionTypeEnum::DEPOSIT->value === $journal['transaction_type_type'] + + || ( + ( + TransactionTypeEnum::TRANSFER->value === $journal['transaction_type_type'] + || TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type'] + || TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] + ) + && in_array($journal['destination_account_id'], $this->accountIds, true) + ) + ) { + return 'earned'; + } + + return 'spent'; + } + + private function getRate(TransactionCurrency $currency, Carbon $date): string + { + try { + $rate = $this->converter->getCurrencyRate($currency, $this->primary, $date); + } catch (FireflyException $e) { + app('log')->error($e->getMessage()); + $rate = '1'; + } + + return $rate; + } + + private function processJournal(array $journal): void + { + // format the date according to the period + $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $currency = $this->findCurrency($currencyId); + + // set the array with monetary info, if it does not exist. + $this->createDefaultDataEntry($journal); + // set the array (in monetary info) with spent/earned in this $period, if it does not exist. + $this->createDefaultPeriodEntry($journal); + + // is this journal's amount in- our outgoing? + $key = $this->getDataKey($journal); + $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']); + + // get conversion rate + $rate = $this->getRate($currency, $journal['date']); + $amountConverted = bcmul($amount, $rate); + + // perhaps transaction already has the foreign amount in the primary currency. + if ((int)$journal['foreign_currency_id'] === $this->primary->id) { + $amountConverted = $journal['foreign_amount'] ?? '0'; + $amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted); + } + + // add normal entry + $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount); + + // add converted entry + $convertedKey = sprintf('pc_%s', $key); + $this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted); + } } diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index a3d54974cf..aef2ec443f 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -43,7 +43,7 @@ trait CleansChartData $return = []; /** - * @var int $index + * @var int $index * @var array $array */ foreach ($data as $index => $array) { diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index c78b7afce8..84d57a4649 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -94,57 +94,22 @@ class ExchangeRateConverter return '0' === $rate ? '1' : $rate; } - /** - * @throws FireflyException - */ - private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string + public function setIgnoreSettings(bool $ignoreSettings): void { - $key = $this->getCacheKey($from, $to, $date); - $res = Cache::get($key, null); + $this->ignoreSettings = $ignoreSettings; + } - // find in cache - if (null !== $res) { - Log::debug(sprintf('ExchangeRateConverter: Return cached rate (%s) from %s to %s on %s.', $res, $from->code, $to->code, $date->format('Y-m-d'))); + public function setUserGroup(UserGroup $userGroup): void + { + $this->userGroup = $userGroup; + } - return $res; + public function summarize(): void + { + if (false === $this->enabled()) { + return; } - - // find in database - $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); - if (null !== $rate) { - Cache::forever($key, $rate); - Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); - - return $rate; - } - - // find reverse in database - $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); - if (null !== $rate) { - $rate = bcdiv('1', $rate); - Cache::forever($key, $rate); - Log::debug(sprintf('ExchangeRateConverter: Return inverse DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); - - return $rate; - } - - // fallback scenario. - $first = $this->getEuroRate($from, $date); - $second = $this->getEuroRate($to, $date); - - // combined (if present), they can be used to calculate the necessary conversion rate. - if (0 === bccomp('0', $first) || 0 === bccomp('0', $second)) { - Log::warning(sprintf('There is not enough information to convert %s to %s on date %s', $from->code, $to->code, $date->format('Y-m-d'))); - - return '1'; - } - - $second = bcdiv('1', $second); - $rate = bcmul($first, $second); - Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); - Cache::forever($key, $rate); - - return $rate; + Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount)); } private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string @@ -152,6 +117,57 @@ class ExchangeRateConverter return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d')); } + /** + * @throws FireflyException + */ + private function getEuroId(): int + { + Log::debug('getEuroId()'); + $cache = new CacheProperties(); + $cache->addProperty('cer-euro-id'); + if ($cache->has()) { + return (int)$cache->get(); + } + $euro = Amount::getTransactionCurrencyByCode('EUR'); + ++$this->queryCount; + $cache->store($euro->id); + + return $euro->id; + } + + /** + * @throws FireflyException + */ + private function getEuroRate(TransactionCurrency $currency, Carbon $date): string + { + $euroId = $this->getEuroId(); + if ($euroId === $currency->id) { + return '1'; + } + $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); + + if (null !== $rate) { + // app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate)); + return $rate; + } + $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); + if (null !== $rate) { + return bcdiv('1', $rate); + // app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate)); + // return $rate; + } + // grab backup values from config file: + $backup = config(sprintf('cer.rates.%s', $currency->code)); + if (null !== $backup) { + return bcdiv('1', (string)$backup); + // app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup)); + // return $backup; + } + + // app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code)); + return '0'; + } + private function getFromDB(int $from, int $to, string $date): ?string { if ($from === $to) { @@ -223,69 +239,53 @@ class ExchangeRateConverter /** * @throws FireflyException */ - private function getEuroRate(TransactionCurrency $currency, Carbon $date): string + private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string { - $euroId = $this->getEuroId(); - if ($euroId === $currency->id) { - return '1'; - } - $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); + $key = $this->getCacheKey($from, $to, $date); + $res = Cache::get($key, null); + // find in cache + if (null !== $res) { + Log::debug(sprintf('ExchangeRateConverter: Return cached rate (%s) from %s to %s on %s.', $res, $from->code, $to->code, $date->format('Y-m-d'))); + + return $res; + } + + // find in database + $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); if (null !== $rate) { - // app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate)); + Cache::forever($key, $rate); + Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); + return $rate; } - $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); + + // find reverse in database + $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); if (null !== $rate) { - return bcdiv('1', $rate); - // app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate)); - // return $rate; - } - // grab backup values from config file: - $backup = config(sprintf('cer.rates.%s', $currency->code)); - if (null !== $backup) { - return bcdiv('1', (string)$backup); - // app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup)); - // return $backup; + $rate = bcdiv('1', $rate); + Cache::forever($key, $rate); + Log::debug(sprintf('ExchangeRateConverter: Return inverse DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); + + return $rate; } - // app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code)); - return '0'; - } + // fallback scenario. + $first = $this->getEuroRate($from, $date); + $second = $this->getEuroRate($to, $date); - /** - * @throws FireflyException - */ - private function getEuroId(): int - { - Log::debug('getEuroId()'); - $cache = new CacheProperties(); - $cache->addProperty('cer-euro-id'); - if ($cache->has()) { - return (int)$cache->get(); + // combined (if present), they can be used to calculate the necessary conversion rate. + if (0 === bccomp('0', $first) || 0 === bccomp('0', $second)) { + Log::warning(sprintf('There is not enough information to convert %s to %s on date %s', $from->code, $to->code, $date->format('Y-m-d'))); + + return '1'; } - $euro = Amount::getTransactionCurrencyByCode('EUR'); - ++$this->queryCount; - $cache->store($euro->id); - return $euro->id; - } + $second = bcdiv('1', $second); + $rate = bcmul($first, $second); + Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); + Cache::forever($key, $rate); - public function setIgnoreSettings(bool $ignoreSettings): void - { - $this->ignoreSettings = $ignoreSettings; - } - - public function setUserGroup(UserGroup $userGroup): void - { - $this->userGroup = $userGroup; - } - - public function summarize(): void - { - if (false === $this->enabled()) { - return; - } - Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount)); + return $rate; } } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 7371b997bd..1dda34bf8d 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -38,6 +38,7 @@ use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Navigation; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; /** @@ -72,6 +73,7 @@ trait PeriodOverview protected AccountRepositoryInterface $accountRepository; protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; + private Collection $statistics; /** * This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range) @@ -82,30 +84,231 @@ 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'), $end->format('Y-m-d'))); + 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->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + + // TODO needs to be re-arranged: + // get all period stats for entire range. + // loop blocks, an loop the types, and select the missing ones. + // create new ones, or use collected. + /** @var array $dates */ $dates = Navigation::blockPeriods($start, $end, $range); $entries = []; + $types = ['spent', 'earned', 'transferred_in', 'transferred_away']; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { - $entries[] = $this->getSingleAccountPeriod($account, $currentDate['start'], $currentDate['end']); + $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); } Log::debug('End of getAccountPeriodOverview()'); return $entries; } - protected function getSingleAccountPeriod(Account $account, Carbon $start, Carbon $end): array + /** + * Overview for single category. Has been refactored recently. + * + * @throws FireflyException + */ + protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array + { + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + + // properties for entries with their amounts. + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($range); + $cache->addProperty('category-show-period-entries'); + $cache->addProperty($category->id); + + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setCategory($category); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earnedSet = $collector->getExtractedJournals(); + + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setCategory($category); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spentSet = $collector->getExtractedJournals(); + + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setCategory($category); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferSet = $collector->getExtractedJournals(); + foreach ($dates as $currentDate) { + $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); + $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); + $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')] + ), + 'total_transactions' => count($spent) + count($earned) + count($transferred), + 'spent' => $this->groupByCurrency($spent), + 'earned' => $this->groupByCurrency($earned), + 'transferred' => $this->groupByCurrency($transferred), + ]; + } + $cache->store($entries); + + return $entries; + } + + /** + * Same as above, but for lists that involve transactions without a budget. + * + * This method has been refactored recently. + * + * @throws FireflyException + */ + protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array + { + $range = Navigation::getViewRange(true); + + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($this->convertToPrimary); + $cache->addProperty('no-budget-period-entries'); + + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + + // get all expenses without a budget. + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $journals = $collector->getExtractedJournals(); + + foreach ($dates as $currentDate) { + $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; + } + $cache->store($entries); + + return $entries; + } + + /** + * TODO fix the date. + * + * Show period overview for no category view. + * + * @throws FireflyException + */ + protected function getNoCategoryPeriodOverview(Carbon $theDate): array + { + Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d'))); + $range = Navigation::getViewRange(true); + $first = $this->journalRepos->firstNull(); + $start = null === $first ? new Carbon() : $first->date; + $end = clone $theDate; + $end = Navigation::endOfPeriod($end, $range); + + Log::debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d'))); + Log::debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d'))); + + // properties for cache + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earnedSet = $collector->getExtractedJournals(); + + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spentSet = $collector->getExtractedJournals(); + + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferSet = $collector->getExtractedJournals(); + + /** @var array $currentDate */ + foreach ($dates as $currentDate) { + $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); + $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); + $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'title' => $title, + 'route' => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($spent) + count($earned) + count($transferred), + 'spent' => $this->groupByCurrency($spent), + 'earned' => $this->groupByCurrency($earned), + 'transferred' => $this->groupByCurrency($transferred), + ]; + } + Log::debug('End of loops'); + + return $entries; + } + + protected function getSingleAccountPeriod(Account $account, string $period, Carbon $start, Carbon $end): array { Log::debug(sprintf('Now in getSingleAccountPeriod(#%d, %s %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); $types = ['spent', 'earned', 'transferred_in', 'transferred_away']; $return = [ - 'title' => Navigation::periodShow($start, $end), + 'title' => Navigation::periodShow($start, $period), 'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $start->format('Y-m-d')]), 'total_transactions' => 0, ]; @@ -119,13 +322,34 @@ trait PeriodOverview return $return; } + protected function filterStatistics(Carbon $start, Carbon $end, string $type): Collection + { + return $this->statistics->filter( + function (PeriodStatistic $statistic) use ($start, $end, $type) { + if( + !$statistic->end->equalTo($end) + && $statistic->end->format('Y-m-d H:i:s') === $end->format('Y-m-d H:i:s') + ) { + echo sprintf('End: "%s" vs "%s": %s', $statistic->end->toW3cString(), $end->toW3cString(), var_export($statistic->end->eq($end), true)); + var_dump($statistic->end); + var_dump($end); + exit; + } + + + return $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type; + } + ); + } + protected function getSingleAccountPeriodByType(Account $account, Carbon $start, Carbon $end, string $type): array { Log::debug(sprintf('Now in getSingleAccountPeriodByType(#%d, %s %s, %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type)); - $statistics = $this->periodStatisticRepo->findPeriodStatistic($account, $start, $end, $type); + $statistics = $this->filterStatistics($start, $end, $type); // nothing found, regenerate them. if (0 === $statistics->count()) { + Log::debug(sprintf('Found nothing in this period for type "%s"', $type)); $transactions = $this->accountRepository->periodCollection($account, $start, $end); switch ($type) { @@ -183,12 +407,195 @@ trait PeriodOverview return $grouped; } + /** + * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums. + * + * @throws FireflyException + */ + protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. + { + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + + // properties for cache + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('tag-period-entries'); + $cache->addProperty($tag->id); + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setTag($tag); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earnedSet = $collector->getExtractedJournals(); + + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setTag($tag); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spentSet = $collector->getExtractedJournals(); + + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setTag($tag); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferSet = $collector->getExtractedJournals(); + + // filer all of them: + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); + + foreach ($dates as $currentDate) { + $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); + $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); + $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')] + ), + 'total_transactions' => count($spent) + count($earned) + count($transferred), + 'spent' => $this->groupByCurrency($spent), + 'earned' => $this->groupByCurrency($earned), + 'transferred' => $this->groupByCurrency($transferred), + ]; + } + + return $entries; + } + + /** + * @throws FireflyException + */ + protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array + { + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + + // properties for cache + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('transactions-period-entries'); + $cache->addProperty($transactionType); + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; + // collect all journals in this period (regardless of type) + $collector = app(GroupCollectorInterface::class); + $collector->setTypes($types)->setRange($start, $end); + $genericSet = $collector->getExtractedJournals(); + $loops = 0; + + foreach ($dates as $currentDate) { + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + + if ($loops < 10) { + // set to correct array + if ('expenses' === $transactionType || 'withdrawal' === $transactionType) { + $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); + } + if ('revenue' === $transactionType || 'deposit' === $transactionType) { + $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); + } + if ('transfer' === $transactionType || 'transfers' === $transactionType) { + $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); + } + } + $entries[] + = [ + 'title' => $title, + 'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($spent) + count($earned) + count($transferred), + 'spent' => $this->groupByCurrency($spent), + 'earned' => $this->groupByCurrency($earned), + 'transferred' => $this->groupByCurrency($transferred), + ]; + ++$loops; + } + + return $entries; + } + + protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void + { + unset($array['count']); + foreach ($array as $entry) { + $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); + } + } + + /** + * Filter a list of journals by a set of dates, and then group them by currency. + */ + private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array + { + $result = []; + + /** @var array $journal */ + foreach ($array as $journal) { + if ($journal['date'] <= $end && $journal['date'] >= $start) { + $result[] = $journal; + } + } + + return $result; + } + + private function filterJournalsByTag(array $set, Tag $tag): array + { + $return = []; + foreach ($set as $entry) { + $found = false; + + /** @var array $localTag */ + foreach ($entry['tags'] as $localTag) { + if ($localTag['id'] === $tag->id) { + $found = true; + } + } + if (false === $found) { + continue; + } + $return[] = $entry; + } + + return $return; + } + private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array { $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -203,12 +610,46 @@ trait PeriodOverview return $result; } + /** + * Return only transactions where $account is the source. + */ + private function filterTransferredAway(Account $account, array $journals): array + { + $return = []; + + /** @var array $journal */ + foreach ($journals as $journal) { + if ($account->id === (int)$journal['source_account_id']) { + $return[] = $journal; + } + } + + return $return; + } + + /** + * Return only transactions where $account is the source. + */ + private function filterTransferredIn(Account $account, array $journals): array + { + $return = []; + + /** @var array $journal */ + foreach ($journals as $journal) { + if ($account->id === (int)$journal['destination_account_id']) { + $return[] = $journal; + } + } + + return $return; + } + private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array { $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -289,414 +730,4 @@ trait PeriodOverview return $return; } - - protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void - { - unset($array['count']); - foreach ($array as $entry) { - $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); - } - } - - /** - * Overview for single category. Has been refactored recently. - * - * @throws FireflyException - */ - protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array - { - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - // properties for entries with their amounts. - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($range); - $cache->addProperty('category-show-period-entries'); - $cache->addProperty($category->id); - - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')] - ), - 'total_transactions' => count($spent) + count($earned) + count($transferred), - 'spent' => $this->groupByCurrency($spent), - 'earned' => $this->groupByCurrency($earned), - 'transferred' => $this->groupByCurrency($transferred), - ]; - } - $cache->store($entries); - - return $entries; - } - - /** - * Filter a list of journals by a set of dates, and then group them by currency. - */ - private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array - { - $result = []; - - /** @var array $journal */ - foreach ($array as $journal) { - if ($journal['date'] <= $end && $journal['date'] >= $start) { - $result[] = $journal; - } - } - - return $result; - } - - /** - * Same as above, but for lists that involve transactions without a budget. - * - * This method has been refactored recently. - * - * @throws FireflyException - */ - protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array - { - $range = Navigation::getViewRange(true); - - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($this->convertToPrimary); - $cache->addProperty('no-budget-period-entries'); - - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // get all expenses without a budget. - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); - - foreach ($dates as $currentDate) { - $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; - } - $cache->store($entries); - - return $entries; - } - - /** - * TODO fix the date. - * - * Show period overview for no category view. - * - * @throws FireflyException - */ - protected function getNoCategoryPeriodOverview(Carbon $theDate): array - { - app('log')->debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d'))); - $range = Navigation::getViewRange(true); - $first = $this->journalRepos->firstNull(); - $start = null === $first ? new Carbon() : $first->date; - $end = clone $theDate; - $end = Navigation::endOfPeriod($end, $range); - - app('log')->debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d'))); - app('log')->debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d'))); - - // properties for cache - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - - /** @var array $currentDate */ - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'title' => $title, - 'route' => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($spent) + count($earned) + count($transferred), - 'spent' => $this->groupByCurrency($spent), - 'earned' => $this->groupByCurrency($earned), - 'transferred' => $this->groupByCurrency($transferred), - ]; - } - app('log')->debug('End of loops'); - - return $entries; - } - - /** - * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums. - * - * @throws FireflyException - */ - protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. - { - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - // properties for cache - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('tag-period-entries'); - $cache->addProperty($tag->id); - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - - // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); - - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')] - ), - 'total_transactions' => count($spent) + count($earned) + count($transferred), - 'spent' => $this->groupByCurrency($spent), - 'earned' => $this->groupByCurrency($earned), - 'transferred' => $this->groupByCurrency($transferred), - ]; - } - - return $entries; - } - - private function filterJournalsByTag(array $set, Tag $tag): array - { - $return = []; - foreach ($set as $entry) { - $found = false; - - /** @var array $localTag */ - foreach ($entry['tags'] as $localTag) { - if ($localTag['id'] === $tag->id) { - $found = true; - } - } - if (false === $found) { - continue; - } - $return[] = $entry; - } - - return $return; - } - - /** - * @throws FireflyException - */ - protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array - { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - // properties for cache - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('transactions-period-entries'); - $cache->addProperty($transactionType); - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; - // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); - $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; - - foreach ($dates as $currentDate) { - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - - if ($loops < 10) { - // set to correct array - if ('expenses' === $transactionType || 'withdrawal' === $transactionType) { - $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); - } - if ('revenue' === $transactionType || 'deposit' === $transactionType) { - $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); - } - if ('transfer' === $transactionType || 'transfers' === $transactionType) { - $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); - } - } - $entries[] - = [ - 'title' => $title, - 'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($spent) + count($earned) + count($transferred), - 'spent' => $this->groupByCurrency($spent), - 'earned' => $this->groupByCurrency($earned), - 'transferred' => $this->groupByCurrency($transferred), - ]; - ++$loops; - } - - return $entries; - } - - /** - * Return only transactions where $account is the source. - */ - private function filterTransferredAway(Account $account, array $journals): array - { - $return = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - if ($account->id === (int)$journal['source_account_id']) { - $return[] = $journal; - } - } - - return $return; - } - - /** - * Return only transactions where $account is the source. - */ - private function filterTransferredIn(Account $account, array $journals): array - { - $return = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - if ($account->id === (int)$journal['destination_account_id']) { - $return[] = $journal; - } - } - - return $return; - } } diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php index 73a39655e6..b7600f778a 100644 --- a/app/Support/Http/Controllers/RequestInformation.php +++ b/app/Support/Http/Controllers/RequestInformation.php @@ -53,6 +53,22 @@ trait RequestInformation return $parts['host'] ?? ''; } + final protected function getPageName(): string // get request info + { + return str_replace('.', '_', RouteFacade::currentRouteName()); + } + + /** + * Get the specific name of a page for intro. + */ + final protected function getSpecificPageName(): string // get request info + { + /** @var null|string $param */ + $param = RouteFacade::current()->parameter('objectType'); + + return null === $param ? '' : sprintf('_%s', $param); + } + /** * Get a list of triggers. */ @@ -102,22 +118,6 @@ trait RequestInformation return $shownDemo; } - final protected function getPageName(): string // get request info - { - return str_replace('.', '_', RouteFacade::currentRouteName()); - } - - /** - * Get the specific name of a page for intro. - */ - final protected function getSpecificPageName(): string // get request info - { - /** @var null|string $param */ - $param = RouteFacade::current()->parameter('objectType'); - - return null === $param ? '' : sprintf('_%s', $param); - } - /** * Check if date is outside session range. */ diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php index 62dee47082..7dc3cb444c 100644 --- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php @@ -53,29 +53,29 @@ use Override; */ class AccountEnrichment implements EnrichmentInterface { - private array $ids = []; - private array $accountTypeIds = []; - private array $accountTypes = []; - private Collection $collection; - private array $currencies = []; - private array $locations = []; - private array $meta = []; - private readonly TransactionCurrency $primaryCurrency; - private array $notes = []; - private array $openingBalances = []; - private User $user; - private UserGroup $userGroup; - private array $lastActivities = []; - private ?Carbon $date = null; - private ?Carbon $start = null; - private ?Carbon $end = null; + private array $accountTypeIds = []; + private array $accountTypes = []; + private array $balances = []; + private Collection $collection; private readonly bool $convertToPrimary; - private array $balances = []; - private array $startBalances = []; - private array $endBalances = []; - private array $objectGroups = []; - private array $mappedObjects = []; - private array $sort = []; + private array $currencies = []; + private ?Carbon $date = null; + private ?Carbon $end = null; + private array $endBalances = []; + private array $ids = []; + private array $lastActivities = []; + private array $locations = []; + private array $mappedObjects = []; + private array $meta = []; + private array $notes = []; + private array $objectGroups = []; + private array $openingBalances = []; + private readonly TransactionCurrency $primaryCurrency; + private array $sort = []; + private ?Carbon $start = null; + private array $startBalances = []; + private User $user; + private UserGroup $userGroup; /** * TODO The account enricher must do conversion from and to the primary currency. @@ -86,16 +86,6 @@ class AccountEnrichment implements EnrichmentInterface $this->convertToPrimary = Amount::convertToPrimary(); } - #[Override] - public function enrichSingle(array|Model $model): Account|array - { - Log::debug(__METHOD__); - $collection = new Collection()->push($model); - $collection = $this->enrich($collection); - - return $collection->first(); - } - #[Override] /** * Do the actual enrichment. @@ -121,114 +111,47 @@ class AccountEnrichment implements EnrichmentInterface return $this->collection; } - private function collectIds(): void + #[Override] + public function enrichSingle(array | Model $model): Account | array { - /** @var Account $account */ - foreach ($this->collection as $account) { - $this->ids[] = (int)$account->id; - $this->accountTypeIds[] = (int)$account->account_type_id; - } - $this->ids = array_unique($this->ids); - $this->accountTypeIds = array_unique($this->accountTypeIds); + Log::debug(__METHOD__); + $collection = new Collection()->push($model); + $collection = $this->enrich($collection); + + return $collection->first(); } - private function getAccountTypes(): void + public function getDate(): Carbon { - $types = AccountType::whereIn('id', $this->accountTypeIds)->get(); - - /** @var AccountType $type */ - foreach ($types as $type) { - $this->accountTypes[(int)$type->id] = $type->type; + if (!$this->date instanceof Carbon) { + return now(); } + + return $this->date; } - private function collectMetaData(): void + public function setDate(?Carbon $date): void { - $set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt']) - ->whereIn('account_id', $this->ids) - ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray() - ; - - /** @var array $entry */ - foreach ($set as $entry) { - $this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data']; - if ('currency_id' === $entry['name']) { - $this->currencies[(int)$entry['data']] = true; - } - } - if (count($this->currencies) > 0) { - $currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get(); - foreach ($currencies as $currency) { - $this->currencies[(int)$currency->id] = $currency; - } - } - $this->currencies[0] = $this->primaryCurrency; - foreach ($this->currencies as $id => $currency) { - if (true === $currency) { - throw new FireflyException(sprintf('Currency #%d not found.', $id)); - } + if ($date instanceof Carbon) { + $date->endOfDay(); + Log::debug(sprintf('Date is now %s', $date->toW3cString())); } + $this->date = $date; } - private function collectNotes(): void + public function setEnd(?Carbon $end): void { - $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; - foreach ($notes as $note) { - $this->notes[(int)$note['noteable_id']] = (string)$note['text']; - } - Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); + $this->end = $end; } - private function collectLocations(): void + public function setSort(array $sort): void { - $locations = Location::query()->whereIn('locatable_id', $this->ids) - ->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray() - ; - foreach ($locations as $location) { - $this->locations[(int)$location['locatable_id']] - = [ - 'latitude' => (float)$location['latitude'], - 'longitude' => (float)$location['longitude'], - 'zoom_level' => (int)$location['zoom_level'], - ]; - } - Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations))); + $this->sort = $sort; } - private function collectOpeningBalances(): void + public function setStart(?Carbon $start): void { - // use new group collector: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector - ->setUser($this->user) - ->setUserGroup($this->userGroup) - ->setAccounts($this->collection) - ->withAccountInformation() - ->setTypes([TransactionTypeEnum::OPENING_BALANCE->value]) - ; - $journals = $collector->getExtractedJournals(); - foreach ($journals as $journal) { - $this->openingBalances[(int)$journal['source_account_id']] - = [ - 'amount' => Steam::negative($journal['amount']), - 'date' => $journal['date'], - ]; - $this->openingBalances[(int)$journal['destination_account_id']] - = [ - 'amount' => Steam::positive($journal['amount']), - 'date' => $journal['date'], - ]; - } - } - - public function setUserGroup(UserGroup $userGroup): void - { - $this->userGroup = $userGroup; + $this->start = $start; } public function setUser(User $user): void @@ -237,12 +160,17 @@ class AccountEnrichment implements EnrichmentInterface $this->userGroup = $user->userGroup; } + public function setUserGroup(UserGroup $userGroup): void + { + $this->userGroup = $userGroup; + } + private function appendCollectedData(): void { $this->collection = $this->collection->map(function (Account $item) { - $id = (int)$item->id; - $item->full_account_type = $this->accountTypes[(int)$item->account_type_id] ?? null; - $meta = [ + $id = (int)$item->id; + $item->full_account_type = $this->accountTypes[(int)$item->account_type_id] ?? null; + $meta = [ 'currency' => null, 'location' => [ 'latitude' => null, @@ -289,30 +217,30 @@ class AccountEnrichment implements EnrichmentInterface // add balances // get currencies: - $currency = $this->primaryCurrency; // assume primary currency + $currency = $this->primaryCurrency; // assume primary currency if (null !== $meta['currency']) { $currency = $meta['currency']; } // get the current balance: - $date = $this->getDate(); + $date = $this->getDate(); // $finalBalance = Steam::finalAccountBalance($item, $date, $this->primaryCurrency, $this->convertToPrimary); - $finalBalance = $this->balances[$id]; - $balanceDifference = $this->getBalanceDifference($id, $currency); + $finalBalance = $this->balances[$id]; + $balanceDifference = $this->getBalanceDifference($id, $currency); Log::debug(sprintf('Call finalAccountBalance(%s) with date/time "%s"', var_export($this->convertToPrimary, true), $date->toIso8601String()), $finalBalance); // collect current balances: - $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); - $openingBalance = Steam::bcround($meta['opening_balance_amount'] ?? '0', $currency->decimal_places); - $virtualBalance = Steam::bcround($item->virtual_balance ?? '0', $currency->decimal_places); - $debtAmount = $meta['current_debt'] ?? null; + $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); + $openingBalance = Steam::bcround($meta['opening_balance_amount'] ?? '0', $currency->decimal_places); + $virtualBalance = Steam::bcround($item->virtual_balance ?? '0', $currency->decimal_places); + $debtAmount = $meta['current_debt'] ?? null; // set some pc_ default values to NULL: - $pcCurrentBalance = null; - $pcOpeningBalance = null; - $pcVirtualBalance = null; - $pcDebtAmount = null; - $pcBalanceDifference = null; + $pcCurrentBalance = null; + $pcOpeningBalance = null; + $pcVirtualBalance = null; + $pcDebtAmount = null; + $pcBalanceDifference = null; // convert to primary currency if needed: if ($this->convertToPrimary && $currency->id !== $this->primaryCurrency->id) { @@ -351,17 +279,12 @@ class AccountEnrichment implements EnrichmentInterface 'pc_balance_difference' => $pcBalanceDifference, ]; // end add balances - $item->meta = $meta; + $item->meta = $meta; return $item; }); } - private function collectLastActivities(): void - { - $this->lastActivities = Steam::getLastActivities($this->ids); - } - private function collectBalances(): void { $this->balances = Steam::accountsBalancesOptimized($this->collection, $this->getDate(), $this->primaryCurrency, $this->convertToPrimary); @@ -371,15 +294,84 @@ class AccountEnrichment implements EnrichmentInterface } } + private function collectIds(): void + { + /** @var Account $account */ + foreach ($this->collection as $account) { + $this->ids[] = (int)$account->id; + $this->accountTypeIds[] = (int)$account->account_type_id; + } + $this->ids = array_unique($this->ids); + $this->accountTypeIds = array_unique($this->accountTypeIds); + } + + private function collectLastActivities(): void + { + $this->lastActivities = Steam::getLastActivities($this->ids); + } + + private function collectLocations(): void + { + $locations = Location::query()->whereIn('locatable_id', $this->ids) + ->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray(); + foreach ($locations as $location) { + $this->locations[(int)$location['locatable_id']] + = [ + 'latitude' => (float)$location['latitude'], + 'longitude' => (float)$location['longitude'], + 'zoom_level' => (int)$location['zoom_level'], + ]; + } + Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations))); + } + + private function collectMetaData(): void + { + $set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt']) + ->whereIn('account_id', $this->ids) + ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray(); + + /** @var array $entry */ + foreach ($set as $entry) { + $this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data']; + if ('currency_id' === $entry['name']) { + $this->currencies[(int)$entry['data']] = true; + } + } + if (count($this->currencies) > 0) { + $currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get(); + foreach ($currencies as $currency) { + $this->currencies[(int)$currency->id] = $currency; + } + } + $this->currencies[0] = $this->primaryCurrency; + foreach ($this->currencies as $id => $currency) { + if (true === $currency) { + throw new FireflyException(sprintf('Currency #%d not found.', $id)); + } + } + } + + private function collectNotes(): void + { + $notes = Note::query()->whereIn('noteable_id', $this->ids) + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + foreach ($notes as $note) { + $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + } + Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); + } + private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', Account::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', Account::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -393,32 +385,40 @@ class AccountEnrichment implements EnrichmentInterface } } - public function setDate(?Carbon $date): void + private function collectOpeningBalances(): void { - if ($date instanceof Carbon) { - $date->endOfDay(); - Log::debug(sprintf('Date is now %s', $date->toW3cString())); + // use new group collector: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector + ->setUser($this->user) + ->setUserGroup($this->userGroup) + ->setAccounts($this->collection) + ->withAccountInformation() + ->setTypes([TransactionTypeEnum::OPENING_BALANCE->value]); + $journals = $collector->getExtractedJournals(); + foreach ($journals as $journal) { + $this->openingBalances[(int)$journal['source_account_id']] + = [ + 'amount' => Steam::negative($journal['amount']), + 'date' => $journal['date'], + ]; + $this->openingBalances[(int)$journal['destination_account_id']] + = [ + 'amount' => Steam::positive($journal['amount']), + 'date' => $journal['date'], + ]; } - $this->date = $date; } - public function getDate(): Carbon + private function getAccountTypes(): void { - if (!$this->date instanceof Carbon) { - return now(); + $types = AccountType::whereIn('id', $this->accountTypeIds)->get(); + + /** @var AccountType $type */ + foreach ($types as $type) { + $this->accountTypes[(int)$type->id] = $type->type; } - - return $this->date; - } - - public function setStart(?Carbon $start): void - { - $this->start = $start; - } - - public function setEnd(?Carbon $end): void - { - $this->end = $end; } private function getBalanceDifference(int $id, TransactionCurrency $currency): ?string @@ -431,17 +431,12 @@ class AccountEnrichment implements EnrichmentInterface if (0 === count($startBalance) || 0 === count($endBalance)) { return null; } - $start = $startBalance[$currency->code] ?? '0'; - $end = $endBalance[$currency->code] ?? '0'; + $start = $startBalance[$currency->code] ?? '0'; + $end = $endBalance[$currency->code] ?? '0'; return bcsub($end, $start); } - public function setSort(array $sort): void - { - $this->sort = $sort; - } - private function sortData(): void { $dbParams = config('firefly.allowed_db_sort_parameters.Account', []); @@ -458,7 +453,7 @@ class AccountEnrichment implements EnrichmentInterface case 'current_balance': case 'pc_current_balance': - $this->collection = $this->collection->sortBy(static fn (Account $account) => $account->meta['balances'][$parameter[0]] ?? '0', SORT_NUMERIC, 'desc' === $parameter[1]); + $this->collection = $this->collection->sortBy(static fn(Account $account) => $account->meta['balances'][$parameter[0]] ?? '0', SORT_NUMERIC, 'desc' === $parameter[1]); break; } diff --git a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php index 6154c941aa..85711c7efb 100644 --- a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php @@ -40,20 +40,20 @@ use Override; class AvailableBudgetEnrichment implements EnrichmentInterface { - private User $user; // @phpstan-ignore-line - private UserGroup $userGroup; // @phpstan-ignore-line - private readonly bool $convertToPrimary; - private array $ids = []; - private array $currencyIds = []; + private Collection $collection; // @phpstan-ignore-line + private readonly bool $convertToPrimary; // @phpstan-ignore-line private array $currencies = []; - private Collection $collection; - private array $spentInBudgets = []; - private array $spentOutsideBudgets = []; - private array $pcSpentInBudgets = []; - private array $pcSpentOutsideBudgets = []; + private array $currencyIds = []; + private array $ids = []; private readonly NoBudgetRepositoryInterface $noBudgetRepository; private readonly OperationsRepositoryInterface $opsRepository; + private array $pcSpentInBudgets = []; + private array $pcSpentOutsideBudgets = []; private readonly BudgetRepositoryInterface $repository; + private array $spentInBudgets = []; + private array $spentOutsideBudgets = []; + private User $user; + private UserGroup $userGroup; public function __construct() { @@ -79,7 +79,7 @@ class AvailableBudgetEnrichment implements EnrichmentInterface } #[Override] - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -104,6 +104,34 @@ class AvailableBudgetEnrichment implements EnrichmentInterface $this->repository->setUserGroup($userGroup); } + private function appendCollectedData(): void + { + $this->collection = $this->collection->map(function (AvailableBudget $item) { + $id = (int)$item->id; + $currencyId = $this->currencyIds[$id]; + $currency = $this->currencies[$currencyId]; + $meta = [ + 'currency' => $currency, + 'spent_in_budgets' => $this->spentInBudgets[$id] ?? [], + 'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [], + 'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [], + 'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [], + ]; + $item->meta = $meta; + + return $item; + }); + } + + private function collectCurrencies(): void + { + $ids = array_unique(array_values($this->currencyIds)); + $set = TransactionCurrency::whereIn('id', $ids)->get(); + foreach ($set as $currency) { + $this->currencies[(int)$currency->id] = $currency; + } + } + private function collectIds(): void { /** @var AvailableBudget $availableBudget */ @@ -138,32 +166,4 @@ class AvailableBudgetEnrichment implements EnrichmentInterface } } } - - private function appendCollectedData(): void - { - $this->collection = $this->collection->map(function (AvailableBudget $item) { - $id = (int)$item->id; - $currencyId = $this->currencyIds[$id]; - $currency = $this->currencies[$currencyId]; - $meta = [ - 'currency' => $currency, - 'spent_in_budgets' => $this->spentInBudgets[$id] ?? [], - 'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [], - 'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [], - 'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [], - ]; - $item->meta = $meta; - - return $item; - }); - } - - private function collectCurrencies(): void - { - $ids = array_unique(array_values($this->currencyIds)); - $set = TransactionCurrency::whereIn('id', $ids)->get(); - foreach ($set as $currency) { - $this->currencies[(int)$currency->id] = $currency; - } - } } diff --git a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php index 2aa21bc9cf..71e4ff160b 100644 --- a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php @@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log; class BudgetEnrichment implements EnrichmentInterface { - private Collection $collection; - private User $user; - private UserGroup $userGroup; - private array $ids = []; - private array $notes = []; - private array $autoBudgets = []; - private array $currencies = []; - private ?Carbon $start = null; - private ?Carbon $end = null; - private array $spent = []; - private array $pcSpent = []; - private array $objectGroups = []; - private array $mappedObjects = []; + private array $autoBudgets = []; + private Collection $collection; + private array $currencies = []; + private ?Carbon $end = null; + private array $ids = []; + private array $mappedObjects = []; + private array $notes = []; + private array $objectGroups = []; + private array $pcSpent = []; + private array $spent = []; + private ?Carbon $start = null; + private User $user; + private UserGroup $userGroup; public function __construct() {} @@ -70,7 +70,7 @@ class BudgetEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -79,6 +79,16 @@ class BudgetEnrichment implements EnrichmentInterface return $collection->first(); } + public function setEnd(?Carbon $end): void + { + $this->end = $end; + } + + public function setStart(?Carbon $start): void + { + $this->start = $start; + } + public function setUser(User $user): void { $this->user = $user; @@ -90,33 +100,11 @@ class BudgetEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } - private function collectIds(): void - { - /** @var Budget $budget */ - foreach ($this->collection as $budget) { - $this->ids[] = (int)$budget->id; - } - $this->ids = array_unique($this->ids); - } - - private function collectNotes(): void - { - $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; - foreach ($notes as $note) { - $this->notes[(int)$note['noteable_id']] = (string)$note['text']; - } - Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); - } - private function appendCollectedData(): void { $this->collection = $this->collection->map(function (Budget $item) { - $id = (int)$item->id; - $meta = [ + $id = (int)$item->id; + $meta = [ 'object_group_id' => null, 'object_group_order' => null, 'object_group_title' => null, @@ -130,7 +118,7 @@ class BudgetEnrichment implements EnrichmentInterface // add object group if available if (array_key_exists($id, $this->mappedObjects)) { $key = $this->mappedObjects[$id]; - $meta['object_group_id'] = (string) $this->objectGroups[$key]['id']; + $meta['object_group_id'] = (string)$this->objectGroups[$key]['id']; $meta['object_group_title'] = $this->objectGroups[$key]['title']; $meta['object_group_order'] = $this->objectGroups[$key]['order']; } @@ -168,7 +156,7 @@ class BudgetEnrichment implements EnrichmentInterface $opsRepository->setUserGroup($this->userGroup); // $spent = $this->beautify(); // $set = $this->opsRepository->sumExpenses($start, $end, null, new Collection()->push($budget)) - $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection, null); + $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection, null); foreach ($this->collection as $item) { $id = (int)$item->id; $this->spent[$id] = array_values($opsRepository->sumCollectedExpensesByBudget($expenses, $item, false)); @@ -177,25 +165,35 @@ class BudgetEnrichment implements EnrichmentInterface } } - public function setEnd(?Carbon $end): void + private function collectIds(): void { - $this->end = $end; + /** @var Budget $budget */ + foreach ($this->collection as $budget) { + $this->ids[] = (int)$budget->id; + } + $this->ids = array_unique($this->ids); } - public function setStart(?Carbon $start): void + private function collectNotes(): void { - $this->start = $start; + $notes = Note::query()->whereIn('noteable_id', $this->ids) + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + foreach ($notes as $note) { + $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + } + Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); } private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', Budget::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', Budget::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; diff --git a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php index 0ba9e1f1a3..f0a7fd3479 100644 --- a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php @@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log; class BudgetLimitEnrichment implements EnrichmentInterface { - private User $user; - private UserGroup $userGroup; // @phpstan-ignore-line private Collection $collection; - private array $ids = []; - private array $notes = []; - private Carbon $start; + private bool $convertToPrimary = true; // @phpstan-ignore-line + private array $currencies = []; + private array $currencyIds = []; private Carbon $end; private array $expenses = []; + private array $ids = []; + private array $notes = []; private array $pcExpenses = []; - private array $currencyIds = []; - private array $currencies = []; - private bool $convertToPrimary = true; private readonly TransactionCurrency $primaryCurrency; + private Carbon $start; + private User $user; + private UserGroup $userGroup; public function __construct() { @@ -73,7 +73,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -93,36 +93,6 @@ class BudgetLimitEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } - private function collectIds(): void - { - $this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth(); - $this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth(); - - /** @var BudgetLimit $limit */ - foreach ($this->collection as $limit) { - $id = (int)$limit->id; - $this->ids[] = $id; - if (0 !== (int)$limit->transaction_currency_id) { - $this->currencyIds[$id] = (int)$limit->transaction_currency_id; - } - } - $this->ids = array_unique($this->ids); - $this->currencyIds = array_unique($this->currencyIds); - } - - private function collectNotes(): void - { - $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; - foreach ($notes as $note) { - $this->notes[(int)$note['noteable_id']] = (string)$note['text']; - } - Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); - } - private function appendCollectedData(): void { $this->collection = $this->collection->map(function (BudgetLimit $item) { @@ -145,12 +115,12 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function collectBudgets(): void { - $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); - $budgets = Budget::whereIn('id', $budgetIds)->get(); + $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); + $budgets = Budget::whereIn('id', $budgetIds)->get(); $repository = app(OperationsRepository::class); $repository->setUser($this->user); - $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); + $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); /** @var BudgetLimit $budgetLimit */ foreach ($this->collection as $budgetLimit) { @@ -179,26 +149,55 @@ class BudgetLimitEnrichment implements EnrichmentInterface } } - private function stringifyIds(): void + private function collectIds(): void { - $this->expenses = array_map(fn ($first) => array_map(function ($second) { - $second['currency_id'] = (string)($second['currency_id'] ?? 0); + $this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth(); + $this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth(); - return $second; - }, $first), $this->expenses); + /** @var BudgetLimit $limit */ + foreach ($this->collection as $limit) { + $id = (int)$limit->id; + $this->ids[] = $id; + if (0 !== (int)$limit->transaction_currency_id) { + $this->currencyIds[$id] = (int)$limit->transaction_currency_id; + } + } + $this->ids = array_unique($this->ids); + $this->currencyIds = array_unique($this->currencyIds); + } - $this->pcExpenses = array_map(fn ($first) => array_map(function ($second) { - $second['currency_id'] = (string)($second['currency_id'] ?? 0); - - return $second; - }, $first), $this->expenses); + private function collectNotes(): void + { + $notes = Note::query()->whereIn('noteable_id', $this->ids) + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + foreach ($notes as $note) { + $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + } + Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); } private function filterToBudget(array $expenses, int $budget): array { - $result = array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget); + $result = array_filter($expenses, fn(array $item) => (int)$item['budget_id'] === $budget); Log::debug(sprintf('filterToBudget for budget #%d, from %d to %d items', $budget, count($expenses), count($result))); return $result; } + + private function stringifyIds(): void + { + $this->expenses = array_map(fn($first) => array_map(function ($second) { + $second['currency_id'] = (string)($second['currency_id'] ?? 0); + + return $second; + }, $first), $this->expenses); + + $this->pcExpenses = array_map(fn($first) => array_map(function ($second) { + $second['currency_id'] = (string)($second['currency_id'] ?? 0); + + return $second; + }, $first), $this->expenses); + } } diff --git a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php index bbe24f94c2..074747011b 100644 --- a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php +++ b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php @@ -38,18 +38,18 @@ use Illuminate\Support\Facades\Log; class CategoryEnrichment implements EnrichmentInterface { private Collection $collection; - private User $user; - private UserGroup $userGroup; + private array $earned = []; + private ?Carbon $end = null; private array $ids = []; private array $notes = []; - private ?Carbon $start = null; - private ?Carbon $end = null; - private array $spent = []; - private array $pcSpent = []; - private array $earned = []; private array $pcEarned = []; - private array $transfers = []; + private array $pcSpent = []; private array $pcTransfers = []; + private array $spent = []; + private ?Carbon $start = null; + private array $transfers = []; + private User $user; + private UserGroup $userGroup; public function enrich(Collection $collection): Collection { @@ -62,7 +62,7 @@ class CategoryEnrichment implements EnrichmentInterface return $collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -71,6 +71,16 @@ class CategoryEnrichment implements EnrichmentInterface return $collection->first(); } + public function setEnd(?Carbon $end): void + { + $this->end = $end; + } + + public function setStart(?Carbon $start): void + { + $this->start = $start; + } + public function setUser(User $user): void { $this->user = $user; @@ -82,15 +92,6 @@ class CategoryEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } - private function collectIds(): void - { - /** @var Category $category */ - foreach ($this->collection as $category) { - $this->ids[] = (int)$category->id; - } - $this->ids = array_unique($this->ids); - } - private function appendCollectedData(): void { $this->collection = $this->collection->map(function (Category $item) { @@ -110,23 +111,21 @@ class CategoryEnrichment implements EnrichmentInterface }); } - public function setEnd(?Carbon $end): void + private function collectIds(): void { - $this->end = $end; - } - - public function setStart(?Carbon $start): void - { - $this->start = $start; + /** @var Category $category */ + foreach ($this->collection as $category) { + $this->ids[] = (int)$category->id; + } + $this->ids = array_unique($this->ids); } private function collectNotes(): void { $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); foreach ($notes as $note) { $this->notes[(int)$note['noteable_id']] = (string)$note['text']; } @@ -140,9 +139,9 @@ class CategoryEnrichment implements EnrichmentInterface $opsRepository = app(OperationsRepositoryInterface::class); $opsRepository->setUser($this->user); $opsRepository->setUserGroup($this->userGroup); - $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection); - $income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection); - $transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection); + $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection); + $income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection); + $transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection); foreach ($this->collection as $item) { $id = (int)$item->id; $this->spent[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($expenses, $item, 'negative', false)); diff --git a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php index 0ccfb7c060..e93ddcf283 100644 --- a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php +++ b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php @@ -33,7 +33,7 @@ interface EnrichmentInterface { public function enrich(Collection $collection): Collection; - public function enrichSingle(array|Model $model): array|Model; + public function enrichSingle(array | Model $model): array | Model; public function setUser(User $user): void; diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php index bacf8d12c3..aebdde8f67 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php @@ -43,20 +43,20 @@ use Illuminate\Support\Facades\Log; class PiggyBankEnrichment implements EnrichmentInterface { - private User $user; // @phpstan-ignore-line - private UserGroup $userGroup; // @phpstan-ignore-line - private Collection $collection; - private array $ids = []; - private array $currencyIds = []; - private array $currencies = []; - private array $accountIds = []; + private array $accountIds = []; // @phpstan-ignore-line + private array $accounts = []; // @phpstan-ignore-line + private array $amounts = []; + private Collection $collection; + private array $currencies = []; + private array $currencyIds = []; + private array $ids = []; // private array $accountCurrencies = []; - private array $notes = []; - private array $mappedObjects = []; + private array $mappedObjects = []; + private array $notes = []; + private array $objectGroups = []; private readonly TransactionCurrency $primaryCurrency; - private array $amounts = []; - private array $accounts = []; - private array $objectGroups = []; + private User $user; + private UserGroup $userGroup; public function __construct() { @@ -77,7 +77,7 @@ class PiggyBankEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -97,80 +97,17 @@ class PiggyBankEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } - private function collectIds(): void - { - /** @var PiggyBank $piggy */ - foreach ($this->collection as $piggy) { - $id = (int)$piggy->id; - $this->ids[] = $id; - $this->currencyIds[$id] = (int)$piggy->transaction_currency_id; - } - $this->ids = array_unique($this->ids); - - // collect currencies. - $currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get(); - foreach ($currencies as $currency) { - $this->currencies[(int)$currency->id] = $currency; - } - - // collect accounts - $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']); - foreach ($set as $item) { - $id = (int)$item->piggy_bank_id; - $accountId = (int)$item->account_id; - $this->amounts[$id] ??= []; - if (!array_key_exists($id, $this->accountIds)) { - $this->accountIds[$id] = (int)$item->account_id; - } - if (!array_key_exists($accountId, $this->amounts[$id])) { - $this->amounts[$id][$accountId] = [ - 'current_amount' => '0', - 'pc_current_amount' => '0', - ]; - } - $this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string) $item->current_amount); - if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) { - $this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], (string) $item->native_current_amount); - } - } - - // get account currency preference for ALL. - $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get(); - - /** @var AccountMeta $item */ - foreach ($set as $item) { - $accountId = (int)$item->account_id; - $currencyId = (int)$item->data; - if (!array_key_exists($currencyId, $this->currencies)) { - $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId); - } - // $this->accountCurrencies[$accountId] = $this->currencies[$currencyId]; - } - - // get account info. - $set = Account::whereIn('id', array_values($this->accountIds))->get(); - - /** @var Account $item */ - foreach ($set as $item) { - $id = (int)$item->id; - $this->accounts[$id] = [ - 'id' => $id, - 'name' => $item->name, - ]; - } - } - private function appendCollectedData(): void { $this->collection = $this->collection->map(function (PiggyBank $item) { - $id = (int)$item->id; - $currencyId = (int)$item->transaction_currency_id; - $currency = $this->currencies[$currencyId] ?? $this->primaryCurrency; - $targetAmount = null; + $id = (int)$item->id; + $currencyId = (int)$item->transaction_currency_id; + $currency = $this->currencies[$currencyId] ?? $this->primaryCurrency; + $targetAmount = null; if (0 !== bccomp($item->target_amount, '0')) { $targetAmount = $item->target_amount; } - $meta = [ + $meta = [ 'notes' => $this->notes[$id] ?? null, 'currency' => $this->currencies[$currencyId] ?? null, // 'auto_budget' => $this->autoBudgets[$id] ?? null, @@ -193,23 +130,23 @@ class PiggyBankEnrichment implements EnrichmentInterface // add object group if available if (array_key_exists($id, $this->mappedObjects)) { $key = $this->mappedObjects[$id]; - $meta['object_group_id'] = (string) $this->objectGroups[$key]['id']; + $meta['object_group_id'] = (string)$this->objectGroups[$key]['id']; $meta['object_group_title'] = $this->objectGroups[$key]['title']; $meta['object_group_order'] = $this->objectGroups[$key]['order']; } // add current amount(s). foreach ($this->amounts[$id] as $accountId => $row) { - $meta['accounts'][] = [ + $meta['accounts'][] = [ 'account_id' => (string)$accountId, 'name' => $this->accounts[$accountId]['name'] ?? '', 'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places), 'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places), ]; - $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']); + $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']); // only add pc_current_amount when the pc_current_amount is set $meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']); } - $meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places); + $meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places); // only round this number when pc_current_amount is set. $meta['pc_current_amount'] = null === $meta['pc_current_amount'] ? null : Steam::bcround($meta['pc_current_amount'], $this->primaryCurrency->decimal_places); @@ -223,19 +160,83 @@ class PiggyBankEnrichment implements EnrichmentInterface $meta['save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['target_amount'], $meta['current_amount']), $currency->decimal_places); $meta['pc_save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['pc_target_amount'], $meta['pc_current_amount']), $currency->decimal_places); - $item->meta = $meta; + $item->meta = $meta; return $item; }); } + private function collectCurrentAmounts(): void {} + + private function collectIds(): void + { + /** @var PiggyBank $piggy */ + foreach ($this->collection as $piggy) { + $id = (int)$piggy->id; + $this->ids[] = $id; + $this->currencyIds[$id] = (int)$piggy->transaction_currency_id; + } + $this->ids = array_unique($this->ids); + + // collect currencies. + $currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get(); + foreach ($currencies as $currency) { + $this->currencies[(int)$currency->id] = $currency; + } + + // collect accounts + $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']); + foreach ($set as $item) { + $id = (int)$item->piggy_bank_id; + $accountId = (int)$item->account_id; + $this->amounts[$id] ??= []; + if (!array_key_exists($id, $this->accountIds)) { + $this->accountIds[$id] = (int)$item->account_id; + } + if (!array_key_exists($accountId, $this->amounts[$id])) { + $this->amounts[$id][$accountId] = [ + 'current_amount' => '0', + 'pc_current_amount' => '0', + ]; + } + $this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string)$item->current_amount); + if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) { + $this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], (string)$item->native_current_amount); + } + } + + // get account currency preference for ALL. + $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get(); + + /** @var AccountMeta $item */ + foreach ($set as $item) { + $accountId = (int)$item->account_id; + $currencyId = (int)$item->data; + if (!array_key_exists($currencyId, $this->currencies)) { + $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId); + } + // $this->accountCurrencies[$accountId] = $this->currencies[$currencyId]; + } + + // get account info. + $set = Account::whereIn('id', array_values($this->accountIds))->get(); + + /** @var Account $item */ + foreach ($set as $item) { + $id = (int)$item->id; + $this->accounts[$id] = [ + 'id' => $id, + 'name' => $item->name, + ]; + } + } + private function collectNotes(): void { $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', PiggyBank::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', PiggyBank::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); foreach ($notes as $note) { $this->notes[(int)$note['noteable_id']] = (string)$note['text']; } @@ -244,13 +245,12 @@ class PiggyBankEnrichment implements EnrichmentInterface private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', PiggyBank::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', PiggyBank::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -264,8 +264,6 @@ class PiggyBankEnrichment implements EnrichmentInterface } } - private function collectCurrentAmounts(): void {} - /** * Returns the suggested amount the user should save per month, or "". */ diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php index fad6293f90..8758dfe94d 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php @@ -38,16 +38,16 @@ use Illuminate\Support\Facades\Log; class PiggyBankEventEnrichment implements EnrichmentInterface { - private User $user; // @phpstan-ignore-line - private UserGroup $userGroup; // @phpstan-ignore-line + private array $accountCurrencies = []; // @phpstan-ignore-line + private array $accountIds = []; // @phpstan-ignore-line private Collection $collection; + private array $currencies = []; + private array $groupIds = []; private array $ids = []; private array $journalIds = []; - private array $groupIds = []; - private array $accountIds = []; private array $piggyBankIds = []; - private array $accountCurrencies = []; - private array $currencies = []; + private User $user; + private UserGroup $userGroup; // private bool $convertToPrimary = false; // private TransactionCurrency $primaryCurrency; @@ -66,7 +66,7 @@ class PiggyBankEventEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -86,53 +86,13 @@ class PiggyBankEventEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } - private function collectIds(): void - { - /** @var PiggyBankEvent $event */ - foreach ($this->collection as $event) { - $this->ids[] = (int)$event->id; - $this->journalIds[(int)$event->id] = (int)$event->transaction_journal_id; - $this->piggyBankIds[(int)$event->id] = (int)$event->piggy_bank_id; - } - $this->ids = array_unique($this->ids); - // collect groups with journal info. - $set = TransactionJournal::whereIn('id', $this->journalIds)->get(['id', 'transaction_group_id']); - - /** @var TransactionJournal $item */ - foreach ($set as $item) { - $this->groupIds[(int)$item->id] = (int)$item->transaction_group_id; - } - - // collect account info. - $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->piggyBankIds)->get(['piggy_bank_id', 'account_id']); - foreach ($set as $item) { - $id = (int)$item->piggy_bank_id; - if (!array_key_exists($id, $this->accountIds)) { - $this->accountIds[$id] = (int)$item->account_id; - } - } - - // get account currency preference for ALL. - $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get(); - - /** @var AccountMeta $item */ - foreach ($set as $item) { - $accountId = (int)$item->account_id; - $currencyId = (int)$item->data; - if (!array_key_exists($currencyId, $this->currencies)) { - $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId); - } - $this->accountCurrencies[$accountId] = $this->currencies[$currencyId]; - } - } - private function appendCollectedData(): void { $this->collection = $this->collection->map(function (PiggyBankEvent $item) { - $id = (int)$item->id; - $piggyId = (int)$item->piggy_bank_id; - $journalId = (int)$item->transaction_journal_id; - $currency = null; + $id = (int)$item->id; + $piggyId = (int)$item->piggy_bank_id; + $journalId = (int)$item->transaction_journal_id; + $currency = null; if (array_key_exists($piggyId, $this->accountIds)) { $accountId = $this->accountIds[$piggyId]; if (array_key_exists($accountId, $this->accountCurrencies)) { @@ -149,4 +109,44 @@ class PiggyBankEventEnrichment implements EnrichmentInterface }); } + + private function collectIds(): void + { + /** @var PiggyBankEvent $event */ + foreach ($this->collection as $event) { + $this->ids[] = (int)$event->id; + $this->journalIds[(int)$event->id] = (int)$event->transaction_journal_id; + $this->piggyBankIds[(int)$event->id] = (int)$event->piggy_bank_id; + } + $this->ids = array_unique($this->ids); + // collect groups with journal info. + $set = TransactionJournal::whereIn('id', $this->journalIds)->get(['id', 'transaction_group_id']); + + /** @var TransactionJournal $item */ + foreach ($set as $item) { + $this->groupIds[(int)$item->id] = (int)$item->transaction_group_id; + } + + // collect account info. + $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->piggyBankIds)->get(['piggy_bank_id', 'account_id']); + foreach ($set as $item) { + $id = (int)$item->piggy_bank_id; + if (!array_key_exists($id, $this->accountIds)) { + $this->accountIds[$id] = (int)$item->account_id; + } + } + + // get account currency preference for ALL. + $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get(); + + /** @var AccountMeta $item */ + foreach ($set as $item) { + $accountId = (int)$item->account_id; + $currencyId = (int)$item->data; + if (!array_key_exists($currencyId, $this->currencies)) { + $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId); + } + $this->accountCurrencies[$accountId] = $this->currencies[$currencyId]; + } + } } diff --git a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php index c8a3d8707a..30484f5388 100644 --- a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php +++ b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php @@ -51,30 +51,29 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; - use function Safe\json_decode; class RecurringEnrichment implements EnrichmentInterface { - private Collection $collection; - private array $ids = []; + private array $accounts = []; + private Collection $collection; // private array $transactionTypeIds = []; // private array $transactionTypes = []; - private array $notes = []; - private array $repetitions = []; - private array $transactions = []; - private User $user; - private UserGroup $userGroup; - private string $language = 'en_US'; - private array $currencyIds = []; - private array $foreignCurrencyIds = []; - private array $sourceAccountIds = []; - private array $destinationAccountIds = []; - private array $accounts = []; - private array $currencies = []; - private array $recurrenceIds = []; + private bool $convertToPrimary = false; + private array $currencies = []; + private array $currencyIds = []; + private array $destinationAccountIds = []; + private array $foreignCurrencyIds = []; + private array $ids = []; + private string $language = 'en_US'; + private array $notes = []; private readonly TransactionCurrency $primaryCurrency; - private bool $convertToPrimary = false; + private array $recurrenceIds = []; + private array $repetitions = []; + private array $sourceAccountIds = []; + private array $transactions = []; + private User $user; + private UserGroup $userGroup; public function __construct() { @@ -98,7 +97,7 @@ class RecurringEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -107,139 +106,6 @@ class RecurringEnrichment implements EnrichmentInterface return $collection->first(); } - public function setUser(User $user): void - { - $this->user = $user; - $this->setUserGroup($user->userGroup); - $this->getLanguage(); - } - - public function setUserGroup(UserGroup $userGroup): void - { - $this->userGroup = $userGroup; - } - - private function collectIds(): void - { - /** @var Recurrence $recurrence */ - foreach ($this->collection as $recurrence) { - $id = (int)$recurrence->id; - // $typeId = (int)$recurrence->transaction_type_id; - $this->ids[] = $id; - // $this->transactionTypeIds[$id] = $typeId; - } - $this->ids = array_unique($this->ids); - - // collect transaction types. - // $transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get(); - // foreach ($transactionTypes as $transactionType) { - // $id = (int)$transactionType->id; - // $this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type); - // } - } - - private function collectRepetitions(): void - { - Log::debug('Start of enrichment: collectRepetitions()'); - $repository = app(RecurringRepositoryInterface::class); - $repository->setUserGroup($this->userGroup); - $set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get(); - - /** @var RecurrenceRepetition $repetition */ - foreach ($set as $repetition) { - $recurrence = $this->collection->filter(fn (Recurrence $item) => (int)$item->id === (int)$repetition->recurrence_id)->first(); - $fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date); - $id = (int)$repetition->recurrence_id; - $repId = (int)$repetition->id; - $this->repetitions[$id] ??= []; - - // get the (future) occurrences for this specific type of repetition: - $amount = 'daily' === $repetition->repetition_type ? 9 : 5; - $set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount); - $occurrences = []; - - /** @var Carbon $carbon */ - foreach ($set as $carbon) { - $occurrences[] = $carbon->toAtomString(); - } - $this->repetitions[$id][$repId] = [ - 'id' => (string)$repId, - 'created_at' => $repetition->created_at->toAtomString(), - 'updated_at' => $repetition->updated_at->toAtomString(), - 'type' => $repetition->repetition_type, - 'moment' => (string)$repetition->repetition_moment, - 'skip' => (int)$repetition->repetition_skip, - 'weekend' => RecurrenceRepetitionWeekend::from((int)$repetition->weekend)->value, - 'description' => $this->getRepetitionDescription($repetition), - 'occurrences' => $occurrences, - ]; - } - Log::debug('End of enrichment: collectRepetitions()'); - } - - private function collectTransactions(): void - { - $set = RecurrenceTransaction::whereIn('recurrence_id', $this->ids)->get(); - - /** @var RecurrenceTransaction $transaction */ - foreach ($set as $transaction) { - $id = (int)$transaction->recurrence_id; - $transactionId = (int)$transaction->id; - $this->recurrenceIds[$transactionId] = $id; - $this->transactions[$id] ??= []; - $amount = $transaction->amount; - $foreignAmount = $transaction->foreign_amount; - - $this->transactions[$id][$transactionId] = [ - 'id' => (string)$transactionId, - // 'recurrence_id' => $id, - 'transaction_currency_id' => (int)$transaction->transaction_currency_id, - 'foreign_currency_id' => null === $transaction->foreign_currency_id ? null : (int)$transaction->foreign_currency_id, - 'source_id' => (int)$transaction->source_id, - 'object_has_currency_setting' => true, - 'destination_id' => (int)$transaction->destination_id, - 'amount' => $amount, - 'foreign_amount' => $foreignAmount, - 'pc_amount' => null, - 'pc_foreign_amount' => null, - 'description' => $transaction->description, - 'tags' => [], - 'category_id' => null, - 'category_name' => null, - 'budget_id' => null, - 'budget_name' => null, - 'piggy_bank_id' => null, - 'piggy_bank_name' => null, - 'subscription_id' => null, - 'subscription_name' => null, - - ]; - // collect all kinds of meta data to be collected later. - $this->currencyIds[$transactionId] = (int)$transaction->transaction_currency_id; - $this->sourceAccountIds[$transactionId] = (int)$transaction->source_id; - $this->destinationAccountIds[$transactionId] = (int)$transaction->destination_id; - if (null !== $transaction->foreign_currency_id) { - $this->foreignCurrencyIds[$transactionId] = (int)$transaction->foreign_currency_id; - } - } - } - - private function appendCollectedData(): void - { - $this->collection = $this->collection->map(function (Recurrence $item) { - $id = (int)$item->id; - $meta = [ - 'notes' => $this->notes[$id] ?? null, - 'repetitions' => array_values($this->repetitions[$id] ?? []), - 'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])), - ]; - - $item->meta = $meta; - - return $item; - }); - } - /** * Parse the repetition in a string that is user readable. * TODO duplicate with repository. @@ -265,7 +131,7 @@ class RecurringEnrichment implements EnrichmentInterface return (string)trans('firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1], $this->language); } if ('ndom' === $repetition->repetition_type) { - $parts = explode(',', $repetition->repetition_moment); + $parts = explode(',', $repetition->repetition_moment); // first part is number of week, second is weekday. $dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $this->language); if ($repetition->repetition_skip > 0) { @@ -282,7 +148,7 @@ class RecurringEnrichment implements EnrichmentInterface } // $diffInYears = (int)$today->diffInYears($repDate, true); // $repDate->addYears($diffInYears); // technically not necessary. - $string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js')); + $string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js')); return (string)trans('firefly.recurring_yearly', ['date' => $string], $this->language); } @@ -290,96 +156,32 @@ class RecurringEnrichment implements EnrichmentInterface return ''; } - private function getLanguage(): void + public function setUser(User $user): void { - /** @var Preference $preference */ - $preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US')); - $language = $preference->data; - if (is_array($language)) { - $language = 'en_US'; - } - $language = (string)$language; - $this->language = $language; + $this->user = $user; + $this->setUserGroup($user->userGroup); + $this->getLanguage(); } - private function collectCurrencies(): void + public function setUserGroup(UserGroup $userGroup): void { - $all = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds)); - $currencies = TransactionCurrency::whereIn('id', array_unique($all))->get(); - foreach ($currencies as $currency) { - $id = (int)$currency->id; - $this->currencies[$id] = $currency; - } + $this->userGroup = $userGroup; } - private function processTransactions(array $transactions): array + private function appendCollectedData(): void { - $return = []; - $converter = new ExchangeRateConverter(); - foreach ($transactions as $transaction) { - $currencyId = $transaction['transaction_currency_id']; - $pcAmount = null; - $pcForeignAmount = null; - // set the same amount in the primary currency, if both are the same anyway. - if (true === $this->convertToPrimary && $currencyId === (int)$this->primaryCurrency->id) { - $pcAmount = $transaction['amount']; - } - // convert the amount to the primary currency, if it is not the same. - if (true === $this->convertToPrimary && $currencyId !== (int)$this->primaryCurrency->id) { - $pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']); - } - if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) { - $foreignCurrencyId = $transaction['foreign_currency_id']; - if ($foreignCurrencyId !== $this->primaryCurrency->id) { - $pcForeignAmount = $converter->convert($this->currencies[$foreignCurrencyId], $this->primaryCurrency, today(), $transaction['foreign_amount']); - } - } + $this->collection = $this->collection->map(function (Recurrence $item) { + $id = (int)$item->id; + $meta = [ + 'notes' => $this->notes[$id] ?? null, + 'repetitions' => array_values($this->repetitions[$id] ?? []), + 'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])), + ]; - $transaction['pc_amount'] = $pcAmount; - $transaction['pc_foreign_amount'] = $pcForeignAmount; + $item->meta = $meta; - $sourceId = $transaction['source_id']; - $transaction['source_name'] = $this->accounts[$sourceId]->name; - $transaction['source_iban'] = $this->accounts[$sourceId]->iban; - $transaction['source_type'] = $this->accounts[$sourceId]->accountType->type; - $transaction['source_id'] = (string)$transaction['source_id']; - - $destId = $transaction['destination_id']; - $transaction['destination_name'] = $this->accounts[$destId]->name; - $transaction['destination_iban'] = $this->accounts[$destId]->iban; - $transaction['destination_type'] = $this->accounts[$destId]->accountType->type; - $transaction['destination_id'] = (string)$transaction['destination_id']; - - $transaction['currency_id'] = (string)$currencyId; - $transaction['currency_name'] = $this->currencies[$currencyId]->name; - $transaction['currency_code'] = $this->currencies[$currencyId]->code; - $transaction['currency_symbol'] = $this->currencies[$currencyId]->symbol; - $transaction['currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places; - - $transaction['primary_currency_id'] = (string)$this->primaryCurrency->id; - $transaction['primary_currency_name'] = $this->primaryCurrency->name; - $transaction['primary_currency_code'] = $this->primaryCurrency->code; - $transaction['primary_currency_symbol'] = $this->primaryCurrency->symbol; - $transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places; - - // $transaction['foreign_currency_id'] = null; - $transaction['foreign_currency_name'] = null; - $transaction['foreign_currency_code'] = null; - $transaction['foreign_currency_symbol'] = null; - $transaction['foreign_currency_decimal_places'] = null; - if (null !== $transaction['foreign_currency_id']) { - $currencyId = $transaction['foreign_currency_id']; - $transaction['foreign_currency_id'] = (string)$currencyId; - $transaction['foreign_currency_name'] = $this->currencies[$currencyId]->name; - $transaction['foreign_currency_code'] = $this->currencies[$currencyId]->code; - $transaction['foreign_currency_symbol'] = $this->currencies[$currencyId]->symbol; - $transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places; - } - unset($transaction['transaction_currency_id']); - $return[] = $transaction; - } - - return $return; + return $item; + }); } private function collectAccounts(): void @@ -394,10 +196,183 @@ class RecurringEnrichment implements EnrichmentInterface } } + private function collectBillInfo(array $billIds): void + { + if (0 === count($billIds)) { + return; + } + $ids = Arr::pluck($billIds, 'bill_id'); + $bills = Bill::whereIn('id', $ids)->get(); + $mapped = []; + foreach ($bills as $bill) { + $mapped[(int)$bill->id] = $bill; + } + foreach ($billIds as $info) { + $recurrenceId = $info['recurrence_id']; + $transactionId = $info['transaction_id']; + $this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? ''; + } + } + + private function collectBudgetInfo(array $budgetIds): void + { + if (0 === count($budgetIds)) { + return; + } + $ids = Arr::pluck($budgetIds, 'budget_id'); + $categories = Budget::whereIn('id', $ids)->get(); + $mapped = []; + foreach ($categories as $category) { + $mapped[(int)$category->id] = $category; + } + foreach ($budgetIds as $info) { + $recurrenceId = $info['recurrence_id']; + $transactionId = $info['transaction_id']; + $this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? ''; + } + } + + private function collectCategoryIdInfo(array $categoryIds): void + { + if (0 === count($categoryIds)) { + return; + } + $ids = Arr::pluck($categoryIds, 'category_id'); + $categories = Category::whereIn('id', $ids)->get(); + $mapped = []; + foreach ($categories as $category) { + $mapped[(int)$category->id] = $category; + } + foreach ($categoryIds as $info) { + $recurrenceId = $info['recurrence_id']; + $transactionId = $info['transaction_id']; + $this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? ''; + } + } + + /** + * TODO This method does look-up in a loop. + */ + private function collectCategoryNameInfo(array $categoryNames): void + { + if (0 === count($categoryNames)) { + return; + } + $factory = app(CategoryFactory::class); + $factory->setUser($this->user); + foreach ($categoryNames as $info) { + $recurrenceId = $info['recurrence_id']; + $transactionId = $info['transaction_id']; + $category = $factory->findOrCreate(null, $info['category_name']); + if (null !== $category) { + $this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$category->id; + $this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name; + } + } + } + + private function collectCurrencies(): void + { + $all = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds)); + $currencies = TransactionCurrency::whereIn('id', array_unique($all))->get(); + foreach ($currencies as $currency) { + $id = (int)$currency->id; + $this->currencies[$id] = $currency; + } + } + + private function collectIds(): void + { + /** @var Recurrence $recurrence */ + foreach ($this->collection as $recurrence) { + $id = (int)$recurrence->id; + // $typeId = (int)$recurrence->transaction_type_id; + $this->ids[] = $id; + // $this->transactionTypeIds[$id] = $typeId; + } + $this->ids = array_unique($this->ids); + + // collect transaction types. + // $transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get(); + // foreach ($transactionTypes as $transactionType) { + // $id = (int)$transactionType->id; + // $this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type); + // } + } + + private function collectNotes(): void + { + $notes = Note::query()->whereIn('noteable_id', $this->ids) + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Recurrence::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + foreach ($notes as $note) { + $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + } + Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); + } + + private function collectPiggyBankInfo(array $piggyBankIds): void + { + if (0 === count($piggyBankIds)) { + return; + } + $ids = Arr::pluck($piggyBankIds, 'piggy_bank_id'); + $piggyBanks = PiggyBank::whereIn('id', $ids)->get(); + $mapped = []; + foreach ($piggyBanks as $piggyBank) { + $mapped[(int)$piggyBank->id] = $piggyBank; + } + foreach ($piggyBankIds as $info) { + $recurrenceId = $info['recurrence_id']; + $transactionId = $info['transaction_id']; + $this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? ''; + } + } + + private function collectRepetitions(): void + { + Log::debug('Start of enrichment: collectRepetitions()'); + $repository = app(RecurringRepositoryInterface::class); + $repository->setUserGroup($this->userGroup); + $set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get(); + + /** @var RecurrenceRepetition $repetition */ + foreach ($set as $repetition) { + $recurrence = $this->collection->filter(fn(Recurrence $item) => (int)$item->id === (int)$repetition->recurrence_id)->first(); + $fromDate = clone($recurrence->latest_date ?? $recurrence->first_date); + $id = (int)$repetition->recurrence_id; + $repId = (int)$repetition->id; + $this->repetitions[$id] ??= []; + + // get the (future) occurrences for this specific type of repetition: + $amount = 'daily' === $repetition->repetition_type ? 9 : 5; + $set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount); + $occurrences = []; + + /** @var Carbon $carbon */ + foreach ($set as $carbon) { + $occurrences[] = $carbon->toAtomString(); + } + $this->repetitions[$id][$repId] = [ + 'id' => (string)$repId, + 'created_at' => $repetition->created_at->toAtomString(), + 'updated_at' => $repetition->updated_at->toAtomString(), + 'type' => $repetition->repetition_type, + 'moment' => (string)$repetition->repetition_moment, + 'skip' => (int)$repetition->repetition_skip, + 'weekend' => RecurrenceRepetitionWeekend::from((int)$repetition->weekend)->value, + 'description' => $this->getRepetitionDescription($repetition), + 'occurrences' => $occurrences, + ]; + } + Log::debug('End of enrichment: collectRepetitions()'); + } + private function collectTransactionMetaData(): void { - $ids = array_keys($this->transactions); - $meta = RecurrenceTransactionMeta::whereNull('deleted_at')->whereIn('rt_id', $ids)->get(); + $ids = array_keys($this->transactions); + $meta = RecurrenceTransactionMeta::whereNull('deleted_at')->whereIn('rt_id', $ids)->get(); // other meta-data to be collected: $billIds = []; $piggyBankIds = []; @@ -409,8 +384,8 @@ class RecurringEnrichment implements EnrichmentInterface $transactionId = (int)$entry->rt_id; // this should refer to another array, were rtIds can be used to find the recurrence. - $recurrenceId = $this->recurrenceIds[$transactionId] ?? 0; - $name = (string)($entry->name ?? ''); + $recurrenceId = $this->recurrenceIds[$transactionId] ?? 0; + $name = (string)($entry->name ?? ''); if (0 === $recurrenceId) { Log::error(sprintf('Could not find recurrence ID for recurrence transaction ID %d', $transactionId)); @@ -504,109 +479,132 @@ class RecurringEnrichment implements EnrichmentInterface $this->collectBudgetInfo($budgetIds); } - private function collectBillInfo(array $billIds): void + private function collectTransactions(): void { - if (0 === count($billIds)) { - return; - } - $ids = Arr::pluck($billIds, 'bill_id'); - $bills = Bill::whereIn('id', $ids)->get(); - $mapped = []; - foreach ($bills as $bill) { - $mapped[(int)$bill->id] = $bill; - } - foreach ($billIds as $info) { - $recurrenceId = $info['recurrence_id']; - $transactionId = $info['transaction_id']; - $this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? ''; - } - } + $set = RecurrenceTransaction::whereIn('recurrence_id', $this->ids)->get(); - private function collectPiggyBankInfo(array $piggyBankIds): void - { - if (0 === count($piggyBankIds)) { - return; - } - $ids = Arr::pluck($piggyBankIds, 'piggy_bank_id'); - $piggyBanks = PiggyBank::whereIn('id', $ids)->get(); - $mapped = []; - foreach ($piggyBanks as $piggyBank) { - $mapped[(int)$piggyBank->id] = $piggyBank; - } - foreach ($piggyBankIds as $info) { - $recurrenceId = $info['recurrence_id']; - $transactionId = $info['transaction_id']; - $this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? ''; - } - } + /** @var RecurrenceTransaction $transaction */ + foreach ($set as $transaction) { + $id = (int)$transaction->recurrence_id; + $transactionId = (int)$transaction->id; + $this->recurrenceIds[$transactionId] = $id; + $this->transactions[$id] ??= []; + $amount = $transaction->amount; + $foreignAmount = $transaction->foreign_amount; - private function collectCategoryIdInfo(array $categoryIds): void - { - if (0 === count($categoryIds)) { - return; - } - $ids = Arr::pluck($categoryIds, 'category_id'); - $categories = Category::whereIn('id', $ids)->get(); - $mapped = []; - foreach ($categories as $category) { - $mapped[(int)$category->id] = $category; - } - foreach ($categoryIds as $info) { - $recurrenceId = $info['recurrence_id']; - $transactionId = $info['transaction_id']; - $this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? ''; - } - } + $this->transactions[$id][$transactionId] = [ + 'id' => (string)$transactionId, + // 'recurrence_id' => $id, + 'transaction_currency_id' => (int)$transaction->transaction_currency_id, + 'foreign_currency_id' => null === $transaction->foreign_currency_id ? null : (int)$transaction->foreign_currency_id, + 'source_id' => (int)$transaction->source_id, + 'object_has_currency_setting' => true, + 'destination_id' => (int)$transaction->destination_id, + 'amount' => $amount, + 'foreign_amount' => $foreignAmount, + 'pc_amount' => null, + 'pc_foreign_amount' => null, + 'description' => $transaction->description, + 'tags' => [], + 'category_id' => null, + 'category_name' => null, + 'budget_id' => null, + 'budget_name' => null, + 'piggy_bank_id' => null, + 'piggy_bank_name' => null, + 'subscription_id' => null, + 'subscription_name' => null, - /** - * TODO This method does look-up in a loop. - */ - private function collectCategoryNameInfo(array $categoryNames): void - { - if (0 === count($categoryNames)) { - return; - } - $factory = app(CategoryFactory::class); - $factory->setUser($this->user); - foreach ($categoryNames as $info) { - $recurrenceId = $info['recurrence_id']; - $transactionId = $info['transaction_id']; - $category = $factory->findOrCreate(null, $info['category_name']); - if (null !== $category) { - $this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$category->id; - $this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name; + ]; + // collect all kinds of meta data to be collected later. + $this->currencyIds[$transactionId] = (int)$transaction->transaction_currency_id; + $this->sourceAccountIds[$transactionId] = (int)$transaction->source_id; + $this->destinationAccountIds[$transactionId] = (int)$transaction->destination_id; + if (null !== $transaction->foreign_currency_id) { + $this->foreignCurrencyIds[$transactionId] = (int)$transaction->foreign_currency_id; } } } - private function collectBudgetInfo(array $budgetIds): void + private function getLanguage(): void { - if (0 === count($budgetIds)) { - return; - } - $ids = Arr::pluck($budgetIds, 'budget_id'); - $categories = Budget::whereIn('id', $ids)->get(); - $mapped = []; - foreach ($categories as $category) { - $mapped[(int)$category->id] = $category; - } - foreach ($budgetIds as $info) { - $recurrenceId = $info['recurrence_id']; - $transactionId = $info['transaction_id']; - $this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? ''; + /** @var Preference $preference */ + $preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US')); + $language = $preference->data; + if (is_array($language)) { + $language = 'en_US'; } + $language = (string)$language; + $this->language = $language; } - private function collectNotes(): void + private function processTransactions(array $transactions): array { - $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Recurrence::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; - foreach ($notes as $note) { - $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + $return = []; + $converter = new ExchangeRateConverter(); + foreach ($transactions as $transaction) { + $currencyId = $transaction['transaction_currency_id']; + $pcAmount = null; + $pcForeignAmount = null; + // set the same amount in the primary currency, if both are the same anyway. + if (true === $this->convertToPrimary && $currencyId === (int)$this->primaryCurrency->id) { + $pcAmount = $transaction['amount']; + } + // convert the amount to the primary currency, if it is not the same. + if (true === $this->convertToPrimary && $currencyId !== (int)$this->primaryCurrency->id) { + $pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']); + } + if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) { + $foreignCurrencyId = $transaction['foreign_currency_id']; + if ($foreignCurrencyId !== $this->primaryCurrency->id) { + $pcForeignAmount = $converter->convert($this->currencies[$foreignCurrencyId], $this->primaryCurrency, today(), $transaction['foreign_amount']); + } + } + + $transaction['pc_amount'] = $pcAmount; + $transaction['pc_foreign_amount'] = $pcForeignAmount; + + $sourceId = $transaction['source_id']; + $transaction['source_name'] = $this->accounts[$sourceId]->name; + $transaction['source_iban'] = $this->accounts[$sourceId]->iban; + $transaction['source_type'] = $this->accounts[$sourceId]->accountType->type; + $transaction['source_id'] = (string)$transaction['source_id']; + + $destId = $transaction['destination_id']; + $transaction['destination_name'] = $this->accounts[$destId]->name; + $transaction['destination_iban'] = $this->accounts[$destId]->iban; + $transaction['destination_type'] = $this->accounts[$destId]->accountType->type; + $transaction['destination_id'] = (string)$transaction['destination_id']; + + $transaction['currency_id'] = (string)$currencyId; + $transaction['currency_name'] = $this->currencies[$currencyId]->name; + $transaction['currency_code'] = $this->currencies[$currencyId]->code; + $transaction['currency_symbol'] = $this->currencies[$currencyId]->symbol; + $transaction['currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places; + + $transaction['primary_currency_id'] = (string)$this->primaryCurrency->id; + $transaction['primary_currency_name'] = $this->primaryCurrency->name; + $transaction['primary_currency_code'] = $this->primaryCurrency->code; + $transaction['primary_currency_symbol'] = $this->primaryCurrency->symbol; + $transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places; + + // $transaction['foreign_currency_id'] = null; + $transaction['foreign_currency_name'] = null; + $transaction['foreign_currency_code'] = null; + $transaction['foreign_currency_symbol'] = null; + $transaction['foreign_currency_decimal_places'] = null; + if (null !== $transaction['foreign_currency_id']) { + $currencyId = $transaction['foreign_currency_id']; + $transaction['foreign_currency_id'] = (string)$currencyId; + $transaction['foreign_currency_name'] = $this->currencies[$currencyId]->name; + $transaction['foreign_currency_code'] = $this->currencies[$currencyId]->code; + $transaction['foreign_currency_symbol'] = $this->currencies[$currencyId]->symbol; + $transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places; + } + unset($transaction['transaction_currency_id']); + $return[] = $transaction; } - Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); + + return $return; } } diff --git a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php index 06388a80bc..285e0ad37e 100644 --- a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php +++ b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php @@ -46,20 +46,20 @@ use Illuminate\Support\Facades\Log; class SubscriptionEnrichment implements EnrichmentInterface { - private User $user; - private UserGroup $userGroup; // @phpstan-ignore-line - private Collection $collection; + private BillDateCalculator $calculator; + private Collection $collection; // @phpstan-ignore-line private readonly bool $convertToPrimary; - private ?Carbon $start = null; - private ?Carbon $end = null; - private array $subscriptionIds = []; - private array $objectGroups = []; - private array $mappedObjects = []; - private array $paidDates = []; - private array $notes = []; - private array $payDates = []; + private ?Carbon $end = null; + private array $mappedObjects = []; + private array $notes = []; + private array $objectGroups = []; + private array $paidDates = []; + private array $payDates = []; private readonly TransactionCurrency $primaryCurrency; - private BillDateCalculator $calculator; + private ?Carbon $start = null; + private array $subscriptionIds = []; + private User $user; + private UserGroup $userGroup; public function __construct() { @@ -86,11 +86,11 @@ class SubscriptionEnrichment implements EnrichmentInterface $paidDates = $this->paidDates; $payDates = $this->payDates; $this->collection = $this->collection->map(function (Bill $item) use ($notes, $objectGroups, $paidDates, $payDates) { - $id = (int)$item->id; - $currency = $item->transactionCurrency; - $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); + $id = (int)$item->id; + $currency = $item->transactionCurrency; + $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); - $meta = [ + $meta = [ 'notes' => null, 'object_group_id' => null, 'object_group_title' => null, @@ -101,7 +101,7 @@ class SubscriptionEnrichment implements EnrichmentInterface 'nem' => $nem, 'nem_diff' => $this->getNextExpectedMatchDiff($nem, $payDates[$id] ?? []), ]; - $amounts = [ + $amounts = [ 'amount_min' => Steam::bcround($item->amount_min, $currency->decimal_places), 'amount_max' => Steam::bcround($item->amount_max, $currency->decimal_places), 'average' => Steam::bcround(bcdiv(bcadd($item->amount_min, $item->amount_max), '2'), $currency->decimal_places), @@ -142,7 +142,7 @@ class SubscriptionEnrichment implements EnrichmentInterface return $collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -151,17 +151,14 @@ class SubscriptionEnrichment implements EnrichmentInterface return $collection->first(); } - private function collectNotes(): void + public function setEnd(?Carbon $end): void { - $notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; - foreach ($notes as $note) { - $this->notes[(int)$note['noteable_id']] = (string)$note['text']; - } - Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); + $this->end = $end; + } + + public function setStart(?Carbon $start): void + { + $this->start = $start; } public function setUser(User $user): void @@ -175,24 +172,49 @@ class SubscriptionEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } - private function collectSubscriptionIds(): void + /** + * Returns the latest date in the set, or start when set is empty. + */ + protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon { - /** @var Bill $bill */ - foreach ($this->collection as $bill) { - $this->subscriptionIds[] = (int)$bill->id; + $filtered = $dates->filter(fn(TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); + Log::debug(sprintf('Filtered down from %d to %d entries for bill #%d.', $dates->count(), $filtered->count(), $subscription->id)); + if (0 === $filtered->count()) { + return $default; } - $this->subscriptionIds = array_unique($this->subscriptionIds); + + $latest = $filtered->first()->date; + + /** @var TransactionJournal $journal */ + foreach ($filtered as $journal) { + if ($journal->date->gte($latest)) { + $latest = $journal->date; + } + } + + return $latest; + } + + private function collectNotes(): void + { + $notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds) + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + foreach ($notes as $note) { + $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + } + Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); } private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->subscriptionIds) - ->where('object_groupable_type', Bill::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->subscriptionIds) + ->where('object_groupable_type', Bill::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -220,13 +242,13 @@ class SubscriptionEnrichment implements EnrichmentInterface // 2023-07-18 this particular date is used to search for the last paid date. // 2023-07-18 the cloned $searchDate is used to grab the correct transactions. /** @var Carbon $start */ - $start = clone $this->start; - $searchStart = clone $start; + $start = clone $this->start; + $searchStart = clone $start; $start->subDay(); /** @var Carbon $end */ - $end = clone $this->end; - $searchEnd = clone $end; + $end = clone $this->end; + $searchEnd = clone $end; // move the search dates to the start of the day. $searchStart->startOfDay(); @@ -235,13 +257,13 @@ class SubscriptionEnrichment implements EnrichmentInterface Log::debug(sprintf('Search parameters are: start: %s, end: %s', $searchStart->format('Y-m-d H:i:s'), $searchEnd->format('Y-m-d H:i:s'))); // Get from database when bills were paid. - $set = $this->user->transactionJournals() - ->whereIn('bill_id', $this->subscriptionIds) - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') - ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') - ->where('transactions.amount', '>', 0) - ->before($searchEnd)->after($searchStart)->get( + $set = $this->user->transactionJournals() + ->whereIn('bill_id', $this->subscriptionIds) + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') + ->where('transactions.amount', '>', 0) + ->before($searchEnd)->after($searchStart)->get( [ 'transaction_journals.id', 'transaction_journals.date', @@ -258,25 +280,24 @@ class SubscriptionEnrichment implements EnrichmentInterface 'transactions.amount', 'transactions.foreign_amount', ] - ) - ; + ); Log::debug(sprintf('Count %d entries in set', $set->count())); // for each bill, do a loop. - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); /** @var Bill $subscription */ foreach ($this->collection as $subscription) { // Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date. Log::debug(sprintf('Grab last paid date from function, return %s if it comes up with nothing.', $start->format('Y-m-d'))); - $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); + $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); Log::debug(sprintf('Result of lastPaidDate is %s', $lastPaidDate->format('Y-m-d'))); // At this point the "next match" is exactly after the last time the bill was paid. - $result = []; - $filtered = $set->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); + $result = []; + $filtered = $set->filter(fn(TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); foreach ($filtered as $entry) { - $array = [ + $array = [ 'transaction_group_id' => (string)$entry->transaction_group_id, 'transaction_journal_id' => (string)$entry->id, 'date' => $entry->date->toAtomString(), @@ -329,37 +350,47 @@ class SubscriptionEnrichment implements EnrichmentInterface } - public function setStart(?Carbon $start): void + private function collectPayDates(): void { - $this->start = $start; - } + if (!$this->start instanceof Carbon || !$this->end instanceof Carbon) { + Log::debug('Parameters are NULL, set empty array'); - public function setEnd(?Carbon $end): void - { - $this->end = $end; - } - - /** - * Returns the latest date in the set, or start when set is empty. - */ - protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon - { - $filtered = $dates->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); - Log::debug(sprintf('Filtered down from %d to %d entries for bill #%d.', $dates->count(), $filtered->count(), $subscription->id)); - if (0 === $filtered->count()) { - return $default; + return; } - $latest = $filtered->first()->date; - - /** @var TransactionJournal $journal */ - foreach ($filtered as $journal) { - if ($journal->date->gte($latest)) { - $latest = $journal->date; + /** @var Bill $subscription */ + foreach ($this->collection as $subscription) { + $id = (int)$subscription->id; + $lastPaidDate = $this->getLastPaidDate($this->paidDates[$id] ?? []); + $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); + $payDatesFormatted = []; + foreach ($payDates as $string) { + $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); + if (!$date instanceof Carbon) { + $date = today(config('app.timezone')); + } + $payDatesFormatted[] = $date->toAtomString(); } + $this->payDates[$id] = $payDatesFormatted; } + } - return $latest; + private function collectSubscriptionIds(): void + { + /** @var Bill $bill */ + foreach ($this->collection as $bill) { + $this->subscriptionIds[] = (int)$bill->id; + } + $this->subscriptionIds = array_unique($this->subscriptionIds); + } + + private function filterPaidDates(array $entries): array + { + return array_map(function (array $entry) { + unset($entry['date_object']); + + return $entry; + }, $entries); } private function getLastPaidDate(array $paidData): ?Carbon @@ -386,40 +417,6 @@ class SubscriptionEnrichment implements EnrichmentInterface return $return; } - private function collectPayDates(): void - { - if (!$this->start instanceof Carbon || !$this->end instanceof Carbon) { - Log::debug('Parameters are NULL, set empty array'); - - return; - } - - /** @var Bill $subscription */ - foreach ($this->collection as $subscription) { - $id = (int)$subscription->id; - $lastPaidDate = $this->getLastPaidDate($this->paidDates[$id] ?? []); - $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); - $payDatesFormatted = []; - foreach ($payDates as $string) { - $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); - if (!$date instanceof Carbon) { - $date = today(config('app.timezone')); - } - $payDatesFormatted[] = $date->toAtomString(); - } - $this->payDates[$id] = $payDatesFormatted; - } - } - - private function filterPaidDates(array $entries): array - { - return array_map(function (array $entry) { - unset($entry['date_object']); - - return $entry; - }, $entries); - } - private function getNextExpectedMatch(array $payDates): ?Carbon { // next expected match diff --git a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php index d322331135..014aff8e68 100644 --- a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php +++ b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php @@ -45,17 +45,17 @@ use Override; class TransactionGroupEnrichment implements EnrichmentInterface { - private array $attachmentCount = []; - private Collection $collection; - private readonly array $dateFields; - private array $journalIds = []; - private array $locations = []; - private array $metaData = []; - private array $notes = []; - private array $tags = []; - private User $user; // @phpstan-ignore-line + private array $attachmentCount = []; + private Collection $collection; + private readonly array $dateFields; + private array $journalIds = []; + private array $locations = []; + private array $metaData = []; + private array $notes = []; private readonly TransactionCurrency $primaryCurrency; - private UserGroup $userGroup; // @phpstan-ignore-line + private array $tags = []; // @phpstan-ignore-line + private User $user; + private UserGroup $userGroup; // @phpstan-ignore-line public function __construct() { @@ -63,20 +63,6 @@ class TransactionGroupEnrichment implements EnrichmentInterface $this->primaryCurrency = Amount::getPrimaryCurrency(); } - #[Override] - public function enrichSingle(array|Model $model): array|TransactionGroup - { - Log::debug(__METHOD__); - if (is_array($model)) { - $collection = new Collection()->push($model); - $collection = $this->enrich($collection); - - return $collection->first(); - } - - throw new FireflyException('Cannot enrich single model.'); - } - #[Override] public function enrich(Collection $collection): Collection { @@ -96,119 +82,55 @@ class TransactionGroupEnrichment implements EnrichmentInterface return $this->collection; } - private function collectJournalIds(): void + #[Override] + public function enrichSingle(array | Model $model): array | TransactionGroup { - /** @var array $group */ - foreach ($this->collection as $group) { - foreach ($group['transactions'] as $journal) { - $this->journalIds[] = $journal['transaction_journal_id']; - } + Log::debug(__METHOD__); + if (is_array($model)) { + $collection = new Collection()->push($model); + $collection = $this->enrich($collection); + + return $collection->first(); } - $this->journalIds = array_unique($this->journalIds); + + throw new FireflyException('Cannot enrich single model.'); } - private function collectNotes(): void + public function setUser(User $user): void { - $notes = Note::query()->whereIn('noteable_id', $this->journalIds) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', TransactionJournal::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; - foreach ($notes as $note) { - $this->notes[(int) $note['noteable_id']] = (string) $note['text']; - } - Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); + $this->user = $user; + $this->userGroup = $user->userGroup; } - private function collectTags(): void + public function setUserGroup(UserGroup $userGroup): void { - $set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id') - ->whereIn('tag_transaction_journal.transaction_journal_id', $this->journalIds) - ->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag'])->toArray() - ; - foreach ($set as $item) { - $journalId = $item['transaction_journal_id']; - $this->tags[$journalId] ??= []; - $this->tags[$journalId][] = $item['tag']; - } - } - - private function collectMetaData(): void - { - $set = TransactionJournalMeta::whereIn('transaction_journal_id', $this->journalIds)->get(['transaction_journal_id', 'name', 'data'])->toArray(); - foreach ($set as $entry) { - $name = $entry['name']; - $data = (string) $entry['data']; - if ('' === $data) { - continue; - } - if (in_array($name, $this->dateFields, true)) { - // Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data)); - $this->metaData[$entry['transaction_journal_id']][$name] = Carbon::parse($data, config('app.timezone')); - // Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString())); - - continue; - } - $this->metaData[(int) $entry['transaction_journal_id']][$name] = $data; - } - } - - private function collectLocations(): void - { - $locations = Location::query()->whereIn('locatable_id', $this->journalIds) - ->where('locatable_type', TransactionJournal::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray() - ; - foreach ($locations as $location) { - $this->locations[(int) $location['locatable_id']] - = [ - 'latitude' => (float) $location['latitude'], - 'longitude' => (float) $location['longitude'], - 'zoom_level' => (int) $location['zoom_level'], - ]; - } - Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations))); - } - - private function collectAttachmentCount(): void - { - // select count(id) as nr_of_attachments, attachable_id from attachments - // group by attachable_id - $attachments = Attachment::query() - ->whereIn('attachable_id', $this->journalIds) - ->where('attachable_type', TransactionJournal::class) - ->groupBy('attachable_id') - ->get(['attachable_id', DB::raw('COUNT(id) as nr_of_attachments')]) - ->toArray() - ; - foreach ($attachments as $row) { - $this->attachmentCount[(int) $row['attachable_id']] = (int) $row['nr_of_attachments']; - } + $this->userGroup = $userGroup; } private function appendCollectedData(): void { - $notes = $this->notes; - $tags = $this->tags; - $metaData = $this->metaData; - $locations = $this->locations; - $attachmentCount = $this->attachmentCount; - $primaryCurrency = $this->primaryCurrency; + $notes = $this->notes; + $tags = $this->tags; + $metaData = $this->metaData; + $locations = $this->locations; + $attachmentCount = $this->attachmentCount; + $primaryCurrency = $this->primaryCurrency; $this->collection = $this->collection->map(function (array $item) use ($primaryCurrency, $notes, $tags, $metaData, $locations, $attachmentCount) { foreach ($item['transactions'] as $index => $transaction) { - $journalId = (int) $transaction['transaction_journal_id']; + $journalId = (int)$transaction['transaction_journal_id']; // attach notes if they exist: - $item['transactions'][$index]['notes'] = array_key_exists($journalId, $notes) ? $notes[$journalId] : null; + $item['transactions'][$index]['notes'] = array_key_exists($journalId, $notes) ? $notes[$journalId] : null; // attach tags if they exist: - $item['transactions'][$index]['tags'] = array_key_exists($journalId, $tags) ? $tags[$journalId] : []; + $item['transactions'][$index]['tags'] = array_key_exists($journalId, $tags) ? $tags[$journalId] : []; // attachment count $item['transactions'][$index]['attachment_count'] = array_key_exists($journalId, $attachmentCount) ? $attachmentCount[$journalId] : 0; // default location data - $item['transactions'][$index]['location'] = [ + $item['transactions'][$index]['location'] = [ 'latitude' => null, 'longitude' => null, 'zoom_level' => null, @@ -216,16 +138,16 @@ class TransactionGroupEnrichment implements EnrichmentInterface // primary currency $item['transactions'][$index]['primary_currency'] = [ - 'id' => (string) $primaryCurrency->id, - 'code' => $primaryCurrency->code, - 'name' => $primaryCurrency->name, - 'symbol' => $primaryCurrency->symbol, - 'decimal_places' => $primaryCurrency->decimal_places, + 'id' => (string)$primaryCurrency->id, + 'code' => $primaryCurrency->code, + 'name' => $primaryCurrency->name, + 'symbol' => $primaryCurrency->symbol, + 'decimal_places' => $primaryCurrency->decimal_places, ]; // append meta data - $item['transactions'][$index]['meta'] = []; - $item['transactions'][$index]['meta_date'] = []; + $item['transactions'][$index]['meta'] = []; + $item['transactions'][$index]['meta_date'] = []; if (array_key_exists($journalId, $metaData)) { // loop al meta data: foreach ($metaData[$journalId] as $name => $value) { @@ -248,14 +170,88 @@ class TransactionGroupEnrichment implements EnrichmentInterface }); } - public function setUser(User $user): void + private function collectAttachmentCount(): void { - $this->user = $user; - $this->userGroup = $user->userGroup; + // select count(id) as nr_of_attachments, attachable_id from attachments + // group by attachable_id + $attachments = Attachment::query() + ->whereIn('attachable_id', $this->journalIds) + ->where('attachable_type', TransactionJournal::class) + ->groupBy('attachable_id') + ->get(['attachable_id', DB::raw('COUNT(id) as nr_of_attachments')]) + ->toArray(); + foreach ($attachments as $row) { + $this->attachmentCount[(int)$row['attachable_id']] = (int)$row['nr_of_attachments']; + } } - public function setUserGroup(UserGroup $userGroup): void + private function collectJournalIds(): void { - $this->userGroup = $userGroup; + /** @var array $group */ + foreach ($this->collection as $group) { + foreach ($group['transactions'] as $journal) { + $this->journalIds[] = $journal['transaction_journal_id']; + } + } + $this->journalIds = array_unique($this->journalIds); + } + + private function collectLocations(): void + { + $locations = Location::query()->whereIn('locatable_id', $this->journalIds) + ->where('locatable_type', TransactionJournal::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray(); + foreach ($locations as $location) { + $this->locations[(int)$location['locatable_id']] + = [ + 'latitude' => (float)$location['latitude'], + 'longitude' => (float)$location['longitude'], + 'zoom_level' => (int)$location['zoom_level'], + ]; + } + Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations))); + } + + private function collectMetaData(): void + { + $set = TransactionJournalMeta::whereIn('transaction_journal_id', $this->journalIds)->get(['transaction_journal_id', 'name', 'data'])->toArray(); + foreach ($set as $entry) { + $name = $entry['name']; + $data = (string)$entry['data']; + if ('' === $data) { + continue; + } + if (in_array($name, $this->dateFields, true)) { + // Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data)); + $this->metaData[$entry['transaction_journal_id']][$name] = Carbon::parse($data, config('app.timezone')); + // Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString())); + + continue; + } + $this->metaData[(int)$entry['transaction_journal_id']][$name] = $data; + } + } + + private function collectNotes(): void + { + $notes = Note::query()->whereIn('noteable_id', $this->journalIds) + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', TransactionJournal::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + foreach ($notes as $note) { + $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + } + Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); + } + + private function collectTags(): void + { + $set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id') + ->whereIn('tag_transaction_journal.transaction_journal_id', $this->journalIds) + ->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag'])->toArray(); + foreach ($set as $item) { + $journalId = $item['transaction_journal_id']; + $this->tags[$journalId] ??= []; + $this->tags[$journalId][] = $item['tag']; + } } } diff --git a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php index 516705892a..0004c291c8 100644 --- a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php +++ b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php @@ -43,16 +43,15 @@ use stdClass; class WebhookEnrichment implements EnrichmentInterface { private Collection $collection; - private User $user; // @phpstan-ignore-line - private UserGroup $userGroup; // @phpstan-ignore-line - private array $ids = []; - private array $deliveries = []; - private array $responses = []; - private array $triggers = []; - - private array $webhookDeliveries = []; - private array $webhookResponses = []; - private array $webhookTriggers = []; + private array $deliveries = []; // @phpstan-ignore-line + private array $ids = []; // @phpstan-ignore-line + private array $responses = []; + private array $triggers = []; + private User $user; + private UserGroup $userGroup; + private array $webhookDeliveries = []; + private array $webhookResponses = []; + private array $webhookTriggers = []; public function enrich(Collection $collection): Collection { @@ -67,7 +66,7 @@ class WebhookEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -86,6 +85,20 @@ class WebhookEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } + private function appendCollectedInfo(): void + { + $this->collection = $this->collection->map(function (Webhook $item) { + $meta = [ + 'deliveries' => $this->webhookDeliveries[$item->id] ?? [], + 'responses' => $this->webhookResponses[$item->id] ?? [], + 'triggers' => $this->webhookTriggers[$item->id] ?? [], + ]; + $item->meta = $meta; + + return $item; + }); + } + private function collectIds(): void { /** @var Webhook $webhook */ @@ -147,18 +160,4 @@ class WebhookEnrichment implements EnrichmentInterface $this->webhookTriggers[$id][] = WebhookTriggerEnum::from($this->triggers[$triggerId])->name; } } - - private function appendCollectedInfo(): void - { - $this->collection = $this->collection->map(function (Webhook $item) { - $meta = [ - 'deliveries' => $this->webhookDeliveries[$item->id] ?? [], - 'responses' => $this->webhookResponses[$item->id] ?? [], - 'triggers' => $this->webhookTriggers[$item->id] ?? [], - ]; - $item->meta = $meta; - - return $item; - }); - } } diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php index d2a3572b7d..d9e616f9b9 100644 --- a/app/Support/Models/AccountBalanceCalculator.php +++ b/app/Support/Models/AccountBalanceCalculator.php @@ -62,6 +62,46 @@ class AccountBalanceCalculator $object->optimizedCalculation(new Collection()); } + public static function recalculateForJournal(TransactionJournal $transactionJournal): void + { + Log::debug(__METHOD__); + $object = new self(); + + $set = []; + foreach ($transactionJournal->transactions as $transaction) { + $set[$transaction->account_id] = $transaction->account; + } + $accounts = new Collection()->push(...$set); + $object->optimizedCalculation($accounts, $transactionJournal->date); + } + + private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string + { + if (!$notBefore instanceof Carbon) { + return '0'; + } + Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d'))); + $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->whereNull('transactions.deleted_at') + ->where('transaction_journals.transaction_currency_id', $currencyId) + ->whereNull('transaction_journals.deleted_at') + // this order is the same as GroupCollector + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transaction_journals.order', 'ASC') + ->orderBy('transaction_journals.id', 'DESC') + ->orderBy('transaction_journals.description', 'DESC') + ->orderBy('transactions.amount', 'DESC') + ->where('transactions.account_id', $accountId); + $notBefore->startOfDay(); + $query->where('transaction_journals.date', '<', $notBefore); + + $first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']); + $balance = (string)($first->balance_after ?? '0'); + Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0)); + + return $balance; + } + private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void { Log::debug('start of optimizedCalculation'); @@ -72,15 +112,14 @@ class AccountBalanceCalculator $balances = []; $count = 0; $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->whereNull('transaction_journals.deleted_at') // this order is the same as GroupCollector, but in the exact reverse. - ->orderBy('transaction_journals.date', 'asc') - ->orderBy('transaction_journals.order', 'desc') - ->orderBy('transaction_journals.id', 'asc') - ->orderBy('transaction_journals.description', 'asc') - ->orderBy('transactions.amount', 'asc') - ; + ->orderBy('transaction_journals.date', 'asc') + ->orderBy('transaction_journals.order', 'desc') + ->orderBy('transaction_journals.id', 'asc') + ->orderBy('transaction_journals.description', 'asc') + ->orderBy('transactions.amount', 'asc'); if ($accounts->count() > 0) { $query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray()); } @@ -89,7 +128,7 @@ class AccountBalanceCalculator $query->where('transaction_journals.date', '>=', $notBefore); } - $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); + $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); Log::debug(sprintf('Counted %d transaction(s)', $set->count())); // the balance value is an array. @@ -102,8 +141,8 @@ class AccountBalanceCalculator $balances[$entry->account_id][$entry->transaction_currency_id] ??= [$this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore), null]; // before and after are easy: - $before = $balances[$entry->account_id][$entry->transaction_currency_id][0]; - $after = bcadd($before, (string)$entry->amount); + $before = $balances[$entry->account_id][$entry->transaction_currency_id][0]; + $after = bcadd($before, (string)$entry->amount); if (true === $entry->balance_dirty || $accounts->count() > 0) { // update the transaction: $entry->balance_before = $before; @@ -123,34 +162,6 @@ class AccountBalanceCalculator $this->storeAccountBalances($balances); } - private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string - { - if (!$notBefore instanceof Carbon) { - return '0'; - } - Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d'))); - $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNull('transactions.deleted_at') - ->where('transaction_journals.transaction_currency_id', $currencyId) - ->whereNull('transaction_journals.deleted_at') - // this order is the same as GroupCollector - ->orderBy('transaction_journals.date', 'DESC') - ->orderBy('transaction_journals.order', 'ASC') - ->orderBy('transaction_journals.id', 'DESC') - ->orderBy('transaction_journals.description', 'DESC') - ->orderBy('transactions.amount', 'DESC') - ->where('transactions.account_id', $accountId) - ; - $notBefore->startOfDay(); - $query->where('transaction_journals.date', '<', $notBefore); - - $first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']); - $balance = (string)($first->balance_after ?? '0'); - Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0)); - - return $balance; - } - private function storeAccountBalances(array $balances): void { /** @@ -196,17 +207,4 @@ class AccountBalanceCalculator } } } - - public static function recalculateForJournal(TransactionJournal $transactionJournal): void - { - Log::debug(__METHOD__); - $object = new self(); - - $set = []; - foreach ($transactionJournal->transactions as $transaction) { - $set[$transaction->account_id] = $transaction->account; - } - $accounts = new Collection()->push(...$set); - $object->optimizedCalculation($accounts, $transactionJournal->date); - } } diff --git a/app/Support/Models/BillDateCalculator.php b/app/Support/Models/BillDateCalculator.php index 3d49c867a3..9f40348777 100644 --- a/app/Support/Models/BillDateCalculator.php +++ b/app/Support/Models/BillDateCalculator.php @@ -49,15 +49,15 @@ class BillDateCalculator Log::debug(sprintf('Dates must be between %s and %s.', $earliest->format('Y-m-d'), $latest->format('Y-m-d'))); Log::debug(sprintf('Bill started on %s, period is "%s", skip is %d, last paid = "%s".', $billStart->format('Y-m-d'), $period, $skip, $lastPaid?->format('Y-m-d'))); - $daysUntilEOM = app('navigation')->daysUntilEndOfMonth($billStart); + $daysUntilEOM = app('navigation')->daysUntilEndOfMonth($billStart); Log::debug(sprintf('For bill start, days until end of month is %d', $daysUntilEOM)); - $set = new Collection(); - $currentStart = clone $earliest; + $set = new Collection(); + $currentStart = clone $earliest; // 2023-06-23 subDay to fix 7655 $currentStart->subDay(); - $loop = 0; + $loop = 0; Log::debug('Start of loop'); while ($currentStart <= $latest) { @@ -107,7 +107,7 @@ class BillDateCalculator // for the next loop, go to end of period, THEN add day. Log::debug('Add one day to nextExpectedMatch/currentStart.'); $nextExpectedMatch->addDay(); - $currentStart = clone $nextExpectedMatch; + $currentStart = clone $nextExpectedMatch; ++$loop; if ($loop > 31) { @@ -117,8 +117,8 @@ class BillDateCalculator } } Log::debug('end of loop'); - $simple = $set->map( // @phpstan-ignore-line - static fn (Carbon $date) => $date->format('Y-m-d') + $simple = $set->map( // @phpstan-ignore-line + static fn(Carbon $date) => $date->format('Y-m-d') ); Log::debug(sprintf('Found %d pay dates', $set->count()), $simple->toArray()); @@ -140,7 +140,7 @@ class BillDateCalculator return $billStartDate; } - $steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate); + $steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate); if ($steps === $this->diffInMonths) { Log::debug(sprintf('Steps is %d, which is the same as diffInMonths (%d), so we add another 1.', $steps, $this->diffInMonths)); ++$steps; diff --git a/app/Support/Models/ReturnsIntegerIdTrait.php b/app/Support/Models/ReturnsIntegerIdTrait.php index 804b9be384..d8178e07a5 100644 --- a/app/Support/Models/ReturnsIntegerIdTrait.php +++ b/app/Support/Models/ReturnsIntegerIdTrait.php @@ -39,7 +39,7 @@ trait ReturnsIntegerIdTrait protected function id(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } diff --git a/app/Support/Models/ReturnsIntegerUserIdTrait.php b/app/Support/Models/ReturnsIntegerUserIdTrait.php index a0d2bc79e9..8eca6e943c 100644 --- a/app/Support/Models/ReturnsIntegerUserIdTrait.php +++ b/app/Support/Models/ReturnsIntegerUserIdTrait.php @@ -37,14 +37,14 @@ trait ReturnsIntegerUserIdTrait protected function userGroupId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function userId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index d88b01ba38..a1e2c256c7 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -77,10 +77,10 @@ class Navigation if (!array_key_exists($repeatFreq, $functionMap)) { Log::error(sprintf( - 'The periodicity %s is unknown. Choose one of available periodicity: %s', - $repeatFreq, - implode(', ', array_keys($functionMap)) - )); + 'The periodicity %s is unknown. Choose one of available periodicity: %s', + $repeatFreq, + implode(', ', array_keys($functionMap)) + )); return $theDate; } @@ -88,30 +88,12 @@ class Navigation return $this->nextDateByInterval($date, $functionMap[$repeatFreq], $skip); } - public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon - { - try { - return $this->calculator->nextDateByInterval($epoch, $periodicity, $skipInterval); - } catch (IntervalException $exception) { - Log::warning($exception->getMessage(), ['exception' => $exception]); - } catch (Throwable $exception) { - Log::error($exception->getMessage(), ['exception' => $exception]); - } - - Log::debug( - 'Any error occurred to calculate the next date.', - ['date' => $epoch, 'periodicity' => $periodicity->name, 'skipInterval' => $skipInterval] - ); - - return $epoch; - } - public function blockPeriods(Carbon $start, Carbon $end, string $range): array { if ($end < $start) { [$start, $end] = [$end, $start]; } - $periods = []; + $periods = []; // first, 13 periods of [range] $loopCount = 0; $loopDate = clone $end; @@ -159,86 +141,71 @@ class Navigation return $periods; } - public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon + public function daysUntilEndOfMonth(Carbon $date): int { - $date = clone $theDate; - // Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); - $functionMap = [ - '1D' => 'startOfDay', - 'daily' => 'startOfDay', - '1W' => 'startOfWeek', - 'week' => 'startOfWeek', - 'weekly' => 'startOfWeek', - 'month' => 'startOfMonth', - '1M' => 'startOfMonth', - 'monthly' => 'startOfMonth', - '3M' => 'firstOfQuarter', - 'quarter' => 'firstOfQuarter', - 'quarterly' => 'firstOfQuarter', - 'year' => 'startOfYear', - 'yearly' => 'startOfYear', - '1Y' => 'startOfYear', - 'MTD' => 'startOfMonth', + $endOfMonth = $date->copy()->endOfMonth(); + + return (int)$date->diffInDays($endOfMonth, true); + } + + public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int + { + Log::debug(sprintf( + 'diffInPeriods: %s (skip: %d), between %s and %s.', + $period, + $skip, + $beginning->format('Y-m-d'), + $end->format('Y-m-d') + )); + $map = [ + 'daily' => 'diffInDays', + 'weekly' => 'diffInWeeks', + 'monthly' => 'diffInMonths', + 'quarterly' => 'diffInMonths', + 'half-year' => 'diffInMonths', + 'yearly' => 'diffInYears', ]; + if (!array_key_exists($period, $map)) { + Log::warning(sprintf('No diffInPeriods for period "%s"', $period)); - $parameterMap = [ - 'startOfWeek' => [Carbon::MONDAY], - ]; - - if (array_key_exists($repeatFreq, $functionMap)) { - $function = $functionMap[$repeatFreq]; - // Log::debug(sprintf('Function is ->%s()', $function)); - if (array_key_exists($function, $parameterMap)) { - // Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); - $date->{$function}($parameterMap[$function][0]); // @phpstan-ignore-line - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); - - return $date; - } - - $date->{$function}(); // @phpstan-ignore-line - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); - - return $date; + return 1; } - if ('half-year' === $repeatFreq || '6M' === $repeatFreq) { - $skipTo = $date->month > 7 ? 6 : 0; - $date->startOfYear()->addMonths($skipTo); - // Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + $func = $map[$period]; + // first do the diff + $floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line - return $date; + // then correct for quarterly or half-year + if ('quarterly' === $period) { + Log::debug(sprintf('Q: Corrected %f to %f', $floatDiff, $floatDiff / 3)); + $floatDiff /= 3; + } + if ('half-year' === $period) { + Log::debug(sprintf('H: Corrected %f to %f', $floatDiff, $floatDiff / 6)); + $floatDiff /= 6; } - $result = match ($repeatFreq) { - 'last7' => $date->subDays(7)->startOfDay(), - 'last30' => $date->subDays(30)->startOfDay(), - 'last90' => $date->subDays(90)->startOfDay(), - 'last365' => $date->subDays(365)->startOfDay(), - 'MTD' => $date->startOfMonth()->startOfDay(), - 'QTD' => $date->firstOfQuarter()->startOfDay(), - 'YTD' => $date->startOfYear()->startOfDay(), - default => null, - }; - if (null !== $result) { - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + // then do ceil() + $diff = ceil($floatDiff); - return $result; + Log::debug(sprintf('Diff is %f periods (%d rounded up)', $floatDiff, $diff)); + + if ($skip > 0) { + $parameter = $skip + 1; + $diff = ceil($diff / $parameter) * $parameter; + Log::debug(sprintf( + 'diffInPeriods: skip is %d, so param is %d, and diff becomes %d', + $skip, + $parameter, + $diff + )); } - if ('custom' === $repeatFreq) { - // Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); - - return $date; // the date is already at the start. - } - Log::error(sprintf('Cannot do startOfPeriod for $repeat_freq "%s"', $repeatFreq)); - - return $theDate; + return (int)$diff; } public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon { - $currentEnd = clone $end; + $currentEnd = clone $end; // Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq)); $functionMap = [ @@ -272,11 +239,11 @@ class Navigation Log::debug('Session data available.'); /** @var Carbon $tStart */ - $tStart = session('start', today(config('app.timezone'))->startOfMonth()); + $tStart = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $tEnd */ $tEnd = session('end', today(config('app.timezone'))->endOfMonth()); - $diffInDays = (int) $tStart->diffInDays($tEnd, true); + $diffInDays = (int)$tStart->diffInDays($tEnd, true); } Log::debug(sprintf('Diff in days is %d', $diffInDays)); $currentEnd->addDays($diffInDays); @@ -286,13 +253,13 @@ class Navigation if ('MTD' === $repeatFreq) { $today = today(); if ($today->isSameMonth($end)) { - return $today->endOfDay(); + return $today->endOfDay()->milli(0); } return $end->endOfMonth(); } - $result = match ($repeatFreq) { + $result = match ($repeatFreq) { 'last7' => $currentEnd->addDays(7)->startOfDay(), 'last30' => $currentEnd->addDays(30)->startOfDay(), 'last90' => $currentEnd->addDays(90)->startOfDay(), @@ -312,19 +279,19 @@ class Navigation return $end; } - $function = $functionMap[$repeatFreq]; + $function = $functionMap[$repeatFreq]; if (array_key_exists($repeatFreq, $modifierMap)) { - $currentEnd->{$function}($modifierMap[$repeatFreq]); // @phpstan-ignore-line + $currentEnd->{$function}($modifierMap[$repeatFreq])->milli(0); // @phpstan-ignore-line if (in_array($repeatFreq, $subDay, true)) { $currentEnd->subDay(); } - $currentEnd->endOfDay(); + $currentEnd->endOfDay()->milli(0); return $currentEnd; } $currentEnd->{$function}(); // @phpstan-ignore-line - $currentEnd->endOfDay(); + $currentEnd->endOfDay()->milli(0); if (in_array($repeatFreq, $subDay, true)) { $currentEnd->subDay(); } @@ -333,68 +300,6 @@ class Navigation return $currentEnd; } - public function daysUntilEndOfMonth(Carbon $date): int - { - $endOfMonth = $date->copy()->endOfMonth(); - - return (int) $date->diffInDays($endOfMonth, true); - } - - public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int - { - Log::debug(sprintf( - 'diffInPeriods: %s (skip: %d), between %s and %s.', - $period, - $skip, - $beginning->format('Y-m-d'), - $end->format('Y-m-d') - )); - $map = [ - 'daily' => 'diffInDays', - 'weekly' => 'diffInWeeks', - 'monthly' => 'diffInMonths', - 'quarterly' => 'diffInMonths', - 'half-year' => 'diffInMonths', - 'yearly' => 'diffInYears', - ]; - if (!array_key_exists($period, $map)) { - Log::warning(sprintf('No diffInPeriods for period "%s"', $period)); - - return 1; - } - $func = $map[$period]; - // first do the diff - $floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line - - // then correct for quarterly or half-year - if ('quarterly' === $period) { - Log::debug(sprintf('Q: Corrected %f to %f', $floatDiff, $floatDiff / 3)); - $floatDiff /= 3; - } - if ('half-year' === $period) { - Log::debug(sprintf('H: Corrected %f to %f', $floatDiff, $floatDiff / 6)); - $floatDiff /= 6; - } - - // then do ceil() - $diff = ceil($floatDiff); - - Log::debug(sprintf('Diff is %f periods (%d rounded up)', $floatDiff, $diff)); - - if ($skip > 0) { - $parameter = $skip + 1; - $diff = ceil($diff / $parameter) * $parameter; - Log::debug(sprintf( - 'diffInPeriods: skip is %d, so param is %d, and diff becomes %d', - $skip, - $parameter, - $diff - )); - } - - return (int) $diff; - } - public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon { $functionMap = [ @@ -414,7 +319,7 @@ class Navigation 'yearly' => 'endOfYear', ]; - $currentEnd = clone $theCurrentEnd; + $currentEnd = clone $theCurrentEnd; if (array_key_exists($repeatFreq, $functionMap)) { $function = $functionMap[$repeatFreq]; @@ -438,7 +343,7 @@ class Navigation if (is_array($range)) { $range = '1M'; } - $range = (string) $range; + $range = (string)$range; if (!$correct) { return $range; } @@ -457,25 +362,25 @@ class Navigation */ public function listOfPeriods(Carbon $start, Carbon $end): array { - $locale = app('steam')->getLocale(); + $locale = app('steam')->getLocale(); // define period to increment $increment = 'addDay'; $format = $this->preferredCarbonFormat($start, $end); - $displayFormat = (string) trans('config.month_and_day_js', [], $locale); + $displayFormat = (string)trans('config.month_and_day_js', [], $locale); $diff = $start->diffInMonths($end, true); // increment by month (for year) if ($diff >= 1.0001 && $diff < 12.001) { $increment = 'addMonth'; - $displayFormat = (string) trans('config.month_js'); + $displayFormat = (string)trans('config.month_js'); } // increment by year (for multi-year) if ($diff >= 12.0001) { $increment = 'addYear'; - $displayFormat = (string) trans('config.year_js'); + $displayFormat = (string)trans('config.year_js'); } - $begin = clone $start; - $entries = []; + $begin = clone $start; + $entries = []; while ($begin < $end) { $formatted = $begin->format($format); $displayed = $begin->isoFormat($displayFormat); @@ -486,6 +391,59 @@ class Navigation return $entries; } + public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon + { + try { + return $this->calculator->nextDateByInterval($epoch, $periodicity, $skipInterval); + } catch (IntervalException $exception) { + Log::warning($exception->getMessage(), ['exception' => $exception]); + } catch (Throwable $exception) { + Log::error($exception->getMessage(), ['exception' => $exception]); + } + + Log::debug( + 'Any error occurred to calculate the next date.', + ['date' => $epoch, 'periodicity' => $periodicity->name, 'skipInterval' => $skipInterval] + ); + + return $epoch; + } + + public function periodShow(Carbon $theDate, string $repeatFrequency): string + { + $date = clone $theDate; + $formatMap = [ + '1D' => (string)trans('config.specific_day_js'), + 'daily' => (string)trans('config.specific_day_js'), + 'custom' => (string)trans('config.specific_day_js'), + '1W' => (string)trans('config.week_in_year_js'), + 'week' => (string)trans('config.week_in_year_js'), + 'weekly' => (string)trans('config.week_in_year_js'), + '1M' => (string)trans('config.month_js'), + 'month' => (string)trans('config.month_js'), + 'monthly' => (string)trans('config.month_js'), + '1Y' => (string)trans('config.year_js'), + 'year' => (string)trans('config.year_js'), + 'yearly' => (string)trans('config.year_js'), + '6M' => (string)trans('config.half_year_js'), + ]; + + if (array_key_exists($repeatFrequency, $formatMap)) { + return $date->isoFormat($formatMap[$repeatFrequency]); + } + if ('3M' === $repeatFrequency || 'quarter' === $repeatFrequency) { + $quarter = ceil($theDate->month / 3); + + return sprintf('Q%d %d', $quarter, $theDate->year); + } + + // special formatter for quarter of year + Log::error(sprintf('No date formats for frequency "%s"!', $repeatFrequency)); + throw new FireflyException(sprintf('No date formats for frequency "%s"!', $repeatFrequency)); + + return $date->format('Y-m-d'); + } + /** * If the date difference between start and end is less than a month, method returns "Y-m-d". If the difference is * less than a year, method returns "Y-m". If the date difference is larger, method returns "Y". @@ -508,40 +466,6 @@ class Navigation return $format; } - public function periodShow(Carbon $theDate, string $repeatFrequency): string - { - $date = clone $theDate; - $formatMap = [ - '1D' => (string) trans('config.specific_day_js'), - 'daily' => (string) trans('config.specific_day_js'), - 'custom' => (string) trans('config.specific_day_js'), - '1W' => (string) trans('config.week_in_year_js'), - 'week' => (string) trans('config.week_in_year_js'), - 'weekly' => (string) trans('config.week_in_year_js'), - '1M' => (string) trans('config.month_js'), - 'month' => (string) trans('config.month_js'), - 'monthly' => (string) trans('config.month_js'), - '1Y' => (string) trans('config.year_js'), - 'year' => (string) trans('config.year_js'), - 'yearly' => (string) trans('config.year_js'), - '6M' => (string) trans('config.half_year_js'), - ]; - - if (array_key_exists($repeatFrequency, $formatMap)) { - return $date->isoFormat($formatMap[$repeatFrequency]); - } - if ('3M' === $repeatFrequency || 'quarter' === $repeatFrequency) { - $quarter = ceil($theDate->month / 3); - - return sprintf('Q%d %d', $quarter, $theDate->year); - } - - // special formatter for quarter of year - Log::error(sprintf('No date formats for frequency "%s"!', $repeatFrequency)); - - return $date->format('Y-m-d'); - } - /** * Same as preferredCarbonFormat but by string */ @@ -567,14 +491,14 @@ class Navigation $locale = app('steam')->getLocale(); $diff = $start->diffInMonths($end, true); if ($diff >= 1.001 && $diff < 12.001) { - return (string) trans('config.month_js', [], $locale); + return (string)trans('config.month_js', [], $locale); } if ($diff >= 12.001) { - return (string) trans('config.year_js', [], $locale); + return (string)trans('config.year_js', [], $locale); } - return (string) trans('config.month_and_day_js', [], $locale); + return (string)trans('config.month_and_day_js', [], $locale); } /** @@ -631,13 +555,90 @@ class Navigation return '%Y-%m-%d'; } + public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon + { + $date = clone $theDate; + // Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); + $functionMap = [ + '1D' => 'startOfDay', + 'daily' => 'startOfDay', + '1W' => 'startOfWeek', + 'week' => 'startOfWeek', + 'weekly' => 'startOfWeek', + 'month' => 'startOfMonth', + '1M' => 'startOfMonth', + 'monthly' => 'startOfMonth', + '3M' => 'firstOfQuarter', + 'quarter' => 'firstOfQuarter', + 'quarterly' => 'firstOfQuarter', + 'year' => 'startOfYear', + 'yearly' => 'startOfYear', + '1Y' => 'startOfYear', + 'MTD' => 'startOfMonth', + ]; + + $parameterMap = [ + 'startOfWeek' => [Carbon::MONDAY], + ]; + + if (array_key_exists($repeatFreq, $functionMap)) { + $function = $functionMap[$repeatFreq]; + // Log::debug(sprintf('Function is ->%s()', $function)); + if (array_key_exists($function, $parameterMap)) { + // Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); + $date->{$function}($parameterMap[$function][0]); // @phpstan-ignore-line + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $date; + } + + $date->{$function}(); // @phpstan-ignore-line + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $date; + } + if ('half-year' === $repeatFreq || '6M' === $repeatFreq) { + $skipTo = $date->month > 7 ? 6 : 0; + $date->startOfYear()->addMonths($skipTo); + // Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $date; + } + + $result = match ($repeatFreq) { + 'last7' => $date->subDays(7)->startOfDay(), + 'last30' => $date->subDays(30)->startOfDay(), + 'last90' => $date->subDays(90)->startOfDay(), + 'last365' => $date->subDays(365)->startOfDay(), + 'MTD' => $date->startOfMonth()->startOfDay(), + 'QTD' => $date->firstOfQuarter()->startOfDay(), + 'YTD' => $date->startOfYear()->startOfDay(), + default => null, + }; + if (null !== $result) { + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $result; + } + + if ('custom' === $repeatFreq) { + // Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); + + return $date; // the date is already at the start. + } + Log::error(sprintf('Cannot do startOfPeriod for $repeat_freq "%s"', $repeatFreq)); + + return $theDate; + } + /** * @throws FireflyException */ public function subtractPeriod(Carbon $theDate, string $repeatFreq, ?int $subtract = null): Carbon { $subtract ??= 1; - $date = clone $theDate; + $date = clone $theDate; // 1D 1W 1M 3M 6M 1Y $functionMap = [ '1D' => 'subDays', @@ -676,11 +677,11 @@ class Navigation // this is then subtracted from $theDate (* $subtract). if ('custom' === $repeatFreq) { /** @var Carbon $tStart */ - $tStart = session('start', today(config('app.timezone'))->startOfMonth()); + $tStart = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $tEnd */ $tEnd = session('end', today(config('app.timezone'))->endOfMonth()); - $diffInDays = (int) $tStart->diffInDays($tEnd, true); + $diffInDays = (int)$tStart->diffInDays($tEnd, true); $date->subDays($diffInDays * $subtract); return $date; @@ -770,7 +771,7 @@ class Navigation return $fiscalHelper->endOfFiscalYear($end); } - $list = [ + $list = [ 'last7', 'last30', 'last90', diff --git a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php index 3f50036ab1..d71872971c 100644 --- a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php +++ b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php @@ -39,12 +39,98 @@ use Spatie\Period\Precision; trait RecalculatesAvailableBudgetsTrait { + private function calculateAmount(AvailableBudget $availableBudget): void + { + $repository = app(BudgetLimitRepositoryInterface::class); + $repository->setUser($availableBudget->user); + $newAmount = '0'; + $abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY()); + Log::debug( + sprintf( + 'Now at AB #%d, ("%s" to "%s")', + $availableBudget->id, + $availableBudget->start_date->format('Y-m-d'), + $availableBudget->end_date->format('Y-m-d') + ) + ); + // have to recalculate everything just in case. + $set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date); + Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count())); + + /** @var BudgetLimit $budgetLimit */ + foreach ($set as $budgetLimit) { + Log::debug( + sprintf( + 'Found interesting budget limit #%d ("%s" to "%s")', + $budgetLimit->id, + $budgetLimit->start_date->format('Y-m-d'), + $budgetLimit->end_date->format('Y-m-d') + ) + ); + // overlap in days: + $limitPeriod = Period::make( + $budgetLimit->start_date, + $budgetLimit->end_date, + precision : Precision::DAY(), + boundaries: Boundaries::EXCLUDE_NONE() + ); + // if both equal each other, amount from this BL must be added to the AB + if ($limitPeriod->equals($abPeriod)) { + Log::debug('This budget limit is equal to the available budget period.'); + $newAmount = bcadd($newAmount, (string)$budgetLimit->amount); + } + // if budget limit period is inside AB period, it can be added in full. + if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) { + Log::debug('This budget limit is smaller than the available budget period.'); + $newAmount = bcadd($newAmount, (string)$budgetLimit->amount); + } + if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) { + Log::debug('This budget limit is something else entirely!'); + $overlap = $abPeriod->overlap($limitPeriod); + if ($overlap instanceof Period) { + $length = $overlap->length(); + $daily = bcmul($this->getDailyAmount($budgetLimit), (string)$length); + $newAmount = bcadd($newAmount, $daily); + } + } + } + if (0 === bccomp('0', $newAmount)) { + Log::debug('New amount is zero, deleting AB.'); + $availableBudget->delete(); + + return; + } + Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount)); + $availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places); + $availableBudget->save(); + } + + private function getDailyAmount(BudgetLimit $budgetLimit): string + { + if (0 === $budgetLimit->id) { + return '0'; + } + $limitPeriod = Period::make( + $budgetLimit->start_date, + $budgetLimit->end_date, + precision : Precision::DAY(), + boundaries: Boundaries::EXCLUDE_NONE() + ); + $days = $limitPeriod->length(); + $amount = bcdiv($budgetLimit->amount, (string)$days, 12); + Log::debug( + sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount) + ); + + return $amount; + } + private function updateAvailableBudget(BudgetLimit $budgetLimit): void { Log::debug(sprintf('Now in updateAvailableBudget(limit #%d)', $budgetLimit->id)); /** @var null|Budget $budget */ - $budget = Budget::find($budgetLimit->budget_id); + $budget = Budget::find($budgetLimit->budget_id); if (null === $budget) { Log::warning('Budget is null, probably deleted, find deleted version.'); @@ -59,7 +145,7 @@ trait RecalculatesAvailableBudgetsTrait } /** @var null|User $user */ - $user = $budget->user; + $user = $budget->user; // sanity check. It happens when the budget has been deleted so the original user is unknown. if (null === $user) { @@ -75,7 +161,7 @@ trait RecalculatesAvailableBudgetsTrait // all have to be created or updated. try { $viewRange = app('preferences')->getForUser($user, 'viewRange', '1M')->data; - } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { + } catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) { Log::error($e->getMessage()); $viewRange = '1M'; } @@ -83,20 +169,20 @@ trait RecalculatesAvailableBudgetsTrait if (null === $viewRange || is_array($viewRange)) { $viewRange = '1M'; } - $viewRange = (string) $viewRange; + $viewRange = (string)$viewRange; - $start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange); - $end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange); - $end = app('navigation')->endOfPeriod($end, $viewRange); + $start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange); + $end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange); + $end = app('navigation')->endOfPeriod($end, $viewRange); // limit period in total is: $limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE()); Log::debug(sprintf('Limit period is from %s to %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); // from the start until the end of the budget limit, need to loop! - $current = clone $start; + $current = clone $start; while ($current <= $end) { - $currentEnd = app('navigation')->endOfPeriod($current, $viewRange); + $currentEnd = app('navigation')->endOfPeriod($current, $viewRange); // create or find AB for this particular period, and set the amount accordingly. /** @var null|AvailableBudget $availableBudget */ @@ -111,7 +197,7 @@ trait RecalculatesAvailableBudgetsTrait // if not exists: $currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE()); $daily = $this->getDailyAmount($budgetLimit); - $amount = bcmul($daily, (string) $currentPeriod->length(), 12); + $amount = bcmul($daily, (string)$currentPeriod->length(), 12); // no need to calculate if period is equal. if ($currentPeriod->equals($limitPeriod)) { @@ -141,93 +227,7 @@ trait RecalculatesAvailableBudgetsTrait } // prep for next loop - $current = app('navigation')->addPeriod($current, $viewRange, 0); + $current = app('navigation')->addPeriod($current, $viewRange, 0); } } - - private function calculateAmount(AvailableBudget $availableBudget): void - { - $repository = app(BudgetLimitRepositoryInterface::class); - $repository->setUser($availableBudget->user); - $newAmount = '0'; - $abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY()); - Log::debug( - sprintf( - 'Now at AB #%d, ("%s" to "%s")', - $availableBudget->id, - $availableBudget->start_date->format('Y-m-d'), - $availableBudget->end_date->format('Y-m-d') - ) - ); - // have to recalculate everything just in case. - $set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date); - Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count())); - - /** @var BudgetLimit $budgetLimit */ - foreach ($set as $budgetLimit) { - Log::debug( - sprintf( - 'Found interesting budget limit #%d ("%s" to "%s")', - $budgetLimit->id, - $budgetLimit->start_date->format('Y-m-d'), - $budgetLimit->end_date->format('Y-m-d') - ) - ); - // overlap in days: - $limitPeriod = Period::make( - $budgetLimit->start_date, - $budgetLimit->end_date, - precision : Precision::DAY(), - boundaries: Boundaries::EXCLUDE_NONE() - ); - // if both equal each other, amount from this BL must be added to the AB - if ($limitPeriod->equals($abPeriod)) { - Log::debug('This budget limit is equal to the available budget period.'); - $newAmount = bcadd($newAmount, (string) $budgetLimit->amount); - } - // if budget limit period is inside AB period, it can be added in full. - if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) { - Log::debug('This budget limit is smaller than the available budget period.'); - $newAmount = bcadd($newAmount, (string) $budgetLimit->amount); - } - if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) { - Log::debug('This budget limit is something else entirely!'); - $overlap = $abPeriod->overlap($limitPeriod); - if ($overlap instanceof Period) { - $length = $overlap->length(); - $daily = bcmul($this->getDailyAmount($budgetLimit), (string) $length); - $newAmount = bcadd($newAmount, $daily); - } - } - } - if (0 === bccomp('0', $newAmount)) { - Log::debug('New amount is zero, deleting AB.'); - $availableBudget->delete(); - - return; - } - Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount)); - $availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places); - $availableBudget->save(); - } - - private function getDailyAmount(BudgetLimit $budgetLimit): string - { - if (0 === $budgetLimit->id) { - return '0'; - } - $limitPeriod = Period::make( - $budgetLimit->start_date, - $budgetLimit->end_date, - precision : Precision::DAY(), - boundaries: Boundaries::EXCLUDE_NONE() - ); - $days = $limitPeriod->length(); - $amount = bcdiv($budgetLimit->amount, (string) $days, 12); - Log::debug( - sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount) - ); - - return $amount; - } } diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php index f2ab22a672..bd91585f8b 100644 --- a/app/Support/ParseDateString.php +++ b/app/Support/ParseDateString.php @@ -29,7 +29,6 @@ use Carbon\CarbonInterface; use Carbon\Exceptions\InvalidFormatException; use FireflyIII\Exceptions\FireflyException; use Illuminate\Support\Facades\Log; - use function Safe\preg_match; /** @@ -79,15 +78,15 @@ class ParseDateString public function parseDate(string $date): Carbon { Log::debug(sprintf('parseDate("%s")', $date)); - $date = strtolower($date); + $date = strtolower($date); // parse keywords: if (in_array($date, $this->keywords, true)) { return $this->parseKeyword($date); } // if regex for YYYY-MM-DD: - $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; - $result = preg_match($pattern, $date); + $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; + $result = preg_match($pattern, $date); if (0 !== $result) { return $this->parseDefaultDate($date); } @@ -114,99 +113,13 @@ class ParseDateString return new Carbon('1984-09-17'); } // maybe a year, nothing else? - if (4 === strlen($date) && is_numeric($date) && (int) $date > 1000 && (int) $date <= 3000) { + if (4 === strlen($date) && is_numeric($date) && (int)$date > 1000 && (int)$date <= 3000) { return new Carbon(sprintf('%d-01-01', $date)); } throw new FireflyException(sprintf('[d] Not a recognised date format: "%s"', $date)); } - protected function parseKeyword(string $keyword): Carbon - { - $today = today(config('app.timezone'))->startOfDay(); - - return match ($keyword) { - default => $today, - 'yesterday' => $today->subDay(), - 'tomorrow' => $today->addDay(), - 'start of this week' => $today->startOfWeek(CarbonInterface::MONDAY), - 'end of this week' => $today->endOfWeek(CarbonInterface::SUNDAY), - 'start of this month' => $today->startOfMonth(), - 'end of this month' => $today->endOfMonth(), - 'start of this quarter' => $today->startOfQuarter(), - 'end of this quarter' => $today->endOfQuarter(), - 'start of this year' => $today->startOfYear(), - 'end of this year' => $today->endOfYear(), - }; - } - - protected function parseDefaultDate(string $date): Carbon - { - $result = false; - - try { - $result = Carbon::createFromFormat('Y-m-d', $date); - } catch (InvalidFormatException $e) { - Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage())); - } - if (false === $result) { - return today(config('app.timezone'))->startOfDay(); - } - - return $result; - } - - protected function parseRelativeDate(string $date): Carbon - { - Log::debug(sprintf('Now in parseRelativeDate("%s")', $date)); - $parts = explode(' ', $date); - $today = today(config('app.timezone'))->startOfDay(); - $functions = [ - [ - 'd' => 'subDays', - 'w' => 'subWeeks', - 'm' => 'subMonths', - 'q' => 'subQuarters', - 'y' => 'subYears', - ], - [ - 'd' => 'addDays', - 'w' => 'addWeeks', - 'm' => 'addMonths', - 'q' => 'addQuarters', - 'y' => 'addYears', - ], - ]; - - foreach ($parts as $part) { - Log::debug(sprintf('Now parsing part "%s"', $part)); - $part = trim($part); - - // verify if correct - $pattern = '/[+-]\d+[wqmdy]/'; - $result = preg_match($pattern, $part); - if (0 === $result) { - Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part)); - - continue; - } - $direction = str_starts_with($part, '+') ? 1 : 0; - $period = $part[strlen($part) - 1]; - $number = (int) substr($part, 1, -1); - if (!array_key_exists($period, $functions[$direction])) { - Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period)); - - continue; - } - $func = $functions[$direction][$period]; - Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d'))); - $today->{$func}($number); // @phpstan-ignore-line - Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d'))); - } - - return $today; - } - public function parseRange(string $date): array { // several types of range can be submitted @@ -269,16 +182,34 @@ class ParseDateString return false; } - /** - * format of string is xxxx-xx-DD - */ - protected function parseDayRange(string $date): array + protected function isDayYearRange(string $date): bool { - $parts = explode('-', $date); + // if regex for YYYY-xx-DD: + $pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/'; + $result = preg_match($pattern, $date); + if (0 !== $result) { + Log::debug(sprintf('"%s" is a day/year range.', $date)); - return [ - 'day' => $parts[2], - ]; + return true; + } + Log::debug(sprintf('"%s" is not a day/year range.', $date)); + + return false; + } + + protected function isMonthDayRange(string $date): bool + { + // if regex for xxxx-MM-DD: + $pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; + $result = preg_match($pattern, $date); + if (0 !== $result) { + Log::debug(sprintf('"%s" is a month/day range.', $date)); + + return true; + } + Log::debug(sprintf('"%s" is not a month/day range.', $date)); + + return false; } protected function isMonthRange(string $date): bool @@ -296,17 +227,19 @@ class ParseDateString return false; } - /** - * format of string is xxxx-MM-xx - */ - protected function parseMonthRange(string $date): array + protected function isMonthYearRange(string $date): bool { - Log::debug(sprintf('parseMonthRange: Parsed "%s".', $date)); - $parts = explode('-', $date); + // if regex for YYYY-MM-xx: + $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/'; + $result = preg_match($pattern, $date); + if (0 !== $result) { + Log::debug(sprintf('"%s" is a month/year range.', $date)); - return [ - 'month' => $parts[1], - ]; + return true; + } + Log::debug(sprintf('"%s" is not a month/year range.', $date)); + + return false; } protected function isYearRange(string $date): bool @@ -324,6 +257,131 @@ class ParseDateString return false; } + /** + * format of string is xxxx-xx-DD + */ + protected function parseDayRange(string $date): array + { + $parts = explode('-', $date); + + return [ + 'day' => $parts[2], + ]; + } + + protected function parseDefaultDate(string $date): Carbon + { + $result = false; + + try { + $result = Carbon::createFromFormat('Y-m-d', $date); + } catch (InvalidFormatException $e) { + Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage())); + } + if (false === $result) { + return today(config('app.timezone'))->startOfDay(); + } + + return $result; + } + + protected function parseKeyword(string $keyword): Carbon + { + $today = today(config('app.timezone'))->startOfDay(); + + return match ($keyword) { + default => $today, + 'yesterday' => $today->subDay(), + 'tomorrow' => $today->addDay(), + 'start of this week' => $today->startOfWeek(CarbonInterface::MONDAY), + 'end of this week' => $today->endOfWeek(CarbonInterface::SUNDAY), + 'start of this month' => $today->startOfMonth(), + 'end of this month' => $today->endOfMonth(), + 'start of this quarter' => $today->startOfQuarter(), + 'end of this quarter' => $today->endOfQuarter(), + 'start of this year' => $today->startOfYear(), + 'end of this year' => $today->endOfYear(), + }; + } + + /** + * format of string is xxxx-MM-xx + */ + protected function parseMonthRange(string $date): array + { + Log::debug(sprintf('parseMonthRange: Parsed "%s".', $date)); + $parts = explode('-', $date); + + return [ + 'month' => $parts[1], + ]; + } + + /** + * format of string is YYYY-MM-xx + */ + protected function parseMonthYearRange(string $date): array + { + Log::debug(sprintf('parseMonthYearRange: Parsed "%s".', $date)); + $parts = explode('-', $date); + + return [ + 'year' => $parts[0], + 'month' => $parts[1], + ]; + } + + protected function parseRelativeDate(string $date): Carbon + { + Log::debug(sprintf('Now in parseRelativeDate("%s")', $date)); + $parts = explode(' ', $date); + $today = today(config('app.timezone'))->startOfDay(); + $functions = [ + [ + 'd' => 'subDays', + 'w' => 'subWeeks', + 'm' => 'subMonths', + 'q' => 'subQuarters', + 'y' => 'subYears', + ], + [ + 'd' => 'addDays', + 'w' => 'addWeeks', + 'm' => 'addMonths', + 'q' => 'addQuarters', + 'y' => 'addYears', + ], + ]; + + foreach ($parts as $part) { + Log::debug(sprintf('Now parsing part "%s"', $part)); + $part = trim($part); + + // verify if correct + $pattern = '/[+-]\d+[wqmdy]/'; + $result = preg_match($pattern, $part); + if (0 === $result) { + Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part)); + + continue; + } + $direction = str_starts_with($part, '+') ? 1 : 0; + $period = $part[strlen($part) - 1]; + $number = (int)substr($part, 1, -1); + if (!array_key_exists($period, $functions[$direction])) { + Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period)); + + continue; + } + $func = $functions[$direction][$period]; + Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d'))); + $today->{$func}($number); // @phpstan-ignore-line + Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d'))); + } + + return $today; + } + /** * format of string is YYYY-xx-xx */ @@ -337,50 +395,6 @@ class ParseDateString ]; } - protected function isMonthDayRange(string $date): bool - { - // if regex for xxxx-MM-DD: - $pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; - $result = preg_match($pattern, $date); - if (0 !== $result) { - Log::debug(sprintf('"%s" is a month/day range.', $date)); - - return true; - } - Log::debug(sprintf('"%s" is not a month/day range.', $date)); - - return false; - } - - /** - * format of string is xxxx-MM-DD - */ - private function parseMonthDayRange(string $date): array - { - Log::debug(sprintf('parseMonthDayRange: Parsed "%s".', $date)); - $parts = explode('-', $date); - - return [ - 'month' => $parts[1], - 'day' => $parts[2], - ]; - } - - protected function isDayYearRange(string $date): bool - { - // if regex for YYYY-xx-DD: - $pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/'; - $result = preg_match($pattern, $date); - if (0 !== $result) { - Log::debug(sprintf('"%s" is a day/year range.', $date)); - - return true; - } - Log::debug(sprintf('"%s" is not a day/year range.', $date)); - - return false; - } - /** * format of string is YYYY-xx-DD */ @@ -395,32 +409,17 @@ class ParseDateString ]; } - protected function isMonthYearRange(string $date): bool - { - // if regex for YYYY-MM-xx: - $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/'; - $result = preg_match($pattern, $date); - if (0 !== $result) { - Log::debug(sprintf('"%s" is a month/year range.', $date)); - - return true; - } - Log::debug(sprintf('"%s" is not a month/year range.', $date)); - - return false; - } - /** - * format of string is YYYY-MM-xx + * format of string is xxxx-MM-DD */ - protected function parseMonthYearRange(string $date): array + private function parseMonthDayRange(string $date): array { - Log::debug(sprintf('parseMonthYearRange: Parsed "%s".', $date)); + Log::debug(sprintf('parseMonthDayRange: Parsed "%s".', $date)); $parts = explode('-', $date); return [ - 'year' => $parts[0], 'month' => $parts[1], + 'day' => $parts[2], ]; } } diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index 4a068cae06..f8fc423cc7 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -48,73 +48,19 @@ class Preferences } return Preference::where('user_id', $user->id) - ->where('name', '!=', 'currencyPreference') - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->get() - ; + ->where('name', '!=', 'currencyPreference') + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->get(); } - public function get(string $name, array|bool|int|string|null $default = null): ?Preference + public function beginsWith(User $user, string $search): Collection { - /** @var null|User $user */ - $user = auth()->user(); - if (null === $user) { - $preference = new Preference(); - $preference->data = $default; + $value = sprintf('%s%%', $search); - return $preference; - } - - return $this->getForUser($user, $name, $default); - } - - public function getForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference - { - // Log::debug(sprintf('getForUser(#%d, "%s")', $user->id, $name)); - // don't care about user group ID, except for some specific preferences. - $userGroupId = $this->getUserGroupId($user, $name); - $query = Preference::where('user_id', $user->id)->where('name', $name); - if (null !== $userGroupId) { - Log::debug('Include user group ID in query'); - $query->where('user_group_id', $userGroupId); - } - - $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']); - - if (null !== $preference && null === $preference->data) { - $preference->delete(); - $preference = null; - Log::debug('Removed empty preference.'); - } - - if (null !== $preference) { - // Log::debug(sprintf('Found preference #%d for user #%d: %s', $preference->id, $user->id, $name)); - - return $preference; - } - // no preference found and default is null: - if (null === $default) { - Log::debug('Return NULL, create no preference.'); - - // return NULL - return null; - } - - return $this->setForUser($user, $name, $default); - } - - private function getUserGroupId(User $user, string $preferenceName): ?int - { - $groupId = null; - $items = config('firefly.admin_specific_prefs') ?? []; - if (in_array($preferenceName, $items, true)) { - return (int) $user->user_group_id; - } - - return $groupId; + return Preference::where('user_id', $user->id)->whereLike('name', $value)->get(); } public function delete(string $name): bool @@ -128,58 +74,6 @@ class Preferences return true; } - public function forget(User $user, string $name): void - { - $key = sprintf('preference%s%s', $user->id, $name); - Cache::forget($key); - Cache::put($key, '', 5); - } - - public function setForUser(User $user, string $name, array|bool|int|string|null $value): Preference - { - $fullName = sprintf('preference%s%s', $user->id, $name); - $userGroupId = $this->getUserGroupId($user, $name); - $userGroupId = 0 === (int) $userGroupId ? null : (int) $userGroupId; - - Cache::forget($fullName); - - $query = Preference::where('user_id', $user->id)->where('name', $name); - if (null !== $userGroupId) { - Log::debug('Include user group ID in query'); - $query->where('user_group_id', $userGroupId); - } - - $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']); - - if (null !== $preference && null === $value) { - $preference->delete(); - - return new Preference(); - } - if (null === $value) { - return new Preference(); - } - if (null === $preference) { - $preference = new Preference(); - $preference->user_id = (int) $user->id; - $preference->user_group_id = $userGroupId; - $preference->name = $name; - - } - $preference->data = $value; - $preference->save(); - Cache::forever($fullName, $preference); - - return $preference; - } - - public function beginsWith(User $user, string $search): Collection - { - $value = sprintf('%s%%', $search); - - return Preference::where('user_id', $user->id)->whereLike('name', $value)->get(); - } - /** * Find by name, has no user ID in it, because the method is called from an unauthenticated route any way. */ @@ -188,17 +82,37 @@ class Preferences return Preference::where('name', $name)->get(); } + public function forget(User $user, string $name): void + { + $key = sprintf('preference%s%s', $user->id, $name); + Cache::forget($key); + Cache::put($key, '', 5); + } + + public function get(string $name, array | bool | int | string | null $default = null): ?Preference + { + /** @var null|User $user */ + $user = auth()->user(); + if (null === $user) { + $preference = new Preference(); + $preference->data = $default; + + return $preference; + } + + return $this->getForUser($user, $name, $default); + } + public function getArrayForUser(User $user, array $list): array { $result = []; $preferences = Preference::where('user_id', $user->id) - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->whereIn('name', $list) - ->get(['id', 'name', 'data']) - ; + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->whereIn('name', $list) + ->get(['id', 'name', 'data']); /** @var Preference $preference */ foreach ($preferences as $preference) { @@ -240,7 +154,7 @@ class Preferences return $result; } - public function getEncryptedForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference + public function getEncryptedForUser(User $user, string $name, array | bool | int | string | null $default = null): ?Preference { $result = $this->getForUser($user, $name, $default); if ('' === $result->data) { @@ -265,7 +179,42 @@ class Preferences return $result; } - public function getFresh(string $name, array|bool|int|string|null $default = null): ?Preference + public function getForUser(User $user, string $name, array | bool | int | string | null $default = null): ?Preference + { + // Log::debug(sprintf('getForUser(#%d, "%s")', $user->id, $name)); + // don't care about user group ID, except for some specific preferences. + $userGroupId = $this->getUserGroupId($user, $name); + $query = Preference::where('user_id', $user->id)->where('name', $name); + if (null !== $userGroupId) { + Log::debug('Include user group ID in query'); + $query->where('user_group_id', $userGroupId); + } + + $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']); + + if (null !== $preference && null === $preference->data) { + $preference->delete(); + $preference = null; + Log::debug('Removed empty preference.'); + } + + if (null !== $preference) { + // Log::debug(sprintf('Found preference #%d for user #%d: %s', $preference->id, $user->id, $name)); + + return $preference; + } + // no preference found and default is null: + if (null === $default) { + Log::debug('Return NULL, create no preference.'); + + // return NULL + return null; + } + + return $this->setForUser($user, $name, $default); + } + + public function getFresh(string $name, array | bool | int | string | null $default = null): ?Preference { /** @var null|User $user */ $user = auth()->user(); @@ -284,8 +233,8 @@ class Preferences */ public function lastActivity(): string { - $instance = PreferencesSingleton::getInstance(); - $pref = $instance->getPreference('last_activity'); + $instance = PreferencesSingleton::getInstance(); + $pref = $instance->getPreference('last_activity'); if (null !== $pref) { // Log::debug(sprintf('Found last activity in singleton: %s', $pref)); return $pref; @@ -299,7 +248,7 @@ class Preferences if (is_array($lastActivity)) { $lastActivity = implode(',', $lastActivity); } - $setting = hash('sha256', (string) $lastActivity); + $setting = hash('sha256', (string)$lastActivity); $instance->setPreference('last_activity', $setting); return $setting; @@ -313,7 +262,7 @@ class Preferences Session::forget('first'); } - public function set(string $name, array|bool|int|string|null $value): Preference + public function set(string $name, array | bool | int | string | null $value): Preference { /** @var null|User $user */ $user = auth()->user(); @@ -341,4 +290,53 @@ class Preferences return $this->set($name, $encrypted); } + + public function setForUser(User $user, string $name, array | bool | int | string | null $value): Preference + { + $fullName = sprintf('preference%s%s', $user->id, $name); + $userGroupId = $this->getUserGroupId($user, $name); + $userGroupId = 0 === (int)$userGroupId ? null : (int)$userGroupId; + + Cache::forget($fullName); + + $query = Preference::where('user_id', $user->id)->where('name', $name); + if (null !== $userGroupId) { + Log::debug('Include user group ID in query'); + $query->where('user_group_id', $userGroupId); + } + + $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']); + + if (null !== $preference && null === $value) { + $preference->delete(); + + return new Preference(); + } + if (null === $value) { + return new Preference(); + } + if (null === $preference) { + $preference = new Preference(); + $preference->user_id = (int)$user->id; + $preference->user_group_id = $userGroupId; + $preference->name = $name; + + } + $preference->data = $value; + $preference->save(); + Cache::forever($fullName, $preference); + + return $preference; + } + + private function getUserGroupId(User $user, string $preferenceName): ?int + { + $groupId = null; + $items = config('firefly.admin_specific_prefs') ?? []; + if (in_array($preferenceName, $items, true)) { + return (int)$user->user_group_id; + } + + return $groupId; + } } diff --git a/app/Support/Report/Budget/BudgetReportGenerator.php b/app/Support/Report/Budget/BudgetReportGenerator.php index c478e3cce4..b859847748 100644 --- a/app/Support/Report/Budget/BudgetReportGenerator.php +++ b/app/Support/Report/Budget/BudgetReportGenerator.php @@ -76,7 +76,7 @@ class BudgetReportGenerator /** @var Account $account */ foreach ($this->accounts as $account) { - $accountId = $account->id; + $accountId = $account->id; $this->report[$accountId] ??= [ 'name' => $account->name, 'id' => $account->id, @@ -91,43 +91,6 @@ class BudgetReportGenerator } } - /** - * Process each row of expenses collected for the "Account per budget" partial - */ - private function processExpenses(array $expenses): void - { - foreach ($expenses['budgets'] as $budget) { - $this->processBudgetExpenses($expenses, $budget); - } - } - - /** - * Process each set of transactions for each row of expenses. - */ - private function processBudgetExpenses(array $expenses, array $budget): void - { - $budgetId = (int) $budget['id']; - $currencyId = (int) $expenses['currency_id']; - foreach ($budget['transaction_journals'] as $journal) { - $sourceAccountId = $journal['source_account_id']; - - $this->report[$sourceAccountId]['currencies'][$currencyId] - ??= [ - 'currency_id' => $expenses['currency_id'], - 'currency_symbol' => $expenses['currency_symbol'], - 'currency_name' => $expenses['currency_name'], - 'currency_decimal_places' => $expenses['currency_decimal_places'], - 'budgets' => [], - ]; - - $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId] - ??= '0'; - - $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId] - = bcadd($this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId], (string) $journal['amount']); - } - } - /** * Generates the data necessary to create the card that displays * the budget overview in the general report. @@ -144,175 +107,6 @@ class BudgetReportGenerator $this->percentageReport(); } - /** - * Start the budgets block on the default report by processing every budget. - */ - private function generalBudgetReport(): void - { - $budgetList = $this->repository->getBudgets(); - - /** @var Budget $budget */ - foreach ($budgetList as $budget) { - $this->processBudget($budget); - } - } - - /** - * Process expenses etc. for a single budget for the budgets block on the default report. - */ - private function processBudget(Budget $budget): void - { - $budgetId = $budget->id; - $this->report['budgets'][$budgetId] ??= [ - 'budget_id' => $budgetId, - 'budget_name' => $budget->name, - 'no_budget' => false, - 'budget_limits' => [], - ]; - - // get all budget limits for budget in period: - $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); - - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $this->processLimit($budget, $limit); - } - } - - /** - * Process a single budget limit for the budgets block on the default report. - */ - private function processLimit(Budget $budget, BudgetLimit $limit): void - { - $budgetId = $budget->id; - $limitId = $limit->id; - $limitCurrency = $limit->transactionCurrency ?? $this->currency; - $currencyId = $limitCurrency->id; - $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $this->accounts, new Collection()->push($budget)); - $spent = $expenses[$currencyId]['sum'] ?? '0'; - $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); - $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; - - $this->report['budgets'][$budgetId]['budget_limits'][$limitId] ??= [ - 'budget_limit_id' => $limitId, - 'start_date' => $limit->start_date, - 'end_date' => $limit->end_date, - 'budgeted' => $limit->amount, - 'budgeted_pct' => '0', - 'spent' => $spent, - 'spent_pct' => '0', - 'left' => $left, - 'overspent' => $overspent, - 'currency_id' => $currencyId, - 'currency_code' => $limitCurrency->code, - 'currency_name' => $limitCurrency->name, - 'currency_symbol' => $limitCurrency->symbol, - 'currency_decimal_places' => $limitCurrency->decimal_places, - ]; - - // make sum information: - $this->report['sums'][$currencyId] - ??= [ - 'budgeted' => '0', - 'spent' => '0', - 'left' => '0', - 'overspent' => '0', - 'currency_id' => $currencyId, - 'currency_code' => $limitCurrency->code, - 'currency_name' => $limitCurrency->name, - 'currency_symbol' => $limitCurrency->symbol, - 'currency_decimal_places' => $limitCurrency->decimal_places, - ]; - $this->report['sums'][$currencyId]['budgeted'] = bcadd((string) $this->report['sums'][$currencyId]['budgeted'], $limit->amount); - $this->report['sums'][$currencyId]['spent'] = bcadd((string) $this->report['sums'][$currencyId]['spent'], $spent); - $this->report['sums'][$currencyId]['left'] = bcadd((string) $this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent)); - $this->report['sums'][$currencyId]['overspent'] = bcadd((string) $this->report['sums'][$currencyId]['overspent'], $overspent); - } - - /** - * Calculate the expenses for transactions without a budget. Part of the "budgets" block of the default report. - */ - private function noBudgetReport(): void - { - // add no budget info. - $this->report['budgets'][0] = [ - 'budget_id' => null, - 'budget_name' => null, - 'no_budget' => true, - 'budget_limits' => [], - ]; - - $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts); - foreach ($noBudget as $noBudgetEntry) { - // currency information: - $nbCurrencyId = (int) ($noBudgetEntry['currency_id'] ?? $this->currency->id); - $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code; - $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name; - $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol; - $nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $this->currency->decimal_places; - - $this->report['budgets'][0]['budget_limits'][] = [ - 'budget_limit_id' => null, - 'start_date' => $this->start, - 'end_date' => $this->end, - 'budgeted' => '0', - 'budgeted_pct' => '0', - 'spent' => $noBudgetEntry['sum'], - 'spent_pct' => '0', - 'left' => '0', - 'overspent' => '0', - 'currency_id' => $nbCurrencyId, - 'currency_code' => $nbCurrencyCode, - 'currency_name' => $nbCurrencyName, - 'currency_symbol' => $nbCurrencySymbol, - 'currency_decimal_places' => $nbCurrencyDp, - ]; - $this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', (string) $noBudgetEntry['sum']); - // append currency info because it may be missing: - $this->report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId; - $this->report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode; - $this->report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName; - $this->report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol; - $this->report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp; - - // append other sums because they might be missing: - $this->report['sums'][$nbCurrencyId]['overspent'] ??= '0'; - $this->report['sums'][$nbCurrencyId]['left'] ??= '0'; - $this->report['sums'][$nbCurrencyId]['budgeted'] ??= '0'; - } - } - - /** - * Calculate the percentages for each budget. Part of the "budgets" block on the default report. - */ - private function percentageReport(): void - { - // make percentages based on total amount. - foreach ($this->report['budgets'] as $budgetId => $data) { - foreach ($data['budget_limits'] as $limitId => $entry) { - $budgetId = (int) $budgetId; - $limitId = (int) $limitId; - $currencyId = (int) $entry['currency_id']; - $spent = $entry['spent']; - $totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0'; - $spentPct = '0'; - $budgeted = $entry['budgeted']; - $totalBudgeted = $this->report['sums'][$currencyId]['budgeted'] ?? '0'; - $budgetedPct = '0'; - - if (0 !== bccomp((string) $spent, '0') && 0 !== bccomp($totalSpent, '0')) { - $spentPct = round((float) bcmul(bcdiv((string) $spent, $totalSpent), '100')); - } - if (0 !== bccomp((string) $budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) { - $budgetedPct = round((float) bcmul(bcdiv((string) $budgeted, $totalBudgeted), '100')); - } - $this->report['sums'][$currencyId]['budgeted'] ??= '0'; - $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct; - $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct; - } - } - } - public function getReport(): array { return $this->report; @@ -349,4 +143,210 @@ class BudgetReportGenerator $this->nbRepository->setUser($user); $this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup); } + + /** + * Start the budgets block on the default report by processing every budget. + */ + private function generalBudgetReport(): void + { + $budgetList = $this->repository->getBudgets(); + + /** @var Budget $budget */ + foreach ($budgetList as $budget) { + $this->processBudget($budget); + } + } + + /** + * Calculate the expenses for transactions without a budget. Part of the "budgets" block of the default report. + */ + private function noBudgetReport(): void + { + // add no budget info. + $this->report['budgets'][0] = [ + 'budget_id' => null, + 'budget_name' => null, + 'no_budget' => true, + 'budget_limits' => [], + ]; + + $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts); + foreach ($noBudget as $noBudgetEntry) { + // currency information: + $nbCurrencyId = (int)($noBudgetEntry['currency_id'] ?? $this->currency->id); + $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code; + $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name; + $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol; + $nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $this->currency->decimal_places; + + $this->report['budgets'][0]['budget_limits'][] = [ + 'budget_limit_id' => null, + 'start_date' => $this->start, + 'end_date' => $this->end, + 'budgeted' => '0', + 'budgeted_pct' => '0', + 'spent' => $noBudgetEntry['sum'], + 'spent_pct' => '0', + 'left' => '0', + 'overspent' => '0', + 'currency_id' => $nbCurrencyId, + 'currency_code' => $nbCurrencyCode, + 'currency_name' => $nbCurrencyName, + 'currency_symbol' => $nbCurrencySymbol, + 'currency_decimal_places' => $nbCurrencyDp, + ]; + $this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', (string)$noBudgetEntry['sum']); + // append currency info because it may be missing: + $this->report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId; + $this->report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode; + $this->report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName; + $this->report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol; + $this->report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp; + + // append other sums because they might be missing: + $this->report['sums'][$nbCurrencyId]['overspent'] ??= '0'; + $this->report['sums'][$nbCurrencyId]['left'] ??= '0'; + $this->report['sums'][$nbCurrencyId]['budgeted'] ??= '0'; + } + } + + /** + * Calculate the percentages for each budget. Part of the "budgets" block on the default report. + */ + private function percentageReport(): void + { + // make percentages based on total amount. + foreach ($this->report['budgets'] as $budgetId => $data) { + foreach ($data['budget_limits'] as $limitId => $entry) { + $budgetId = (int)$budgetId; + $limitId = (int)$limitId; + $currencyId = (int)$entry['currency_id']; + $spent = $entry['spent']; + $totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0'; + $spentPct = '0'; + $budgeted = $entry['budgeted']; + $totalBudgeted = $this->report['sums'][$currencyId]['budgeted'] ?? '0'; + $budgetedPct = '0'; + + if (0 !== bccomp((string)$spent, '0') && 0 !== bccomp($totalSpent, '0')) { + $spentPct = round((float)bcmul(bcdiv((string)$spent, $totalSpent), '100')); + } + if (0 !== bccomp((string)$budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) { + $budgetedPct = round((float)bcmul(bcdiv((string)$budgeted, $totalBudgeted), '100')); + } + $this->report['sums'][$currencyId]['budgeted'] ??= '0'; + $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct; + $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct; + } + } + } + + /** + * Process expenses etc. for a single budget for the budgets block on the default report. + */ + private function processBudget(Budget $budget): void + { + $budgetId = $budget->id; + $this->report['budgets'][$budgetId] ??= [ + 'budget_id' => $budgetId, + 'budget_name' => $budget->name, + 'no_budget' => false, + 'budget_limits' => [], + ]; + + // get all budget limits for budget in period: + $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); + + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $this->processLimit($budget, $limit); + } + } + + /** + * Process each set of transactions for each row of expenses. + */ + private function processBudgetExpenses(array $expenses, array $budget): void + { + $budgetId = (int)$budget['id']; + $currencyId = (int)$expenses['currency_id']; + foreach ($budget['transaction_journals'] as $journal) { + $sourceAccountId = $journal['source_account_id']; + + $this->report[$sourceAccountId]['currencies'][$currencyId] + ??= [ + 'currency_id' => $expenses['currency_id'], + 'currency_symbol' => $expenses['currency_symbol'], + 'currency_name' => $expenses['currency_name'], + 'currency_decimal_places' => $expenses['currency_decimal_places'], + 'budgets' => [], + ]; + + $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId] + ??= '0'; + + $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId] + = bcadd($this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId], (string)$journal['amount']); + } + } + + /** + * Process each row of expenses collected for the "Account per budget" partial + */ + private function processExpenses(array $expenses): void + { + foreach ($expenses['budgets'] as $budget) { + $this->processBudgetExpenses($expenses, $budget); + } + } + + /** + * Process a single budget limit for the budgets block on the default report. + */ + private function processLimit(Budget $budget, BudgetLimit $limit): void + { + $budgetId = $budget->id; + $limitId = $limit->id; + $limitCurrency = $limit->transactionCurrency ?? $this->currency; + $currencyId = $limitCurrency->id; + $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $this->accounts, new Collection()->push($budget)); + $spent = $expenses[$currencyId]['sum'] ?? '0'; + $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); + $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; + + $this->report['budgets'][$budgetId]['budget_limits'][$limitId] ??= [ + 'budget_limit_id' => $limitId, + 'start_date' => $limit->start_date, + 'end_date' => $limit->end_date, + 'budgeted' => $limit->amount, + 'budgeted_pct' => '0', + 'spent' => $spent, + 'spent_pct' => '0', + 'left' => $left, + 'overspent' => $overspent, + 'currency_id' => $currencyId, + 'currency_code' => $limitCurrency->code, + 'currency_name' => $limitCurrency->name, + 'currency_symbol' => $limitCurrency->symbol, + 'currency_decimal_places' => $limitCurrency->decimal_places, + ]; + + // make sum information: + $this->report['sums'][$currencyId] + ??= [ + 'budgeted' => '0', + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', + 'currency_id' => $currencyId, + 'currency_code' => $limitCurrency->code, + 'currency_name' => $limitCurrency->name, + 'currency_symbol' => $limitCurrency->symbol, + 'currency_decimal_places' => $limitCurrency->decimal_places, + ]; + $this->report['sums'][$currencyId]['budgeted'] = bcadd((string)$this->report['sums'][$currencyId]['budgeted'], $limit->amount); + $this->report['sums'][$currencyId]['spent'] = bcadd((string)$this->report['sums'][$currencyId]['spent'], $spent); + $this->report['sums'][$currencyId]['left'] = bcadd((string)$this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent)); + $this->report['sums'][$currencyId]['overspent'] = bcadd((string)$this->report['sums'][$currencyId]['overspent'], $overspent); + } } diff --git a/app/Support/Report/Category/CategoryReportGenerator.php b/app/Support/Report/Category/CategoryReportGenerator.php index 91f1470bb8..8f800d411d 100644 --- a/app/Support/Report/Category/CategoryReportGenerator.php +++ b/app/Support/Report/Category/CategoryReportGenerator.php @@ -62,17 +62,17 @@ class CategoryReportGenerator */ public function operations(): void { - $earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts); - $spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts); + $earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts); + $spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts); // also transferred out and transferred into these accounts in this category: $transferredIn = $this->opsRepository->listTransferredIn($this->start, $this->end, $this->accounts); $transferredOut = $this->opsRepository->listTransferredOut($this->start, $this->end, $this->accounts); - $earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts); - $spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts); + $earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts); + $spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts); - $this->report = [ + $this->report = [ 'categories' => [], 'sums' => [], ]; @@ -83,17 +83,69 @@ class CategoryReportGenerator } } - /** - * Process one of the spent arrays from the operations method. - */ - private function processOpsArray(array $data): void + public function setAccounts(Collection $accounts): void { - /** - * @var int $currencyId - * @var array $currencyRow - */ - foreach ($data as $currencyId => $currencyRow) { - $this->processCurrencyArray($currencyId, $currencyRow); + $this->accounts = $accounts; + } + + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + public function setStart(Carbon $start): void + { + $this->start = $start; + } + + public function setUser(User $user): void + { + $this->noCatRepository->setUser($user); + $this->opsRepository->setUser($user); + } + + private function processCategoryRow(int $currencyId, array $currencyRow, int $categoryId, array $categoryRow): void + { + $key = sprintf('%s-%s', $currencyId, $categoryId); + $this->report['categories'][$key] ??= [ + 'id' => $categoryId, + 'title' => $categoryRow['name'], + 'currency_id' => $currencyRow['currency_id'], + 'currency_symbol' => $currencyRow['currency_symbol'], + 'currency_name' => $currencyRow['currency_name'], + 'currency_code' => $currencyRow['currency_code'], + 'currency_decimal_places' => $currencyRow['currency_decimal_places'], + 'spent' => '0', + 'earned' => '0', + 'sum' => '0', + ]; + // loop journals: + foreach ($categoryRow['transaction_journals'] as $journal) { + // sum of sums + $this->report['sums'][$currencyId]['sum'] = bcadd((string)$this->report['sums'][$currencyId]['sum'], (string)$journal['amount']); + // sum of spent: + $this->report['sums'][$currencyId]['spent'] = -1 === bccomp((string)$journal['amount'], '0') ? bcadd( + (string)$this->report['sums'][$currencyId]['spent'], + (string)$journal['amount'] + ) : $this->report['sums'][$currencyId]['spent']; + // sum of earned + $this->report['sums'][$currencyId]['earned'] = 1 === bccomp((string)$journal['amount'], '0') ? bcadd( + (string)$this->report['sums'][$currencyId]['earned'], + (string)$journal['amount'] + ) : $this->report['sums'][$currencyId]['earned']; + + // sum of category + $this->report['categories'][$key]['sum'] = bcadd((string)$this->report['categories'][$key]['sum'], (string)$journal['amount']); + // total spent in category + $this->report['categories'][$key]['spent'] = -1 === bccomp((string)$journal['amount'], '0') ? bcadd( + (string)$this->report['categories'][$key]['spent'], + (string)$journal['amount'] + ) : $this->report['categories'][$key]['spent']; + // total earned in category + $this->report['categories'][$key]['earned'] = 1 === bccomp((string)$journal['amount'], '0') ? bcadd( + (string)$this->report['categories'][$key]['earned'], + (string)$journal['amount'] + ) : $this->report['categories'][$key]['earned']; } } @@ -119,69 +171,17 @@ class CategoryReportGenerator } } - private function processCategoryRow(int $currencyId, array $currencyRow, int $categoryId, array $categoryRow): void + /** + * Process one of the spent arrays from the operations method. + */ + private function processOpsArray(array $data): void { - $key = sprintf('%s-%s', $currencyId, $categoryId); - $this->report['categories'][$key] ??= [ - 'id' => $categoryId, - 'title' => $categoryRow['name'], - 'currency_id' => $currencyRow['currency_id'], - 'currency_symbol' => $currencyRow['currency_symbol'], - 'currency_name' => $currencyRow['currency_name'], - 'currency_code' => $currencyRow['currency_code'], - 'currency_decimal_places' => $currencyRow['currency_decimal_places'], - 'spent' => '0', - 'earned' => '0', - 'sum' => '0', - ]; - // loop journals: - foreach ($categoryRow['transaction_journals'] as $journal) { - // sum of sums - $this->report['sums'][$currencyId]['sum'] = bcadd((string) $this->report['sums'][$currencyId]['sum'], (string) $journal['amount']); - // sum of spent: - $this->report['sums'][$currencyId]['spent'] = -1 === bccomp((string) $journal['amount'], '0') ? bcadd( - (string) $this->report['sums'][$currencyId]['spent'], - (string) $journal['amount'] - ) : $this->report['sums'][$currencyId]['spent']; - // sum of earned - $this->report['sums'][$currencyId]['earned'] = 1 === bccomp((string) $journal['amount'], '0') ? bcadd( - (string) $this->report['sums'][$currencyId]['earned'], - (string) $journal['amount'] - ) : $this->report['sums'][$currencyId]['earned']; - - // sum of category - $this->report['categories'][$key]['sum'] = bcadd((string) $this->report['categories'][$key]['sum'], (string) $journal['amount']); - // total spent in category - $this->report['categories'][$key]['spent'] = -1 === bccomp((string) $journal['amount'], '0') ? bcadd( - (string) $this->report['categories'][$key]['spent'], - (string) $journal['amount'] - ) : $this->report['categories'][$key]['spent']; - // total earned in category - $this->report['categories'][$key]['earned'] = 1 === bccomp((string) $journal['amount'], '0') ? bcadd( - (string) $this->report['categories'][$key]['earned'], - (string) $journal['amount'] - ) : $this->report['categories'][$key]['earned']; + /** + * @var int $currencyId + * @var array $currencyRow + */ + foreach ($data as $currencyId => $currencyRow) { + $this->processCurrencyArray($currencyId, $currencyRow); } } - - public function setAccounts(Collection $accounts): void - { - $this->accounts = $accounts; - } - - public function setEnd(Carbon $end): void - { - $this->end = $end; - } - - public function setStart(Carbon $start): void - { - $this->start = $start; - } - - public function setUser(User $user): void - { - $this->noCatRepository->setUser($user); - $this->opsRepository->setUser($user); - } } diff --git a/app/Support/Report/Summarizer/TransactionSummarizer.php b/app/Support/Report/Summarizer/TransactionSummarizer.php index aea25a5663..84e0ec3231 100644 --- a/app/Support/Report/Summarizer/TransactionSummarizer.php +++ b/app/Support/Report/Summarizer/TransactionSummarizer.php @@ -43,26 +43,19 @@ class TransactionSummarizer } } - public function setUser(User $user): void - { - $this->user = $user; - $this->default = Amount::getPrimaryCurrencyByUserGroup($user->userGroup); - $this->convertToPrimary = Amount::convertToPrimary($user); - } - public function groupByCurrencyId(array $journals, string $method = 'negative', bool $includeForeign = true): array { Log::debug(sprintf('Now in groupByCurrencyId([%d journals], "%s", %s)', count($journals), $method, var_export($includeForeign, true))); $array = []; foreach ($journals as $journal) { - $field = 'amount'; + $field = 'amount'; // grab default currency information. - $currencyId = (int) $journal['currency_id']; - $currencyName = $journal['currency_name']; - $currencySymbol = $journal['currency_symbol']; - $currencyCode = $journal['currency_code']; - $currencyDecimalPlaces = $journal['currency_decimal_places']; + $currencyId = (int)$journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; // prepare foreign currency info: $foreignCurrencyId = 0; @@ -74,8 +67,8 @@ class TransactionSummarizer if ($this->convertToPrimary) { // Log::debug('convertToPrimary is true.'); // if convert to primary currency, use the primary currency amount yes or no? - $usePrimary = $this->default->id !== (int) $journal['currency_id']; - $useForeign = $this->default->id === (int) $journal['foreign_currency_id']; + $usePrimary = $this->default->id !== (int)$journal['currency_id']; + $useForeign = $this->default->id === (int)$journal['foreign_currency_id']; if ($usePrimary) { // Log::debug(sprintf('Journal #%d switches to primary currency amount (original is %s)', $journal['transaction_journal_id'], $journal['currency_code'])); $field = 'pc_amount'; @@ -88,7 +81,7 @@ class TransactionSummarizer if ($useForeign) { // Log::debug(sprintf('Journal #%d switches to foreign amount (foreign is %s)', $journal['transaction_journal_id'], $journal['foreign_currency_code'])); $field = 'foreign_amount'; - $currencyId = (int) $journal['foreign_currency_id']; + $currencyId = (int)$journal['foreign_currency_id']; $currencyName = $journal['foreign_currency_name']; $currencySymbol = $journal['foreign_currency_symbol']; $currencyCode = $journal['foreign_currency_code']; @@ -98,7 +91,7 @@ class TransactionSummarizer if (!$this->convertToPrimary) { // Log::debug('convertToPrimary is false.'); // use foreign amount? - $foreignCurrencyId = (int) $journal['foreign_currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; if (0 !== $foreignCurrencyId) { Log::debug(sprintf('Journal #%d also includes foreign amount (foreign is "%s")', $journal['transaction_journal_id'], $journal['foreign_currency_code'])); $foreignCurrencyName = $journal['foreign_currency_name']; @@ -109,7 +102,7 @@ class TransactionSummarizer } // first process normal amount - $amount = (string) ($journal[$field] ?? '0'); + $amount = (string)($journal[$field] ?? '0'); $array[$currencyId] ??= [ 'sum' => '0', 'currency_id' => $currencyId, @@ -128,7 +121,7 @@ class TransactionSummarizer // then process foreign amount, if it exists. if (0 !== $foreignCurrencyId && true === $includeForeign) { - $amount = (string) ($journal['foreign_amount'] ?? '0'); + $amount = (string)($journal['foreign_amount'] ?? '0'); $array[$foreignCurrencyId] ??= [ 'sum' => '0', 'currency_id' => $foreignCurrencyId, @@ -186,7 +179,7 @@ class TransactionSummarizer if ($convertToPrimary && $journal['currency_id'] !== $primary->id && $primary->id === $journal['foreign_currency_id']) { $field = 'foreign_amount'; } - $key = sprintf('%s-%s', $journal[$idKey], $currencyId); + $key = sprintf('%s-%s', $journal[$idKey], $currencyId); // sum it all up or create a new array. $array[$key] ??= [ 'id' => $journal[$idKey], @@ -200,15 +193,15 @@ class TransactionSummarizer ]; // add the data from the $field to the array. - $array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string) ($journal[$field] ?? '0'))); // @phpstan-ignore-line + $array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string)($journal[$field] ?? '0'))); // @phpstan-ignore-line Log::debug(sprintf('Field for transaction #%d is "%s" (%s). Sum: %s', $journal['transaction_group_id'], $currencyCode, $field, $array[$key]['sum'])); // also do foreign amount, but only when convertToPrimary is false (otherwise we have it already) // or when convertToPrimary is true and the foreign currency is ALSO not the default currency. - if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int) $journal['foreign_currency_id']) { + if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int)$journal['foreign_currency_id']) { Log::debug(sprintf('Use foreign amount from transaction #%d: %s %s. Sum: %s', $journal['transaction_group_id'], $currencyCode, $journal['foreign_amount'], $array[$key]['sum'])); $key = sprintf('%s-%s', $journal[$idKey], $journal['foreign_currency_id']); - $array[$key] ??= [ + $array[$key] ??= [ 'id' => $journal[$idKey], 'name' => $journal[$nameKey], 'sum' => '0', @@ -218,7 +211,7 @@ class TransactionSummarizer 'currency_code' => $journal['foreign_currency_code'], 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], ]; - $array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string) $journal['foreign_amount'])); // @phpstan-ignore-line + $array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line } } @@ -230,4 +223,11 @@ class TransactionSummarizer Log::debug(sprintf('Overrule convertToPrimary to become %s', var_export($convertToPrimary, true))); $this->convertToPrimary = $convertToPrimary; } + + public function setUser(User $user): void + { + $this->user = $user; + $this->default = Amount::getPrimaryCurrencyByUserGroup($user->userGroup); + $this->convertToPrimary = Amount::convertToPrimary($user); + } } diff --git a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php index 439fd1f50a..4ca2c8c29e 100644 --- a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php @@ -58,7 +58,7 @@ trait CalculateRangeOccurrences { $return = []; $attempts = 0; - $dayOfMonth = (int) $moment; + $dayOfMonth = (int)$moment; if ($start->day > $dayOfMonth) { // day has passed already, add a month. $start->addMonth(); @@ -82,8 +82,8 @@ trait CalculateRangeOccurrences */ protected function getNdomInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array { - $return = []; - $attempts = 0; + $return = []; + $attempts = 0; $start->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? $counters = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth']; @@ -108,12 +108,12 @@ trait CalculateRangeOccurrences */ protected function getWeeklyInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array { - $return = []; - $attempts = 0; + $return = []; + $attempts = 0; app('log')->debug('Rep is weekly.'); // monday = 1 // sunday = 7 - $dayOfWeek = (int) $moment; + $dayOfWeek = (int)$moment; app('log')->debug(sprintf('DoW in repetition is %d, in mutator is %d', $dayOfWeek, $start->dayOfWeekIso)); if ($start->dayOfWeekIso > $dayOfWeek) { // day has already passed this week, add one week: @@ -154,8 +154,8 @@ trait CalculateRangeOccurrences } // is $date between $start and $end? - $obj = clone $date; - $count = 0; + $obj = clone $date; + $count = 0; while ($obj <= $end && $obj >= $start && $count < 10) { if (0 === $attempts % $skipMod) { $return[] = clone $obj; diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrences.php b/app/Support/Repositories/Recurring/CalculateXOccurrences.php index 04f07046d4..602cb03d02 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrences.php @@ -63,7 +63,7 @@ trait CalculateXOccurrences $mutator = clone $date; $total = 0; $attempts = 0; - $dayOfMonth = (int) $moment; + $dayOfMonth = (int)$moment; if ($mutator->day > $dayOfMonth) { // day has passed already, add a month. $mutator->addMonth(); @@ -89,10 +89,10 @@ trait CalculateXOccurrences */ protected function getXNDomOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array { - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; $mutator->addDay(); // always assume today has passed. $mutator->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? @@ -120,14 +120,14 @@ trait CalculateXOccurrences */ protected function getXWeeklyOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array { - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; // monday = 1 // sunday = 7 $mutator->addDay(); // always assume today has passed. - $dayOfWeek = (int) $moment; + $dayOfWeek = (int)$moment; if ($mutator->dayOfWeekIso > $dayOfWeek) { // day has already passed this week, add one week: $mutator->addWeek(); @@ -164,7 +164,7 @@ trait CalculateXOccurrences if ($mutator > $date) { $date->addYear(); } - $obj = clone $date; + $obj = clone $date; while ($total < $count) { if (0 === $attempts % $skipMod) { $return[] = clone $obj; diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php index 215c11bf0a..bd4b44fd7d 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php @@ -68,7 +68,7 @@ trait CalculateXOccurrencesSince $mutator = clone $date; $total = 0; $attempts = 0; - $dayOfMonth = (int) $moment; + $dayOfMonth = (int)$moment; $dayOfMonth = 0 === $dayOfMonth ? 1 : $dayOfMonth; if ($mutator->day > $dayOfMonth) { Log::debug(sprintf('%d is after %d, add a month. Mutator is now...', $mutator->day, $dayOfMonth)); @@ -87,7 +87,7 @@ trait CalculateXOccurrencesSince ++$total; } ++$attempts; - $mutator = $mutator->endOfMonth()->addDay(); + $mutator = $mutator->endOfMonth()->addDay(); } Log::debug('Collected enough occurrences.'); @@ -103,10 +103,10 @@ trait CalculateXOccurrencesSince protected function getXNDomOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; $mutator->addDay(); // always assume today has passed. $mutator->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? @@ -137,15 +137,15 @@ trait CalculateXOccurrencesSince protected function getXWeeklyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; // monday = 1 // sunday = 7 // Removed assumption today has passed, see issue https://github.com/firefly-iii/firefly-iii/issues/4798 // $mutator->addDay(); // always assume today has passed. - $dayOfWeek = (int) $moment; + $dayOfWeek = (int)$moment; if ($mutator->dayOfWeekIso > $dayOfWeek) { // day has already passed this week, add one week: $mutator->addWeek(); @@ -189,7 +189,7 @@ trait CalculateXOccurrencesSince $date->addYear(); Log::debug(sprintf('Date is now %s', $date->format('Y-m-d'))); } - $obj = clone $date; + $obj = clone $date; while ($total < $count) { Log::debug(sprintf('total (%d) < count (%d) so go.', $total, $count)); Log::debug(sprintf('attempts (%d) %% skipmod (%d) === %d', $attempts, $skipMod, $attempts % $skipMod)); diff --git a/app/Support/Repositories/Recurring/FiltersWeekends.php b/app/Support/Repositories/Recurring/FiltersWeekends.php index 508e13f638..886679ced1 100644 --- a/app/Support/Repositories/Recurring/FiltersWeekends.php +++ b/app/Support/Repositories/Recurring/FiltersWeekends.php @@ -46,7 +46,7 @@ trait FiltersWeekends return $dates; } - $return = []; + $return = []; /** @var Carbon $date */ foreach ($dates as $date) { @@ -60,7 +60,7 @@ trait FiltersWeekends // is weekend and must set back to Friday? if (RecurrenceRepetitionWeekend::WEEKEND_TO_FRIDAY->value === $repetition->weekend) { - $clone = clone $date; + $clone = clone $date; $clone->addDays(5 - $date->dayOfWeekIso); Log::debug( sprintf('Date is %s, and this is in the weekend, so corrected to %s (Friday).', $date->format('D d M Y'), $clone->format('D d M Y')) @@ -72,7 +72,7 @@ trait FiltersWeekends // postpone to Monday? if (RecurrenceRepetitionWeekend::WEEKEND_TO_MONDAY->value === $repetition->weekend) { - $clone = clone $date; + $clone = clone $date; $clone->addDays(8 - $date->dayOfWeekIso); Log::debug( sprintf('Date is %s, and this is in the weekend, so corrected to %s (Monday).', $date->format('D d M Y'), $clone->format('D d M Y')) diff --git a/app/Support/Repositories/UserGroup/UserGroupInterface.php b/app/Support/Repositories/UserGroup/UserGroupInterface.php index d7a737b919..67e7fe3ea3 100644 --- a/app/Support/Repositories/UserGroup/UserGroupInterface.php +++ b/app/Support/Repositories/UserGroup/UserGroupInterface.php @@ -37,7 +37,7 @@ interface UserGroupInterface public function getUserGroup(): ?UserGroup; - public function setUser(Authenticatable|User|null $user): void; + public function setUser(Authenticatable | User | null $user): void; public function setUserGroup(UserGroup $userGroup): void; diff --git a/app/Support/Repositories/UserGroup/UserGroupTrait.php b/app/Support/Repositories/UserGroup/UserGroupTrait.php index 98781e5596..b6a1c94f5a 100644 --- a/app/Support/Repositories/UserGroup/UserGroupTrait.php +++ b/app/Support/Repositories/UserGroup/UserGroupTrait.php @@ -61,10 +61,10 @@ trait UserGroupTrait /** * @throws FireflyException */ - public function setUser(Authenticatable|User|null $user): void + public function setUser(Authenticatable | User | null $user): void { if ($user instanceof User) { - $this->user = $user; + $this->user = $user; if (null === $user->userGroup) { throw new FireflyException(sprintf('User #%d ("%s") has no user group.', $user->id, $user->email)); } @@ -99,15 +99,14 @@ trait UserGroupTrait public function setUserGroupById(int $userGroupId): void { $memberships = GroupMembership::where('user_id', $this->user->id) - ->where('user_group_id', $userGroupId) - ->count() - ; + ->where('user_group_id', $userGroupId) + ->count(); if (0 === $memberships) { throw new FireflyException(sprintf('User #%d has no access to administration #%d', $this->user->id, $userGroupId)); } /** @var null|UserGroup $userGroup */ - $userGroup = UserGroup::find($userGroupId); + $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { throw new FireflyException(sprintf('Cannot find administration for user #%d', $this->user->id)); } diff --git a/app/Support/Request/AppendsLocationData.php b/app/Support/Request/AppendsLocationData.php index 149898f986..01a6de40d3 100644 --- a/app/Support/Request/AppendsLocationData.php +++ b/app/Support/Request/AppendsLocationData.php @@ -46,19 +46,17 @@ trait AppendsLocationData return $return; } - private function validLongitude(string $longitude): bool - { - $number = (float) $longitude; - - return $number >= -180 && $number <= 180; - } - - private function validLatitude(string $latitude): bool - { - $number = (float) $latitude; - - return $number >= -90 && $number <= 90; - } + /** + * Abstract method stolen from "InteractsWithInput". + * + * @param null $key + * @param bool $default + * + * @return mixed + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + abstract public function boolean($key = null, $default = false); /** * Abstract method. @@ -69,6 +67,22 @@ trait AppendsLocationData */ abstract public function has($key); + /** + * Abstract method. + * + * @return string + */ + abstract public function method(); + + /** + * Abstract method. + * + * @param mixed ...$patterns + * + * @return mixed + */ + abstract public function routeIs(...$patterns); + /** * Read the submitted Request data and add new or updated Location data to the array. */ @@ -82,12 +96,12 @@ trait AppendsLocationData $data['latitude'] = null; $data['zoom_level'] = null; - $longitudeKey = $this->getLocationKey($prefix, 'longitude'); - $latitudeKey = $this->getLocationKey($prefix, 'latitude'); - $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); - $isValidPOST = $this->isValidPost($prefix); - $isValidPUT = $this->isValidPUT($prefix); - $isValidEmptyPUT = $this->isValidEmptyPUT($prefix); + $longitudeKey = $this->getLocationKey($prefix, 'longitude'); + $latitudeKey = $this->getLocationKey($prefix, 'latitude'); + $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); + $isValidPOST = $this->isValidPost($prefix); + $isValidPUT = $this->isValidPUT($prefix); + $isValidEmptyPUT = $this->isValidEmptyPUT($prefix); // for a POST (store), all fields must be present and not NULL. if ($isValidPOST) { @@ -132,72 +146,22 @@ trait AppendsLocationData return sprintf('%s_%s', $prefix, $key); } - private function isValidPost(?string $prefix): bool + private function isValidEmptyPUT(?string $prefix): bool { - app('log')->debug('Now in isValidPost()'); - $longitudeKey = $this->getLocationKey($prefix, 'longitude'); - $latitudeKey = $this->getLocationKey($prefix, 'latitude'); - $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); - $hasLocationKey = $this->getLocationKey($prefix, 'has_location'); - // fields must not be null: - if (null !== $this->get($longitudeKey) && null !== $this->get($latitudeKey) && null !== $this->get($zoomLevelKey)) { - app('log')->debug('All fields present'); - // if is POST and route contains API, this is enough: - if ('POST' === $this->method() && $this->routeIs('api.v1.*')) { - app('log')->debug('Is API location'); + $longitudeKey = $this->getLocationKey($prefix, 'longitude'); + $latitudeKey = $this->getLocationKey($prefix, 'latitude'); + $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); - return true; - } - // if is POST and route does not contain API, must also have "has_location" = true - if ('POST' === $this->method() && $this->routeIs('*.store') && !$this->routeIs('api.v1.*') && '' !== $hasLocationKey) { - app('log')->debug('Is POST + store route.'); - $hasLocation = $this->boolean($hasLocationKey); - if (true === $hasLocation) { - app('log')->debug('Has form form location'); - - return true; - } - app('log')->debug('Does not have form location'); - - return false; - } - app('log')->debug('Is not POST API or POST form'); - - return false; - } - app('log')->debug('Fields not present'); - - return false; + return ( + null === $this->get($longitudeKey) + && null === $this->get($latitudeKey) + && null === $this->get($zoomLevelKey)) + && ( + 'PUT' === $this->method() + || ('POST' === $this->method() && $this->routeIs('*.update')) + ); } - /** - * Abstract method. - * - * @return string - */ - abstract public function method(); - - /** - * Abstract method. - * - * @param mixed ...$patterns - * - * @return mixed - */ - abstract public function routeIs(...$patterns); - - /** - * Abstract method stolen from "InteractsWithInput". - * - * @param null $key - * @param bool $default - * - * @return mixed - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - abstract public function boolean($key = null, $default = false); - private function isValidPUT(?string $prefix): bool { $longitudeKey = $this->getLocationKey($prefix, 'longitude'); @@ -238,19 +202,55 @@ trait AppendsLocationData return false; } - private function isValidEmptyPUT(?string $prefix): bool + private function isValidPost(?string $prefix): bool { - $longitudeKey = $this->getLocationKey($prefix, 'longitude'); - $latitudeKey = $this->getLocationKey($prefix, 'latitude'); - $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); + app('log')->debug('Now in isValidPost()'); + $longitudeKey = $this->getLocationKey($prefix, 'longitude'); + $latitudeKey = $this->getLocationKey($prefix, 'latitude'); + $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); + $hasLocationKey = $this->getLocationKey($prefix, 'has_location'); + // fields must not be null: + if (null !== $this->get($longitudeKey) && null !== $this->get($latitudeKey) && null !== $this->get($zoomLevelKey)) { + app('log')->debug('All fields present'); + // if is POST and route contains API, this is enough: + if ('POST' === $this->method() && $this->routeIs('api.v1.*')) { + app('log')->debug('Is API location'); - return ( - null === $this->get($longitudeKey) - && null === $this->get($latitudeKey) - && null === $this->get($zoomLevelKey)) - && ( - 'PUT' === $this->method() - || ('POST' === $this->method() && $this->routeIs('*.update')) - ); + return true; + } + // if is POST and route does not contain API, must also have "has_location" = true + if ('POST' === $this->method() && $this->routeIs('*.store') && !$this->routeIs('api.v1.*') && '' !== $hasLocationKey) { + app('log')->debug('Is POST + store route.'); + $hasLocation = $this->boolean($hasLocationKey); + if (true === $hasLocation) { + app('log')->debug('Has form form location'); + + return true; + } + app('log')->debug('Does not have form location'); + + return false; + } + app('log')->debug('Is not POST API or POST form'); + + return false; + } + app('log')->debug('Fields not present'); + + return false; + } + + private function validLatitude(string $latitude): bool + { + $number = (float)$latitude; + + return $number >= -90 && $number <= 90; + } + + private function validLongitude(string $longitude): bool + { + $number = (float)$longitude; + + return $number >= -180 && $number <= 180; } } diff --git a/app/Support/Request/ChecksLogin.php b/app/Support/Request/ChecksLogin.php index 9fd2e11883..8576b17473 100644 --- a/app/Support/Request/ChecksLogin.php +++ b/app/Support/Request/ChecksLogin.php @@ -40,7 +40,7 @@ trait ChecksLogin { app('log')->debug(sprintf('Now in %s', __METHOD__)); // Only allow logged-in users - $check = auth()->check(); + $check = auth()->check(); if (!$check) { return false; } @@ -79,19 +79,19 @@ trait ChecksLogin public function getUserGroup(): ?UserGroup { /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); app('log')->debug('Now in getUserGroup()'); /** @var null|UserGroup $userGroup */ $userGroup = $this->route()?->parameter('userGroup'); if (null === $userGroup) { app('log')->debug('Request class has no userGroup parameter, but perhaps there is a parameter.'); - $userGroupId = (int) $this->get('user_group_id'); + $userGroupId = (int)$this->get('user_group_id'); if (0 === $userGroupId) { app('log')->debug(sprintf('Request class has no user_group_id parameter, grab default from user (group #%d).', $user->user_group_id)); - $userGroupId = (int) $user->user_group_id; + $userGroupId = (int)$user->user_group_id; } - $userGroup = UserGroup::find($userGroupId); + $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { app('log')->error(sprintf('Request class has user_group_id (#%d), but group does not exist.', $userGroupId)); diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index bc8da96efb..aa3896eb72 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -31,7 +31,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; - use function Safe\preg_replace; /** @@ -99,28 +98,6 @@ trait ConvertsDataTypes return Steam::filterSpaces($string); } - public function convertSortParameters(string $field, string $class): array - { - // assume this all works, because the validator would have caught any errors. - $parameter = (string)request()->query->get($field); - if ('' === $parameter) { - return []; - } - $parts = explode(',', $parameter); - $sortParameters = []; - foreach ($parts as $part) { - $part = trim($part); - $direction = 'asc'; - if ('-' === $part[0]) { - $part = substr($part, 1); - $direction = 'desc'; - } - $sortParameters[] = [$part, $direction]; - } - - return $sortParameters; - } - public function clearString(?string $string): ?string { $string = $this->clearStringKeepNewlines($string); @@ -159,6 +136,36 @@ trait ConvertsDataTypes return Steam::filterSpaces($this->convertString($field)); } + /** + * Return integer value. + */ + public function convertInteger(string $field): int + { + return (int)$this->get($field); + } + + public function convertSortParameters(string $field, string $class): array + { + // assume this all works, because the validator would have caught any errors. + $parameter = (string)request()->query->get($field); + if ('' === $parameter) { + return []; + } + $parts = explode(',', $parameter); + $sortParameters = []; + foreach ($parts as $part) { + $part = trim($part); + $direction = 'asc'; + if ('-' === $part[0]) { + $part = substr($part, 1); + $direction = 'desc'; + } + $sortParameters[] = [$part, $direction]; + } + + return $sortParameters; + } + /** * Return string value. */ @@ -178,14 +185,6 @@ trait ConvertsDataTypes */ abstract public function get(string $key, mixed $default = null): mixed; - /** - * Return integer value. - */ - public function convertInteger(string $field): int - { - return (int)$this->get($field); - } - /** * TODO duplicate, see SelectTransactionsRequest * @@ -218,6 +217,16 @@ trait ConvertsDataTypes return $collection; } + /** + * Abstract method that always exists in the Request classes that use this + * trait, OR a stub needs to be added by any other class that uses this train. + * + * @param mixed $key + * + * @return mixed + */ + abstract public function has($key); + /** * Return string value with newlines. */ @@ -386,16 +395,6 @@ trait ConvertsDataTypes return $return; } - /** - * Abstract method that always exists in the Request classes that use this - * trait, OR a stub needs to be added by any other class that uses this train. - * - * @param mixed $key - * - * @return mixed - */ - abstract public function has($key); - /** * Return date or NULL. */ @@ -418,6 +417,21 @@ trait ConvertsDataTypes return $result; } + /** + * Parse to integer + */ + protected function integerFromValue(?string $string): ?int + { + if (null === $string) { + return null; + } + if ('' === $string) { + return null; + } + + return (int)$string; + } + /** * Return integer value, or NULL when it's not set. */ @@ -445,7 +459,7 @@ trait ConvertsDataTypes if (!is_array($entry)) { continue; } - $amount = null; + $amount = null; if (array_key_exists('current_amount', $entry)) { $amount = $this->clearString((string)($entry['current_amount'] ?? '0')); if (null === $entry['current_amount']) { @@ -463,19 +477,4 @@ trait ConvertsDataTypes return $return; } - - /** - * Parse to integer - */ - protected function integerFromValue(?string $string): ?int - { - if (null === $string) { - return null; - } - if ('' === $string) { - return null; - } - - return (int)$string; - } } diff --git a/app/Support/Request/GetRecurrenceData.php b/app/Support/Request/GetRecurrenceData.php index 50dc0cd8f2..40f23738dd 100644 --- a/app/Support/Request/GetRecurrenceData.php +++ b/app/Support/Request/GetRecurrenceData.php @@ -38,12 +38,12 @@ trait GetRecurrenceData foreach ($stringKeys as $key) { if (array_key_exists($key, $transaction)) { - $return[$key] = (string) $transaction[$key]; + $return[$key] = (string)$transaction[$key]; } } foreach ($intKeys as $key) { if (array_key_exists($key, $transaction)) { - $return[$key] = (int) $transaction[$key]; + $return[$key] = (int)$transaction[$key]; } } foreach ($keys as $key) { diff --git a/app/Support/Request/ValidatesWebhooks.php b/app/Support/Request/ValidatesWebhooks.php index 5647184ef4..dff1541fde 100644 --- a/app/Support/Request/ValidatesWebhooks.php +++ b/app/Support/Request/ValidatesWebhooks.php @@ -25,10 +25,10 @@ declare(strict_types=1); namespace FireflyIII\Support\Request; -use Illuminate\Validation\Validator; use FireflyIII\Enums\WebhookTrigger; use FireflyIII\Models\Webhook; use Illuminate\Support\Facades\Log; +use Illuminate\Validation\Validator; trait ValidatesWebhooks { @@ -40,9 +40,9 @@ trait ValidatesWebhooks if (count($validator->failed()) > 0) { return; } - $data = $validator->getData(); - $triggers = $data['triggers'] ?? []; - $responses = $data['responses'] ?? []; + $data = $validator->getData(); + $triggers = $data['triggers'] ?? []; + $responses = $data['responses'] ?? []; if (0 === count($triggers) || 0 === count($responses)) { Log::debug('No trigger or response, return.'); diff --git a/app/Support/Search/AccountSearch.php b/app/Support/Search/AccountSearch.php index 99b89f9c99..fe6e817c72 100644 --- a/app/Support/Search/AccountSearch.php +++ b/app/Support/Search/AccountSearch.php @@ -28,7 +28,6 @@ use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; - use function Safe\json_encode; /** @@ -37,16 +36,16 @@ use function Safe\json_encode; class AccountSearch implements GenericSearchInterface { /** @var string */ - public const string SEARCH_ALL = 'all'; + public const string SEARCH_ALL = 'all'; /** @var string */ - public const string SEARCH_IBAN = 'iban'; + public const string SEARCH_IBAN = 'iban'; /** @var string */ - public const string SEARCH_ID = 'id'; + public const string SEARCH_ID = 'id'; /** @var string */ - public const string SEARCH_NAME = 'name'; + public const string SEARCH_NAME = 'name'; /** @var string */ public const string SEARCH_NUMBER = 'number'; @@ -63,10 +62,9 @@ class AccountSearch implements GenericSearchInterface public function search(): Collection { $searchQuery = $this->user->accounts() - ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') - ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') - ->whereIn('account_types.type', $this->types) - ; + ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') + ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->whereIn('account_types.type', $this->types); $like = sprintf('%%%s%%', $this->query); $originalQuery = $this->query; @@ -92,7 +90,7 @@ class AccountSearch implements GenericSearchInterface break; case self::SEARCH_ID: - $searchQuery->where('accounts.id', '=', (int) $originalQuery); + $searchQuery->where('accounts.id', '=', (int)$originalQuery); break; @@ -137,7 +135,7 @@ class AccountSearch implements GenericSearchInterface $this->types = $types; } - public function setUser(Authenticatable|User|null $user): void + public function setUser(Authenticatable | User | null $user): void { if ($user instanceof User) { $this->user = $user; diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index dc975609f0..2e813bd125 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -105,6 +105,41 @@ class OperatorQuerySearch implements SearchInterface $this->currencyRepository = app(CurrencyRepositoryInterface::class); } + /** + * @throws FireflyException + */ + public static function getRootOperator(string $operator): string + { + $original = $operator; + // if the string starts with "-" (not), we can remove it and recycle + // the configuration from the original operator. + if (str_starts_with($operator, '-')) { + $operator = substr($operator, 1); + } + + $config = config(sprintf('search.operators.%s', $operator)); + if (null === $config) { + throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator)); + } + if (true === $config['alias']) { + $return = $config['alias_for']; + if (str_starts_with($original, '-')) { + $return = sprintf('-%s', $config['alias_for']); + } + Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $original, $return)); + + return $return; + } + Log::debug(sprintf('"%s" is not an alias.', $operator)); + + return $original; + } + + public function getExcludedWords(): array + { + return $this->prohibitedWords; + } + public function getInvalidOperators(): array { return $this->invalidOperators; @@ -120,6 +155,11 @@ class OperatorQuerySearch implements SearchInterface return $this->operators; } + public function getWords(): array + { + return $this->words; + } + public function getWordsAsString(): string { return implode(' ', $this->words); @@ -146,7 +186,7 @@ class OperatorQuerySearch implements SearchInterface try { $parsedQuery = $parser->parse($query); - } catch (LogicException|TypeError $e) { + } catch (LogicException | TypeError $e) { Log::error($e->getMessage()); Log::error(sprintf('Could not parse search: "%s".', $query)); @@ -163,6 +203,124 @@ class OperatorQuerySearch implements SearchInterface $this->collector->excludeSearchWords($this->prohibitedWords); } + public function searchTime(): float + { + return microtime(true) - $this->startTime; + } + + public function searchTransactions(): LengthAwarePaginator + { + $this->parseTagInstructions(); + if (0 === count($this->getWords()) && 0 === count($this->getExcludedWords()) && 0 === count($this->getOperators())) { + return new LengthAwarePaginator([], 0, 5, 1); + } + + return $this->collector->getPaginatedGroups(); + } + + public function setDate(Carbon $date): void + { + $this->date = $date; + } + + public function setLimit(int $limit): void + { + $this->limit = $limit; + $this->collector->setLimit($this->limit); + } + + public function setPage(int $page): void + { + $this->page = $page; + $this->collector->setPage($this->page); + } + + public function setUser(User $user): void + { + $this->accountRepository->setUser($user); + $this->billRepository->setUser($user); + $this->categoryRepository->setUser($user); + $this->budgetRepository->setUser($user); + $this->tagRepository->setUser($user); + $this->collector = app(GroupCollectorInterface::class); + $this->collector->setUser($user); + $this->collector->withAccountInformation()->withCategoryInformation()->withBudgetInformation(); + + $this->setLimit((int)app('preferences')->getForUser($user, 'listPageSize', 50)->data); + } + + private function findCurrency(string $value): ?TransactionCurrency + { + if (str_contains($value, '(') && str_contains($value, ')')) { + // bad method to split and get the currency code: + $parts = explode(' ', $value); + $value = trim($parts[count($parts) - 1], "() \t\n\r\0\x0B"); + } + $result = $this->currencyRepository->findByCode($value); + if (null === $result) { + return $this->currencyRepository->findByName($value); + } + + return $result; + } + + private function getCashAccount(): Account + { + return $this->accountRepository->getCashAccount(); + } + + /** + * @throws FireflyException + */ + private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void + { + $operator = strtolower($node->getOperator()); + $value = $node->getValue(); + $prohibited = $node->isProhibited($flipProhibitedFlag); + + $context = config(sprintf('search.operators.%s.needs_context', $operator)); + + // is an operator that needs no context, and value is false, then prohibited = true. + if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) { + $prohibited = true; + $value = 'true'; + } + // if the operator is prohibited, but the value is false, do an uno reverse + if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) { + $prohibited = false; + $value = 'true'; + } + + // must be valid operator: + $inArray = in_array($operator, $this->validOperators, true); + if ($inArray) { + if ($this->updateCollector($operator, $value, $prohibited)) { + $this->operators->push([ + 'type' => self::getRootOperator($operator), + 'value' => $value, + 'prohibited' => $prohibited, + ]); + Log::debug(sprintf('Added operator type "%s"', $operator)); + } + } + if (!$inArray) { + Log::debug(sprintf('Added INVALID operator type "%s"', $operator)); + $this->invalidOperators[] = [ + 'type' => $operator, + 'value' => $value, + ]; + } + } + + private function handleNodeGroup(NodeGroup $node, bool $flipProhibitedFlag): void + { + $prohibited = $node->isProhibited($flipProhibitedFlag); + + foreach ($node->getNodes() as $subNode) { + $this->handleSearchNode($subNode, $prohibited); + } + } + /** * @throws FireflyException * @@ -197,7 +355,7 @@ class OperatorQuerySearch implements SearchInterface private function handleStringNode(StringNode $node, bool $flipProhibitedFlag): void { - $string = $node->getValue(); + $string = $node->getValue(); $prohibited = $node->isProhibited($flipProhibitedFlag); @@ -214,43 +372,857 @@ class OperatorQuerySearch implements SearchInterface /** * @throws FireflyException */ - private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void + private function parseDateRange(string $type, string $value): array { - $operator = strtolower($node->getOperator()); - $value = $node->getValue(); - $prohibited = $node->isProhibited($flipProhibitedFlag); - - $context = config(sprintf('search.operators.%s.needs_context', $operator)); - - // is an operator that needs no context, and value is false, then prohibited = true. - if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) { - $prohibited = true; - $value = 'true'; - } - // if the operator is prohibited, but the value is false, do an uno reverse - if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) { - $prohibited = false; - $value = 'true'; + $parser = new ParseDateString(); + if ($parser->isDateRange($value)) { + return $parser->parseRange($value); } - // must be valid operator: - $inArray = in_array($operator, $this->validOperators, true); - if ($inArray) { - if ($this->updateCollector($operator, $value, $prohibited)) { - $this->operators->push([ - 'type' => self::getRootOperator($operator), - 'value' => $value, - 'prohibited' => $prohibited, - ]); - Log::debug(sprintf('Added operator type "%s"', $operator)); - } - } - if (!$inArray) { - Log::debug(sprintf('Added INVALID operator type "%s"', $operator)); + try { + $parsedDate = $parser->parseDate($value); + } catch (FireflyException) { + Log::debug(sprintf('Could not parse date "%s", will return empty array.', $value)); $this->invalidOperators[] = [ - 'type' => $operator, + 'type' => $type, 'value' => $value, ]; + + return []; + } + + return [ + 'exact' => $parsedDate, + ]; + } + + private function parseTagInstructions(): void + { + Log::debug('Now in parseTagInstructions()'); + // if exclude tags, remove excluded tags. + if (count($this->excludeTags) > 0) { + Log::debug(sprintf('%d exclude tag(s)', count($this->excludeTags))); + $collection = new Collection(); + foreach ($this->excludeTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + Log::debug(sprintf('Exclude tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + Log::debug(sprintf('Selecting all tags except %d excluded tag(s).', $collection->count())); + $this->collector->setWithoutSpecificTags($collection); + } + // if include tags, include them: + if (count($this->includeTags) > 0) { + Log::debug(sprintf('%d include tag(s)', count($this->includeTags))); + $collection = new Collection(); + foreach ($this->includeTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + Log::debug(sprintf('Include tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + $this->collector->setAllTags($collection); + } + // if include ANY tags, include them: (see #8632) + if (count($this->includeAnyTags) > 0) { + Log::debug(sprintf('%d include ANY tag(s)', count($this->includeAnyTags))); + $collection = new Collection(); + foreach ($this->includeAnyTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + Log::debug(sprintf('Include ANY tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + $this->collector->setTags($collection); + } + } + + /** + * searchDirection: 1 = source (default), 2 = destination, 3 = both + * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + * @SuppressWarnings("PHPMD.NPathComplexity") + */ + private function searchAccount(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void + { + Log::debug(sprintf('searchAccount("%s", %s, %s)', $value, $stringPosition->name, $searchDirection->name)); + + // search direction (default): for source accounts + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setSourceAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeSourceAccounts'; + } + + // search direction: for destination accounts + if (SearchDirection::DESTINATION === $searchDirection) { // destination + // destination can be + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; + $collectorMethod = 'setDestinationAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeDestinationAccounts'; + } + } + // either account could be: + if (SearchDirection::BOTH === $searchDirection) { + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeAccounts'; + } + } + // string position (default): starts with: + $stringMethod = 'str_starts_with'; + + // string position: ends with: + if (StringPosition::ENDS === $stringPosition) { + $stringMethod = 'str_ends_with'; + } + if (StringPosition::CONTAINS === $stringPosition) { + $stringMethod = 'str_contains'; + } + if (StringPosition::IS === $stringPosition) { + $stringMethod = 'stringIsEqual'; + } + + // get accounts: + $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337); + if (0 === $accounts->count() && false === $prohibited) { + Log::warning('Found zero accounts, search for non existing account, NO results will be returned.'); + $this->collector->findNothing(); + + return; + } + if (0 === $accounts->count() && true === $prohibited) { + Log::debug('Found zero accounts, but the search is negated, so effectively we ignore the search parameter.'); + + return; + } + Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); + $filtered = $accounts->filter( + static fn(Account $account) => $stringMethod(strtolower($account->name), strtolower($value)) + ); + + if (0 === $filtered->count()) { + Log::warning('Left with zero accounts, so cannot find anything, NO results will be returned.'); + $this->collector->findNothing(); + + return; + } + Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); + $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line + } + + /** + * TODO make enums + * searchDirection: 1 = source (default), 2 = destination, 3 = both + * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + * @SuppressWarnings("PHPMD.NPathComplexity") + */ + private function searchAccountNr(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void + { + Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection->name, $stringPosition->name)); + + // search direction (default): for source accounts + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setSourceAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeSourceAccounts'; + } + + // search direction: for destination accounts + if (SearchDirection::DESTINATION === $searchDirection) { + // destination can be + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; + $collectorMethod = 'setDestinationAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeDestinationAccounts'; + } + } + + // either account could be: + if (SearchDirection::BOTH === $searchDirection) { + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeAccounts'; + } + } + + // string position (default): starts with: + $stringMethod = 'str_starts_with'; + + // string position: ends with: + if (StringPosition::ENDS === $stringPosition) { + $stringMethod = 'str_ends_with'; + } + if (StringPosition::CONTAINS === $stringPosition) { + $stringMethod = 'str_contains'; + } + if (StringPosition::IS === $stringPosition) { + $stringMethod = 'stringIsEqual'; + } + + // search for accounts: + $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337); + if (0 === $accounts->count()) { + Log::debug('Found zero accounts, search for invalid account.'); + Log::warning('Call to findNothing() from searchAccountNr().'); + $this->collector->findNothing(); + + return; + } + + // if found, do filter + Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); + $filtered = $accounts->filter( + static function (Account $account) use ($value, $stringMethod) { + // either IBAN or account number + $ibanMatch = $stringMethod(strtolower((string)$account->iban), strtolower($value)); + $accountNrMatch = false; + + /** @var AccountMeta $meta */ + foreach ($account->accountMeta as $meta) { + if ('account_number' === $meta->name && $stringMethod(strtolower((string)$meta->data), strtolower($value))) { + $accountNrMatch = true; + } + } + + return $ibanMatch || $accountNrMatch; + } + ); + + if (0 === $filtered->count()) { + Log::debug('Left with zero, search for invalid account'); + Log::warning('Call to findNothing() from searchAccountNr().'); + $this->collector->findNothing(); + + return; + } + Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); + $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setDateAfterParams(array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setDateAfterParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setAfter($value); + $this->operators->push(['type' => 'date_after', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); + $this->collector->yearAfter($value); + $this->operators->push(['type' => 'date_after_year', 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); + $this->collector->monthAfter($value); + $this->operators->push(['type' => 'date_after_month', 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); + $this->collector->dayAfter($value); + $this->operators->push(['type' => 'date_after_day', 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setDateBeforeParams(array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setDateBeforeParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setBefore($value); + $this->operators->push(['type' => 'date_before', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); + $this->collector->yearBefore($value); + $this->operators->push(['type' => 'date_before_year', 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); + $this->collector->monthBefore($value); + $this->operators->push(['type' => 'date_before_month', 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); + $this->collector->dayBefore($value); + $this->operators->push(['type' => 'date_before_day', 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setExactDateParams(array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setExactParameters()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set date_is_exact value "%s"', $value->format('Y-m-d'))); + $this->collector->setRange($value, $value); + $this->operators->push(['type' => 'date_on', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'exact_not': + if ($value instanceof Carbon) { + $this->collector->excludeRange($value, $value); + $this->operators->push(['type' => 'not_date_on', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact YEAR value "%s"', $value)); + $this->collector->yearIs($value); + $this->operators->push(['type' => 'date_on_year', 'value' => $value]); + } + + break; + + case 'year_not': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact_not YEAR value "%s"', $value)); + $this->collector->yearIsNot($value); + $this->operators->push(['type' => 'not_date_on_year', 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact MONTH value "%s"', $value)); + $this->collector->monthIs($value); + $this->operators->push(['type' => 'date_on_month', 'value' => $value]); + } + + break; + + case 'month_not': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact not MONTH value "%s"', $value)); + $this->collector->monthIsNot($value); + $this->operators->push(['type' => 'not_date_on_month', 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact DAY value "%s"', $value)); + $this->collector->dayIs($value); + $this->operators->push(['type' => 'date_on_day', 'value' => $value]); + } + + break; + + case 'day_not': + if (is_string($value)) { + Log::debug(sprintf('Set not date_is_exact DAY value "%s"', $value)); + $this->collector->dayIsNot($value); + $this->operators->push(['type' => 'not_date_on_day', 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setExactMetaDateParams(string $field, array $range, bool $prohibited = false): void + { + Log::debug('Now in setExactMetaDateParams()'); + + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setExactMetaDateParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->setMetaDateRange($value, $value, $field); + $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'exact_not': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->excludeMetaDateRange($value, $value, $field); + $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->metaYearIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'year_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->metaYearIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->metaMonthIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'month_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->metaMonthIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->metaDayIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); + } + + break; + + case 'day_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->metaDayIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setExactObjectDateParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setExactObjectDateParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->setObjectRange($value, clone $value, $field); + $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'exact_not': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->excludeObjectRange($value, clone $value, $field); + $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->objectYearIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'year_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->objectYearIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->objectMonthIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'month_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->objectMonthIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->objectDayIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); + } + + break; + + case 'day_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->objectDayIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setMetaDateAfterParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateAfterParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setMetaAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_after YEAR value "%s"', $field, $value)); + $this->collector->metaYearAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_after MONTH value "%s"', $field, $value)); + $this->collector->metaMonthAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_after DAY value "%s"', $field, $value)); + $this->collector->metaDayAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setMetaDateBeforeParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateBeforeParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setMetaBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_before YEAR value "%s"', $field, $value)); + $this->collector->metaYearBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_before MONTH value "%s"', $field, $value)); + $this->collector->metaMonthBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_before DAY value "%s"', $field, $value)); + $this->collector->metaDayBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setObjectDateAfterParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateAfterParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setObjectAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); + $this->collector->objectYearAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); + $this->collector->objectMonthAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); + $this->collector->objectDayAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setObjectDateBeforeParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateBeforeParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setObjectBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); + $this->collector->objectYearBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); + $this->collector->objectMonthBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); + $this->collector->objectDayBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); + } + + break; + } } } @@ -278,15 +1250,15 @@ class OperatorQuerySearch implements SearchInterface throw new FireflyException(sprintf('Unsupported search operator: "%s"', $operator)); - // some search operators are ignored, basically: + // some search operators are ignored, basically: case 'user_action': Log::info(sprintf('Ignore search operator "%s"', $operator)); return false; - // - // all account related searches: - // + // + // all account related searches: + // case 'account_is': $this->searchAccount($value, SearchDirection::BOTH, StringPosition::IS); @@ -448,7 +1420,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'source_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->setSourceAccounts(new Collection()->push($account)); } @@ -461,7 +1433,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-source_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->excludeSourceAccounts(new Collection()->push($account)); } @@ -474,25 +1446,25 @@ class OperatorQuerySearch implements SearchInterface break; case 'journal_id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->setJournalIds($parts); break; case '-journal_id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->excludeJournalIds($parts); break; case 'id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->setIds($parts); break; case '-id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->excludeIds($parts); break; @@ -578,7 +1550,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'destination_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->setDestinationAccounts(new Collection()->push($account)); } @@ -590,7 +1562,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-destination_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->excludeDestinationAccounts(new Collection()->push($account)); } @@ -603,12 +1575,12 @@ class OperatorQuerySearch implements SearchInterface case 'account_id': Log::debug(sprintf('Now in "account_id" with value "%s"', $value)); - $parts = explode(',', $value); - $collection = new Collection(); + $parts = explode(',', $value); + $collection = new Collection(); foreach ($parts as $accountId) { - $accountId = (int) $accountId; + $accountId = (int)$accountId; Log::debug(sprintf('Searching for account with ID #%d', $accountId)); - $account = $this->accountRepository->find($accountId); + $account = $this->accountRepository->find($accountId); if (null !== $account) { Log::debug(sprintf('Found account with ID #%d ("%s")', $accountId, $account->name)); $collection->push($account); @@ -629,10 +1601,10 @@ class OperatorQuerySearch implements SearchInterface break; case '-account_id': - $parts = explode(',', $value); - $collection = new Collection(); + $parts = explode(',', $value); + $collection = new Collection(); foreach ($parts as $accountId) { - $account = $this->accountRepository->find((int) $accountId); + $account = $this->accountRepository->find((int)$accountId); if (null !== $account) { $collection->push($account); } @@ -647,48 +1619,48 @@ class OperatorQuerySearch implements SearchInterface break; - // - // cash account - // + // + // cash account + // case 'source_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setSourceAccounts(new Collection()->push($account)); break; case '-source_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeSourceAccounts(new Collection()->push($account)); break; case 'destination_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setDestinationAccounts(new Collection()->push($account)); break; case '-destination_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeDestinationAccounts(new Collection()->push($account)); break; case 'account_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setAccounts(new Collection()->push($account)); break; case '-account_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeAccounts(new Collection()->push($account)); break; - // - // description - // + // + // description + // case 'description_starts': $this->collector->descriptionStarts([$value]); @@ -710,7 +1682,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'description_contains': - $this->words[] = $value; + $this->words[] = $value; return false; @@ -729,11 +1701,11 @@ class OperatorQuerySearch implements SearchInterface break; - // - // currency - // + // + // currency + // case 'currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->setCurrency($currency); } @@ -745,7 +1717,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->excludeCurrency($currency); } @@ -757,7 +1729,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'foreign_currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->setForeignCurrency($currency); } @@ -769,7 +1741,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-foreign_currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->excludeForeignCurrency($currency); } @@ -780,9 +1752,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // attachments - // + // + // attachments + // case 'has_attachments': case '-has_no_attachments': Log::debug('Set collector to filter on attachments.'); @@ -797,8 +1769,8 @@ class OperatorQuerySearch implements SearchInterface break; - // - // categories + // + // categories case '-has_any_category': case 'has_no_category': $this->collector->withoutCategory(); @@ -812,7 +1784,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_is': - $category = $this->categoryRepository->findByName($value); + $category = $this->categoryRepository->findByName($value); if (null !== $category) { $this->collector->setCategory($category); @@ -824,7 +1796,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_is': - $category = $this->categoryRepository->findByName($value); + $category = $this->categoryRepository->findByName($value); if (null !== $category) { $this->collector->excludeCategory($category); @@ -834,7 +1806,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_ends': - $result = $this->categoryRepository->categoryEndsWith($value, 1337); + $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -846,7 +1818,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_ends': - $result = $this->categoryRepository->categoryEndsWith($value, 1337); + $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -858,7 +1830,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_starts': - $result = $this->categoryRepository->categoryStartsWith($value, 1337); + $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -870,7 +1842,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_starts': - $result = $this->categoryRepository->categoryStartsWith($value, 1337); + $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -882,7 +1854,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_contains': - $result = $this->categoryRepository->searchCategory($value, 1337); + $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -894,7 +1866,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_contains': - $result = $this->categoryRepository->searchCategory($value, 1337); + $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -905,9 +1877,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // budgets - // + // + // budgets + // case '-has_any_budget': case 'has_no_budget': $this->collector->withoutBudget(); @@ -921,7 +1893,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_contains': - $result = $this->budgetRepository->searchBudget($value, 1337); + $result = $this->budgetRepository->searchBudget($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -933,7 +1905,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_contains': - $result = $this->budgetRepository->searchBudget($value, 1337); + $result = $this->budgetRepository->searchBudget($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -945,7 +1917,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_is': - $budget = $this->budgetRepository->findByName($value); + $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { $this->collector->setBudget($budget); @@ -957,7 +1929,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_is': - $budget = $this->budgetRepository->findByName($value); + $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { $this->collector->excludeBudget($budget); @@ -969,7 +1941,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_ends': - $result = $this->budgetRepository->budgetEndsWith($value, 1337); + $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -981,7 +1953,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_ends': - $result = $this->budgetRepository->budgetEndsWith($value, 1337); + $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -993,7 +1965,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_starts': - $result = $this->budgetRepository->budgetStartsWith($value, 1337); + $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -1005,7 +1977,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_starts': - $result = $this->budgetRepository->budgetStartsWith($value, 1337); + $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -1016,9 +1988,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // bill - // + // + // bill + // case '-has_any_bill': case 'has_no_bill': $this->collector->withoutBill(); @@ -1032,7 +2004,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_contains': - $result = $this->billRepository->searchBill($value, 1337); + $result = $this->billRepository->searchBill($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); @@ -1044,7 +2016,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_contains': - $result = $this->billRepository->searchBill($value, 1337); + $result = $this->billRepository->searchBill($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); @@ -1056,7 +2028,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_is': - $bill = $this->billRepository->findByName($value); + $bill = $this->billRepository->findByName($value); if (null !== $bill) { $this->collector->setBill($bill); @@ -1068,7 +2040,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_is': - $bill = $this->billRepository->findByName($value); + $bill = $this->billRepository->findByName($value); if (null !== $bill) { $this->collector->excludeBills(new Collection()->push($bill)); @@ -1080,7 +2052,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_ends': - $result = $this->billRepository->billEndsWith($value, 1337); + $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); } @@ -1092,7 +2064,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_ends': - $result = $this->billRepository->billEndsWith($value, 1337); + $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); } @@ -1104,7 +2076,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_starts': - $result = $this->billRepository->billStartsWith($value, 1337); + $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); } @@ -1116,7 +2088,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_starts': - $result = $this->billRepository->billStartsWith($value, 1337); + $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); } @@ -1127,9 +2099,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // tags - // + // + // tags + // case '-has_any_tag': case 'has_no_tag': $this->collector->withoutTags(); @@ -1144,7 +2116,7 @@ class OperatorQuerySearch implements SearchInterface case '-tag_is_not': case 'tag_is': - $result = $this->tagRepository->findByTag($value); + $result = $this->tagRepository->findByTag($value); if (null !== $result) { $this->includeTags[] = $result->id; $this->includeTags = array_unique($this->includeTags); @@ -1159,7 +2131,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_contains': - $tags = $this->tagRepository->searchTag($value); + $tags = $this->tagRepository->searchTag($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1174,7 +2146,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_starts': - $tags = $this->tagRepository->tagStartsWith($value); + $tags = $this->tagRepository->tagStartsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1189,7 +2161,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_starts': - $tags = $this->tagRepository->tagStartsWith($value); + $tags = $this->tagRepository->tagStartsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1203,7 +2175,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_ends': - $tags = $this->tagRepository->tagEndsWith($value); + $tags = $this->tagRepository->tagEndsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1217,7 +2189,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_ends': - $tags = $this->tagRepository->tagEndsWith($value); + $tags = $this->tagRepository->tagEndsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1231,7 +2203,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_contains': - $tags = $this->tagRepository->searchTag($value)->keyBy('id'); + $tags = $this->tagRepository->searchTag($value)->keyBy('id'); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); @@ -1247,7 +2219,7 @@ class OperatorQuerySearch implements SearchInterface case '-tag_is': case 'tag_is_not': - $result = $this->tagRepository->findByTag($value); + $result = $this->tagRepository->findByTag($value); if (null !== $result) { $this->excludeTags[] = $result->id; $this->excludeTags = array_unique($this->excludeTags); @@ -1255,9 +2227,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // notes - // + // + // notes + // case 'notes_contains': $this->collector->notesContain($value); @@ -1320,14 +2292,14 @@ class OperatorQuerySearch implements SearchInterface break; - // - // amount - // + // + // amount + // case 'amount_is': // strip comma's, make dots. Log::debug(sprintf('Original value "%s"', $value)); - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIs($amount); @@ -1336,8 +2308,8 @@ class OperatorQuerySearch implements SearchInterface case '-amount_is': // strip comma's, make dots. Log::debug(sprintf('Original value "%s"', $value)); - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIsNot($amount); @@ -1345,9 +2317,9 @@ class OperatorQuerySearch implements SearchInterface case 'foreign_amount_is': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIs($amount); @@ -1355,9 +2327,9 @@ class OperatorQuerySearch implements SearchInterface case '-foreign_amount_is': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIsNot($amount); @@ -1366,9 +2338,9 @@ class OperatorQuerySearch implements SearchInterface case '-amount_more': case 'amount_less': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountLess($amount); @@ -1377,9 +2349,9 @@ class OperatorQuerySearch implements SearchInterface case '-foreign_amount_more': case 'foreign_amount_less': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountLess($amount); @@ -1389,8 +2361,8 @@ class OperatorQuerySearch implements SearchInterface case 'amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountMore($amount); @@ -1400,16 +2372,16 @@ class OperatorQuerySearch implements SearchInterface case 'foreign_amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountMore($amount); break; - // - // transaction type - // + // + // transaction type + // case 'transaction_type': $this->collector->setTypes([ucfirst($value)]); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); @@ -1422,152 +2394,152 @@ class OperatorQuerySearch implements SearchInterface break; - // - // dates - // + // + // dates + // case '-date_on': case 'date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactDateParams($range, $prohibited); return false; case 'date_before': case '-date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setDateBeforeParams($range); return false; case 'date_after': case '-date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setDateAfterParams($range); return false; case 'interest_date_on': case '-interest_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('interest_date', $range, $prohibited); return false; case 'interest_date_before': case '-interest_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('interest_date', $range); return false; case 'interest_date_after': case '-interest_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('interest_date', $range); return false; case 'book_date_on': case '-book_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('book_date', $range, $prohibited); return false; case 'book_date_before': case '-book_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('book_date', $range); return false; case 'book_date_after': case '-book_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('book_date', $range); return false; case 'process_date_on': case '-process_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('process_date', $range, $prohibited); return false; case 'process_date_before': case '-process_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('process_date', $range); return false; case 'process_date_after': case '-process_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('process_date', $range); return false; case 'due_date_on': case '-due_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('due_date', $range, $prohibited); return false; case 'due_date_before': case '-due_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('due_date', $range); return false; case 'due_date_after': case '-due_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('due_date', $range); return false; case 'payment_date_on': case '-payment_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('payment_date', $range, $prohibited); return false; case 'payment_date_before': case '-payment_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('payment_date', $range); return false; case 'payment_date_after': case '-payment_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('payment_date', $range); return false; case 'invoice_date_on': case '-invoice_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('invoice_date', $range, $prohibited); return false; case 'invoice_date_before': case '-invoice_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('invoice_date', $range); return false; case 'invoice_date_after': case '-invoice_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('invoice_date', $range); return false; @@ -1575,7 +2547,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_on': case '-created_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactObjectDateParams('created_at', $range, $prohibited); return false; @@ -1583,7 +2555,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_before': case '-created_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateBeforeParams('created_at', $range); return false; @@ -1591,7 +2563,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_after': case '-created_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateAfterParams('created_at', $range); return false; @@ -1599,7 +2571,7 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_on': case '-updated_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactObjectDateParams('updated_at', $range, $prohibited); return false; @@ -1607,7 +2579,7 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_before': case '-updated_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateBeforeParams('updated_at', $range); return false; @@ -1615,14 +2587,14 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_after': case '-updated_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateAfterParams('updated_at', $range); return false; - // - // external URL - // + // + // external URL + // case '-any_external_url': case 'no_external_url': $this->collector->withoutExternalUrl(); @@ -1687,9 +2659,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // other fields - // + // + // other fields + // case 'external_id_is': $this->collector->setExternalId($value); @@ -1947,976 +2919,4 @@ class OperatorQuerySearch implements SearchInterface return true; } - - /** - * @throws FireflyException - */ - public static function getRootOperator(string $operator): string - { - $original = $operator; - // if the string starts with "-" (not), we can remove it and recycle - // the configuration from the original operator. - if (str_starts_with($operator, '-')) { - $operator = substr($operator, 1); - } - - $config = config(sprintf('search.operators.%s', $operator)); - if (null === $config) { - throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator)); - } - if (true === $config['alias']) { - $return = $config['alias_for']; - if (str_starts_with($original, '-')) { - $return = sprintf('-%s', $config['alias_for']); - } - Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $original, $return)); - - return $return; - } - Log::debug(sprintf('"%s" is not an alias.', $operator)); - - return $original; - } - - /** - * searchDirection: 1 = source (default), 2 = destination, 3 = both - * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - * @SuppressWarnings("PHPMD.NPathComplexity") - */ - private function searchAccount(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void - { - Log::debug(sprintf('searchAccount("%s", %s, %s)', $value, $stringPosition->name, $searchDirection->name)); - - // search direction (default): for source accounts - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setSourceAccounts'; - if ($prohibited) { - $collectorMethod = 'excludeSourceAccounts'; - } - - // search direction: for destination accounts - if (SearchDirection::DESTINATION === $searchDirection) { // destination - // destination can be - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; - $collectorMethod = 'setDestinationAccounts'; - if ($prohibited) { - $collectorMethod = 'excludeDestinationAccounts'; - } - } - // either account could be: - if (SearchDirection::BOTH === $searchDirection) { - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setAccounts'; - if ($prohibited) { - $collectorMethod = 'excludeAccounts'; - } - } - // string position (default): starts with: - $stringMethod = 'str_starts_with'; - - // string position: ends with: - if (StringPosition::ENDS === $stringPosition) { - $stringMethod = 'str_ends_with'; - } - if (StringPosition::CONTAINS === $stringPosition) { - $stringMethod = 'str_contains'; - } - if (StringPosition::IS === $stringPosition) { - $stringMethod = 'stringIsEqual'; - } - - // get accounts: - $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337); - if (0 === $accounts->count() && false === $prohibited) { - Log::warning('Found zero accounts, search for non existing account, NO results will be returned.'); - $this->collector->findNothing(); - - return; - } - if (0 === $accounts->count() && true === $prohibited) { - Log::debug('Found zero accounts, but the search is negated, so effectively we ignore the search parameter.'); - - return; - } - Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); - $filtered = $accounts->filter( - static fn (Account $account) => $stringMethod(strtolower($account->name), strtolower($value)) - ); - - if (0 === $filtered->count()) { - Log::warning('Left with zero accounts, so cannot find anything, NO results will be returned.'); - $this->collector->findNothing(); - - return; - } - Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); - $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line - } - - /** - * TODO make enums - * searchDirection: 1 = source (default), 2 = destination, 3 = both - * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - * @SuppressWarnings("PHPMD.NPathComplexity") - */ - private function searchAccountNr(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void - { - Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection->name, $stringPosition->name)); - - // search direction (default): for source accounts - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setSourceAccounts'; - if (true === $prohibited) { - $collectorMethod = 'excludeSourceAccounts'; - } - - // search direction: for destination accounts - if (SearchDirection::DESTINATION === $searchDirection) { - // destination can be - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; - $collectorMethod = 'setDestinationAccounts'; - if (true === $prohibited) { - $collectorMethod = 'excludeDestinationAccounts'; - } - } - - // either account could be: - if (SearchDirection::BOTH === $searchDirection) { - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setAccounts'; - if (true === $prohibited) { - $collectorMethod = 'excludeAccounts'; - } - } - - // string position (default): starts with: - $stringMethod = 'str_starts_with'; - - // string position: ends with: - if (StringPosition::ENDS === $stringPosition) { - $stringMethod = 'str_ends_with'; - } - if (StringPosition::CONTAINS === $stringPosition) { - $stringMethod = 'str_contains'; - } - if (StringPosition::IS === $stringPosition) { - $stringMethod = 'stringIsEqual'; - } - - // search for accounts: - $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337); - if (0 === $accounts->count()) { - Log::debug('Found zero accounts, search for invalid account.'); - Log::warning('Call to findNothing() from searchAccountNr().'); - $this->collector->findNothing(); - - return; - } - - // if found, do filter - Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); - $filtered = $accounts->filter( - static function (Account $account) use ($value, $stringMethod) { - // either IBAN or account number - $ibanMatch = $stringMethod(strtolower((string) $account->iban), strtolower($value)); - $accountNrMatch = false; - - /** @var AccountMeta $meta */ - foreach ($account->accountMeta as $meta) { - if ('account_number' === $meta->name && $stringMethod(strtolower((string) $meta->data), strtolower($value))) { - $accountNrMatch = true; - } - } - - return $ibanMatch || $accountNrMatch; - } - ); - - if (0 === $filtered->count()) { - Log::debug('Left with zero, search for invalid account'); - Log::warning('Call to findNothing() from searchAccountNr().'); - $this->collector->findNothing(); - - return; - } - Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); - $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line - } - - private function getCashAccount(): Account - { - return $this->accountRepository->getCashAccount(); - } - - private function findCurrency(string $value): ?TransactionCurrency - { - if (str_contains($value, '(') && str_contains($value, ')')) { - // bad method to split and get the currency code: - $parts = explode(' ', $value); - $value = trim($parts[count($parts) - 1], "() \t\n\r\0\x0B"); - } - $result = $this->currencyRepository->findByCode($value); - if (null === $result) { - return $this->currencyRepository->findByName($value); - } - - return $result; - } - - /** - * @throws FireflyException - */ - private function parseDateRange(string $type, string $value): array - { - $parser = new ParseDateString(); - if ($parser->isDateRange($value)) { - return $parser->parseRange($value); - } - - try { - $parsedDate = $parser->parseDate($value); - } catch (FireflyException) { - Log::debug(sprintf('Could not parse date "%s", will return empty array.', $value)); - $this->invalidOperators[] = [ - 'type' => $type, - 'value' => $value, - ]; - - return []; - } - - return [ - 'exact' => $parsedDate, - ]; - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setExactDateParams(array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setExactParameters()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set date_is_exact value "%s"', $value->format('Y-m-d'))); - $this->collector->setRange($value, $value); - $this->operators->push(['type' => 'date_on', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'exact_not': - if ($value instanceof Carbon) { - $this->collector->excludeRange($value, $value); - $this->operators->push(['type' => 'not_date_on', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact YEAR value "%s"', $value)); - $this->collector->yearIs($value); - $this->operators->push(['type' => 'date_on_year', 'value' => $value]); - } - - break; - - case 'year_not': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact_not YEAR value "%s"', $value)); - $this->collector->yearIsNot($value); - $this->operators->push(['type' => 'not_date_on_year', 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact MONTH value "%s"', $value)); - $this->collector->monthIs($value); - $this->operators->push(['type' => 'date_on_month', 'value' => $value]); - } - - break; - - case 'month_not': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact not MONTH value "%s"', $value)); - $this->collector->monthIsNot($value); - $this->operators->push(['type' => 'not_date_on_month', 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact DAY value "%s"', $value)); - $this->collector->dayIs($value); - $this->operators->push(['type' => 'date_on_day', 'value' => $value]); - } - - break; - - case 'day_not': - if (is_string($value)) { - Log::debug(sprintf('Set not date_is_exact DAY value "%s"', $value)); - $this->collector->dayIsNot($value); - $this->operators->push(['type' => 'not_date_on_day', 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setDateBeforeParams(array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setDateBeforeParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setBefore($value); - $this->operators->push(['type' => 'date_before', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); - $this->collector->yearBefore($value); - $this->operators->push(['type' => 'date_before_year', 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); - $this->collector->monthBefore($value); - $this->operators->push(['type' => 'date_before_month', 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); - $this->collector->dayBefore($value); - $this->operators->push(['type' => 'date_before_day', 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setDateAfterParams(array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setDateAfterParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setAfter($value); - $this->operators->push(['type' => 'date_after', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); - $this->collector->yearAfter($value); - $this->operators->push(['type' => 'date_after_year', 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); - $this->collector->monthAfter($value); - $this->operators->push(['type' => 'date_after_month', 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); - $this->collector->dayAfter($value); - $this->operators->push(['type' => 'date_after_day', 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setExactMetaDateParams(string $field, array $range, bool $prohibited = false): void - { - Log::debug('Now in setExactMetaDateParams()'); - - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setExactMetaDateParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->setMetaDateRange($value, $value, $field); - $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'exact_not': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->excludeMetaDateRange($value, $value, $field); - $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->metaYearIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'year_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->metaYearIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->metaMonthIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'month_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->metaMonthIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->metaDayIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); - } - - break; - - case 'day_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->metaDayIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setMetaDateBeforeParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateBeforeParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setMetaBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_before YEAR value "%s"', $field, $value)); - $this->collector->metaYearBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_before MONTH value "%s"', $field, $value)); - $this->collector->metaMonthBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_before DAY value "%s"', $field, $value)); - $this->collector->metaDayBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setMetaDateAfterParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateAfterParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setMetaAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_after YEAR value "%s"', $field, $value)); - $this->collector->metaYearAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_after MONTH value "%s"', $field, $value)); - $this->collector->metaMonthAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_after DAY value "%s"', $field, $value)); - $this->collector->metaDayAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setExactObjectDateParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setExactObjectDateParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->setObjectRange($value, clone $value, $field); - $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'exact_not': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->excludeObjectRange($value, clone $value, $field); - $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->objectYearIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'year_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->objectYearIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->objectMonthIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'month_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->objectMonthIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->objectDayIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); - } - - break; - - case 'day_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->objectDayIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setObjectDateBeforeParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateBeforeParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setObjectBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); - $this->collector->objectYearBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); - $this->collector->objectMonthBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); - $this->collector->objectDayBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setObjectDateAfterParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateAfterParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setObjectAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); - $this->collector->objectYearAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); - $this->collector->objectMonthAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); - $this->collector->objectDayAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); - } - - break; - } - } - } - - private function handleNodeGroup(NodeGroup $node, bool $flipProhibitedFlag): void - { - $prohibited = $node->isProhibited($flipProhibitedFlag); - - foreach ($node->getNodes() as $subNode) { - $this->handleSearchNode($subNode, $prohibited); - } - } - - public function searchTime(): float - { - return microtime(true) - $this->startTime; - } - - public function searchTransactions(): LengthAwarePaginator - { - $this->parseTagInstructions(); - if (0 === count($this->getWords()) && 0 === count($this->getExcludedWords()) && 0 === count($this->getOperators())) { - return new LengthAwarePaginator([], 0, 5, 1); - } - - return $this->collector->getPaginatedGroups(); - } - - private function parseTagInstructions(): void - { - Log::debug('Now in parseTagInstructions()'); - // if exclude tags, remove excluded tags. - if (count($this->excludeTags) > 0) { - Log::debug(sprintf('%d exclude tag(s)', count($this->excludeTags))); - $collection = new Collection(); - foreach ($this->excludeTags as $tagId) { - $tag = $this->tagRepository->find($tagId); - if (null !== $tag) { - Log::debug(sprintf('Exclude tag "%s"', $tag->tag)); - $collection->push($tag); - } - } - Log::debug(sprintf('Selecting all tags except %d excluded tag(s).', $collection->count())); - $this->collector->setWithoutSpecificTags($collection); - } - // if include tags, include them: - if (count($this->includeTags) > 0) { - Log::debug(sprintf('%d include tag(s)', count($this->includeTags))); - $collection = new Collection(); - foreach ($this->includeTags as $tagId) { - $tag = $this->tagRepository->find($tagId); - if (null !== $tag) { - Log::debug(sprintf('Include tag "%s"', $tag->tag)); - $collection->push($tag); - } - } - $this->collector->setAllTags($collection); - } - // if include ANY tags, include them: (see #8632) - if (count($this->includeAnyTags) > 0) { - Log::debug(sprintf('%d include ANY tag(s)', count($this->includeAnyTags))); - $collection = new Collection(); - foreach ($this->includeAnyTags as $tagId) { - $tag = $this->tagRepository->find($tagId); - if (null !== $tag) { - Log::debug(sprintf('Include ANY tag "%s"', $tag->tag)); - $collection->push($tag); - } - } - $this->collector->setTags($collection); - } - } - - public function getWords(): array - { - return $this->words; - } - - public function getExcludedWords(): array - { - return $this->prohibitedWords; - } - - public function setDate(Carbon $date): void - { - $this->date = $date; - } - - public function setPage(int $page): void - { - $this->page = $page; - $this->collector->setPage($this->page); - } - - public function setUser(User $user): void - { - $this->accountRepository->setUser($user); - $this->billRepository->setUser($user); - $this->categoryRepository->setUser($user); - $this->budgetRepository->setUser($user); - $this->tagRepository->setUser($user); - $this->collector = app(GroupCollectorInterface::class); - $this->collector->setUser($user); - $this->collector->withAccountInformation()->withCategoryInformation()->withBudgetInformation(); - - $this->setLimit((int) app('preferences')->getForUser($user, 'listPageSize', 50)->data); - } - - public function setLimit(int $limit): void - { - $this->limit = $limit; - $this->collector->setLimit($this->limit); - } } diff --git a/app/Support/Search/QueryParser/GdbotsQueryParser.php b/app/Support/Search/QueryParser/GdbotsQueryParser.php index a402013e48..0e670a21df 100644 --- a/app/Support/Search/QueryParser/GdbotsQueryParser.php +++ b/app/Support/Search/QueryParser/GdbotsQueryParser.php @@ -32,7 +32,6 @@ use Gdbots\QueryParser\QueryParser as BaseQueryParser; use Illuminate\Support\Facades\Log; use LogicException; use TypeError; - use function Safe\fwrite; class GdbotsQueryParser implements QueryParserInterface @@ -52,12 +51,12 @@ class GdbotsQueryParser implements QueryParserInterface try { $result = $this->parser->parse($query); $nodes = array_map( - fn (GdbotsNode\Node $node) => $this->convertNode($node), + fn(GdbotsNode\Node $node) => $this->convertNode($node), $result->getNodes() ); return new NodeGroup($nodes); - } catch (LogicException|TypeError $e) { + } catch (LogicException | TypeError $e) { fwrite(STDERR, "Setting up GdbotsQueryParserTest\n"); app('log')->error($e->getMessage()); app('log')->error(sprintf('Could not parse search: "%s".', $query)); @@ -76,7 +75,7 @@ class GdbotsQueryParser implements QueryParserInterface case $node instanceof GdbotsNode\Field: return new FieldNode( $node->getValue(), - (string) $node->getNode()->getValue(), + (string)$node->getNode()->getValue(), BoolOperator::PROHIBITED === $node->getBoolOperator() ); @@ -85,7 +84,7 @@ class GdbotsQueryParser implements QueryParserInterface return new NodeGroup( array_map( - fn (GdbotsNode\Node $subNode) => $this->convertNode($subNode), + fn(GdbotsNode\Node $subNode) => $this->convertNode($subNode), $node->getNodes() ) ); @@ -98,7 +97,7 @@ class GdbotsQueryParser implements QueryParserInterface case $node instanceof GdbotsNode\Mention: case $node instanceof GdbotsNode\Emoticon: case $node instanceof GdbotsNode\Emoji: - return new StringNode((string) $node->getValue(), BoolOperator::PROHIBITED === $node->getBoolOperator()); + return new StringNode((string)$node->getValue(), BoolOperator::PROHIBITED === $node->getBoolOperator()); default: throw new FireflyException( diff --git a/app/Support/Search/QueryParser/QueryParser.php b/app/Support/Search/QueryParser/QueryParser.php index c9072f970b..2533bcc3a8 100644 --- a/app/Support/Search/QueryParser/QueryParser.php +++ b/app/Support/Search/QueryParser/QueryParser.php @@ -46,22 +46,6 @@ class QueryParser implements QueryParserInterface return $this->buildNodeGroup(false); } - private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup - { - $nodes = []; - $nodeResult = $this->buildNextNode($isSubquery); - - while ($nodeResult->node instanceof Node) { - $nodes[] = $nodeResult->node; - if ($nodeResult->isSubqueryEnd) { - break; - } - $nodeResult = $this->buildNextNode($isSubquery); - } - - return new NodeGroup($nodes, $prohibited); - } - private function buildNextNode(bool $isSubquery): NodeResult { $tokenUnderConstruction = ''; @@ -155,7 +139,7 @@ class QueryParser implements QueryParserInterface if ('' === $tokenUnderConstruction) { // In any other location, it's just a normal character $tokenUnderConstruction .= $char; - $skipNext = true; + $skipNext = true; } if ('' !== $tokenUnderConstruction && !$skipNext) { // @phpstan-ignore-line Log::debug(sprintf('Turns out that "%s" is a field name. Reset the token.', $tokenUnderConstruction)); @@ -187,13 +171,29 @@ class QueryParser implements QueryParserInterface ++$this->position; } - $finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName + $finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName ? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited) : null; return new NodeResult($finalNode, true); } + private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup + { + $nodes = []; + $nodeResult = $this->buildNextNode($isSubquery); + + while ($nodeResult->node instanceof Node) { + $nodes[] = $nodeResult->node; + if ($nodeResult->isSubqueryEnd) { + break; + } + $nodeResult = $this->buildNextNode($isSubquery); + } + + return new NodeGroup($nodes, $prohibited); + } + private function createNode(string $token, string $fieldName, bool $prohibited): Node { if ('' !== $fieldName) { diff --git a/app/Support/Singleton/PreferencesSingleton.php b/app/Support/Singleton/PreferencesSingleton.php index 32b9bb94f6..e8ff779c5e 100644 --- a/app/Support/Singleton/PreferencesSingleton.php +++ b/app/Support/Singleton/PreferencesSingleton.php @@ -29,7 +29,7 @@ class PreferencesSingleton { private static ?PreferencesSingleton $instance = null; - private array $preferences = []; + private array $preferences = []; private function __construct() { @@ -45,6 +45,11 @@ class PreferencesSingleton return self::$instance; } + public function getPreference(string $key): mixed + { + return $this->preferences[$key] ?? null; + } + public function resetPreferences(): void { $this->preferences = []; @@ -54,9 +59,4 @@ class PreferencesSingleton { $this->preferences[$key] = $value; } - - public function getPreference(string $key): mixed - { - return $this->preferences[$key] ?? null; - } } diff --git a/app/Support/Steam.php b/app/Support/Steam.php index e103f19ece..c9a13be8b7 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -38,7 +38,6 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use ValueError; - use function Safe\parse_url; use function Safe\preg_replace; @@ -47,6 +46,81 @@ use function Safe\preg_replace; */ class Steam { + public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array + { + Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String())); + $result = []; + $convertToPrimary ??= Amount::convertToPrimary(); + $primary ??= Amount::getPrimaryCurrency(); + $currencies = $this->getCurrencies($accounts); + + // balance(s) in all currencies for ALL accounts. + $arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray()) + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->groupBy(['transactions.account_id', 'transaction_currencies.code']) + ->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray(); + + /** @var Account $account */ + foreach ($accounts as $account) { + // this array is PER account, so we wait a bit before we change code here. + $return = [ + 'pc_balance' => '0', + 'balance' => '0', // this key is overwritten right away, but I must remember it is always created. + ]; + $currency = $currencies[$account->id]; + + // second array + $accountSum = array_filter($arrayOfSums, fn($entry) => $entry['account_id'] === $account->id); + if (0 === count($accountSum)) { + $result[$account->id] = $return; + + continue; + } + $accountSum = array_values($accountSum)[0]; + $sumOfAmount = (string)$accountSum['sum_of_amount']; + $sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount); + $sumsByCode = [ + $accountSum['code'] => $sumOfAmount, + ]; + + // Log::debug('All balances are (joined)', $others); + // if there is no request to convert, take this as "balance" and "pc_balance". + $return['balance'] = $sumsByCode[$currency->code] ?? '0'; + if (!$convertToPrimary) { + unset($return['pc_balance']); + // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance'])); + } + // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency. + if ($convertToPrimary) { + $return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date); + // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance'])); + } + + // either way, the balance is always combined with the virtual balance: + $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance); + + if ($convertToPrimary) { + // the primary currency balance is combined with a converted virtual_balance: + $converter = new ExchangeRateConverter(); + $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance); + $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']); + // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance'])); + } + if (!$convertToPrimary) { + // if not, also increase the balance + primary balance for consistency. + $return['balance'] = bcadd($return['balance'], $virtualBalance); + // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance'])); + } + $final = array_merge($return, $sumsByCode); + $result[$account->id] = $final; + // Log::debug('Final balance is', $final); + } + + return $result; + } + /** * https://stackoverflow.com/questions/1642614/how-to-ceil-floor-and-round-bcmath-numbers */ @@ -66,27 +140,15 @@ class Steam // Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision)); if (str_contains($number, '.')) { if ('-' !== $number[0]) { - return bcadd($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcadd($number, '0.' . str_repeat('0', $precision) . '5', $precision); } - return bcsub($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcsub($number, '0.' . str_repeat('0', $precision) . '5', $precision); } return $number; } - public function filterAccountBalances(array $total, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array - { - Log::debug(sprintf('filterAccountBalances(#%d)', $account->id)); - $return = []; - foreach ($total as $key => $value) { - $return[$key] = $this->filterAccountBalance($value, $account, $convertToPrimary, $currency); - } - Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id)); - - return $return; - } - public function filterAccountBalance(array $set, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array { Log::debug(sprintf('filterAccountBalance(#%d)', $account->id), $set); @@ -138,6 +200,18 @@ class Steam return $set; } + public function filterAccountBalances(array $total, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array + { + Log::debug(sprintf('filterAccountBalances(#%d)', $account->id)); + $return = []; + foreach ($total as $key => $value) { + $return[$key] = $this->filterAccountBalance($value, $account, $convertToPrimary, $currency); + } + Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id)); + + return $return; + } + public function filterSpaces(string $string): string { $search = [ @@ -197,6 +271,94 @@ class Steam return str_replace($search, '', $string); } + /** + * Returns smaller than or equal to, so be careful with END OF DAY. + * + * Returns the balance of an account at exact moment given. Array with at least one value. + * Always returns: + * "balance": balance in the account's currency OR user's primary currency if the account has no currency + * "EUR": balance in EUR (or whatever currencies the account has balance in) + * + * If the user has $convertToPrimary: + * "balance": balance in the account's currency OR user's primary currency if the account has no currency + * --> "pc_balance": balance in the user's primary currency, with all amounts converted to the primary currency. + * "EUR": balance in EUR (or whatever currencies the account has balance in) + */ + public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array + { + + $cache = new CacheProperties(); + $cache->addProperty($account->id); + $cache->addProperty($date); + if ($cache->has()) { + Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s'))); + + // return $cache->get(); + } + // Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s'))); + if (null === $convertToPrimary) { + $convertToPrimary = Amount::convertToPrimary($account->user); + } + if (!$primary instanceof TransactionCurrency) { + $primary = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); + } + // account balance thing. + $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency']; + if ($currencyPresent) { + $accountCurrency = $account->meta['currency']; + } + if (!$currencyPresent) { + + $accountCurrency = $this->getAccountCurrency($account); + } + $hasCurrency = null !== $accountCurrency; + $currency = $hasCurrency ? $accountCurrency : $primary; + $return = [ + 'pc_balance' => '0', + 'balance' => '0', // this key is overwritten right away, but I must remember it is always created. + ]; + // balance(s) in all currencies. + $array = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->get(['transaction_currencies.code', 'transactions.amount'])->toArray(); + $others = $this->groupAndSumTransactions($array, 'code', 'amount'); + // Log::debug('All balances are (joined)', $others); + // if there is no request to convert, take this as "balance" and "pc_balance". + $return['balance'] = $others[$currency->code] ?? '0'; + if (!$convertToPrimary) { + unset($return['pc_balance']); + // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance'])); + } + // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency. + if ($convertToPrimary) { + $return['pc_balance'] = $this->convertAllBalances($others, $primary, $date); // todo sum all and convert. + // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance'])); + } + + // either way, the balance is always combined with the virtual balance: + $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance); + + if ($convertToPrimary) { + // the primary currency balance is combined with a converted virtual_balance: + $converter = new ExchangeRateConverter(); + $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance); + $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']); + // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance'])); + } + if (!$convertToPrimary) { + // if not, also increase the balance + primary balance for consistency. + $return['balance'] = bcadd($return['balance'], $virtualBalance); + // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance'])); + } + $final = array_merge($return, $others); + // Log::debug('Final balance is', $final); + $cache->store($final); + + return $final; + } + public function finalAccountBalanceInRange(Account $account, Carbon $start, Carbon $end, bool $convertToPrimary): array { // expand period. @@ -205,7 +367,7 @@ class Steam Log::debug(sprintf('finalAccountBalanceInRange(#%d, %s, %s)', $account->id, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); // set up cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('final-balance-in-range'); $cache->addProperty($start); @@ -215,22 +377,22 @@ class Steam return $cache->get(); } - $balances = []; - $formatted = $start->format('Y-m-d'); + $balances = []; + $formatted = $start->format('Y-m-d'); /* * To make sure the start balance is correct, we need to get the balance at the exact end of the previous day. * Since we just did "startOfDay" we can do subDay()->endOfDay() to get the correct moment. * THAT will be the start balance. */ - $request = clone $start; + $request = clone $start; $request->subDay()->endOfDay(); Log::debug('Get first balance to start.'); Log::debug(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String())); - $startBalance = $this->finalAccountBalance($account, $request); - $primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); - $accountCurrency = $this->getAccountCurrency($account); - $hasCurrency = $accountCurrency instanceof TransactionCurrency; - $currency = $accountCurrency ?? $primaryCurrency; + $startBalance = $this->finalAccountBalance($account, $request); + $primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); + $accountCurrency = $this->getAccountCurrency($account); + $hasCurrency = $accountCurrency instanceof TransactionCurrency; + $currency = $accountCurrency ?? $primaryCurrency; Log::debug(sprintf('Currency is %s', $currency->code)); @@ -243,7 +405,7 @@ class Steam Log::debug(sprintf('Also set start balance in %s', $primaryCurrency->code)); $startBalance[$primaryCurrency->code] ??= '0'; } - $currencies = [ + $currencies = [ $currency->id => $currency, $primaryCurrency->id => $primaryCurrency, ]; @@ -253,48 +415,47 @@ class Steam // sums up the balance changes per day. Log::debug(sprintf('Date >= %s and <= %s', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); - $set = $account->transactions() - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) - ->groupBy('transaction_journals.date') - ->groupBy('transactions.transaction_currency_id') - ->orderBy('transaction_journals.date', 'ASC') - ->whereNull('transaction_journals.deleted_at') - ->get( - [ // @phpstan-ignore-line - 'transaction_journals.date', - 'transactions.transaction_currency_id', - DB::raw('SUM(transactions.amount) AS sum_of_day'), - ] - ) - ; + $set = $account->transactions() + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) + ->groupBy('transaction_journals.date') + ->groupBy('transactions.transaction_currency_id') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') + ->get( + [ // @phpstan-ignore-line + 'transaction_journals.date', + 'transactions.transaction_currency_id', + DB::raw('SUM(transactions.amount) AS sum_of_day'), + ] + ); - $currentBalance = $startBalance; - $converter = new ExchangeRateConverter(); + $currentBalance = $startBalance; + $converter = new ExchangeRateConverter(); /** @var Transaction $entry */ foreach ($set as $entry) { // get date object - $carbon = new Carbon($entry->date, $entry->date_tz); - $carbonKey = $carbon->format('Y-m-d'); + $carbon = new Carbon($entry->date, $entry->date_tz); + $carbonKey = $carbon->format('Y-m-d'); // make sure sum is a string: - $sumOfDay = (string)($entry->sum_of_day ?? '0'); + $sumOfDay = (string)($entry->sum_of_day ?? '0'); // #10426 make sure sum is not in scientific notation. - $sumOfDay = $this->floatalize($sumOfDay); + $sumOfDay = $this->floatalize($sumOfDay); // find currency of this entry, does not have to exist. $currencies[$entry->transaction_currency_id] ??= Amount::getTransactionCurrencyById($entry->transaction_currency_id); // make sure this $entry has its own $entryCurrency /** @var TransactionCurrency $entryCurrency */ - $entryCurrency = $currencies[$entry->transaction_currency_id]; + $entryCurrency = $currencies[$entry->transaction_currency_id]; Log::debug(sprintf('Processing transaction(s) on moment %s', $carbon->format('Y-m-d H:i:s'))); // add amount to current balance in currency code. - $currentBalance[$entryCurrency->code] ??= '0'; + $currentBalance[$entryCurrency->code] ??= '0'; $currentBalance[$entryCurrency->code] = bcadd($sumOfDay, (string)$currentBalance[$entryCurrency->code]); // if not requested to convert to primary currency, add the amount to "balance", do nothing else. @@ -312,7 +473,7 @@ class Steam } } // add to final array. - $balances[$carbonKey] = $currentBalance; + $balances[$carbonKey] = $currentBalance; Log::debug(sprintf('Updated entry [%s]', $carbonKey), $currentBalance); } $cache->store($balances); @@ -321,175 +482,40 @@ class Steam return $balances; } - public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array - { - Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String())); - $result = []; - $convertToPrimary ??= Amount::convertToPrimary(); - $primary ??= Amount::getPrimaryCurrency(); - $currencies = $this->getCurrencies($accounts); - - // balance(s) in all currencies for ALL accounts. - $arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray()) - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) - ->groupBy(['transactions.account_id', 'transaction_currencies.code']) - ->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray() - ; - - /** @var Account $account */ - foreach ($accounts as $account) { - // this array is PER account, so we wait a bit before we change code here. - $return = [ - 'pc_balance' => '0', - 'balance' => '0', // this key is overwritten right away, but I must remember it is always created. - ]; - $currency = $currencies[$account->id]; - - // second array - $accountSum = array_filter($arrayOfSums, fn ($entry) => $entry['account_id'] === $account->id); - if (0 === count($accountSum)) { - $result[$account->id] = $return; - - continue; - } - $accountSum = array_values($accountSum)[0]; - $sumOfAmount = (string)$accountSum['sum_of_amount']; - $sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount); - $sumsByCode = [ - $accountSum['code'] => $sumOfAmount, - ]; - - // Log::debug('All balances are (joined)', $others); - // if there is no request to convert, take this as "balance" and "pc_balance". - $return['balance'] = $sumsByCode[$currency->code] ?? '0'; - if (!$convertToPrimary) { - unset($return['pc_balance']); - // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance'])); - } - // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency. - if ($convertToPrimary) { - $return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date); - // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance'])); - } - - // either way, the balance is always combined with the virtual balance: - $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance); - - if ($convertToPrimary) { - // the primary currency balance is combined with a converted virtual_balance: - $converter = new ExchangeRateConverter(); - $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance); - $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']); - // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance'])); - } - if (!$convertToPrimary) { - // if not, also increase the balance + primary balance for consistency. - $return['balance'] = bcadd($return['balance'], $virtualBalance); - // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance'])); - } - $final = array_merge($return, $sumsByCode); - $result[$account->id] = $final; - // Log::debug('Final balance is', $final); - } - - return $result; - } - /** - * Returns smaller than or equal to, so be careful with END OF DAY. + * https://framework.zend.com/downloads/archives * - * Returns the balance of an account at exact moment given. Array with at least one value. - * Always returns: - * "balance": balance in the account's currency OR user's primary currency if the account has no currency - * "EUR": balance in EUR (or whatever currencies the account has balance in) - * - * If the user has $convertToPrimary: - * "balance": balance in the account's currency OR user's primary currency if the account has no currency - * --> "pc_balance": balance in the user's primary currency, with all amounts converted to the primary currency. - * "EUR": balance in EUR (or whatever currencies the account has balance in) + * Convert a scientific notation to float + * Additionally fixed a problem with PHP <= 5.2.x with big integers */ - public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array + public function floatalize(string $value): string { + $value = strtoupper($value); + if (!str_contains($value, 'E')) { + return $value; + } + Log::debug(sprintf('Floatalizing %s', $value)); - $cache = new CacheProperties(); - $cache->addProperty($account->id); - $cache->addProperty($date); - if ($cache->has()) { - Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s'))); + $number = substr($value, 0, (int)strpos($value, 'E')); + if (str_contains($number, '.')) { + $post = strlen(substr($number, (int)strpos($number, '.') + 1)); + $mantis = substr($value, (int)strpos($value, 'E') + 1); + if ($mantis < 0) { + $post += abs((int)$mantis); + } - // return $cache->get(); - } - // Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s'))); - if (null === $convertToPrimary) { - $convertToPrimary = Amount::convertToPrimary($account->user); - } - if (!$primary instanceof TransactionCurrency) { - $primary = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); - } - // account balance thing. - $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency']; - if ($currencyPresent) { - $accountCurrency = $account->meta['currency']; - } - if (!$currencyPresent) { - - $accountCurrency = $this->getAccountCurrency($account); - } - $hasCurrency = null !== $accountCurrency; - $currency = $hasCurrency ? $accountCurrency : $primary; - $return = [ - 'pc_balance' => '0', - 'balance' => '0', // this key is overwritten right away, but I must remember it is always created. - ]; - // balance(s) in all currencies. - $array = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) - ->get(['transaction_currencies.code', 'transactions.amount'])->toArray() - ; - $others = $this->groupAndSumTransactions($array, 'code', 'amount'); - // Log::debug('All balances are (joined)', $others); - // if there is no request to convert, take this as "balance" and "pc_balance". - $return['balance'] = $others[$currency->code] ?? '0'; - if (!$convertToPrimary) { - unset($return['pc_balance']); - // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance'])); - } - // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency. - if ($convertToPrimary) { - $return['pc_balance'] = $this->convertAllBalances($others, $primary, $date); // todo sum all and convert. - // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance'])); + // TODO careless float could break financial math. + return number_format((float)$value, $post, '.', ''); } - // either way, the balance is always combined with the virtual balance: - $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance); - - if ($convertToPrimary) { - // the primary currency balance is combined with a converted virtual_balance: - $converter = new ExchangeRateConverter(); - $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance); - $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']); - // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance'])); - } - if (!$convertToPrimary) { - // if not, also increase the balance + primary balance for consistency. - $return['balance'] = bcadd($return['balance'], $virtualBalance); - // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance'])); - } - $final = array_merge($return, $others); - // Log::debug('Final balance is', $final); - $cache->store($final); - - return $final; + // TODO careless float could break financial math. + return number_format((float)$value, 0, '.', ''); } public function getAccountCurrency(Account $account): ?TransactionCurrency { - $type = $account->accountType->type; - $list = config('firefly.valid_currency_account_types'); + $type = $account->accountType->type; + $list = config('firefly.valid_currency_account_types'); // return null if not in this list. if (!in_array($type, $list, true)) { @@ -503,45 +529,6 @@ class Steam return Amount::getTransactionCurrencyById((int)$result->data); } - private function groupAndSumTransactions(array $array, string $group, string $field): array - { - $return = []; - - foreach ($array as $item) { - $groupKey = $item[$group] ?? 'unknown'; - $return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string)$item[$field]); - } - - return $return; - } - - private function convertAllBalances(array $others, TransactionCurrency $primary, Carbon $date): string - { - $total = '0'; - $converter = new ExchangeRateConverter(); - $singleton = PreferencesSingleton::getInstance(); - foreach ($others as $key => $amount) { - $preference = $singleton->getPreference($key); - - try { - $currency = $preference ?? Amount::getTransactionCurrencyByCode($key); - } catch (FireflyException) { - continue; - } - if (null === $preference) { - $singleton->setPreference($key, $currency); - } - $current = $amount; - if ($currency->id !== $primary->id) { - $current = $converter->convert($currency, $primary, $date, $amount); - Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current)); - } - $total = bcadd($current, $total); - } - - return $total; - } - /** * @throws FireflyException */ @@ -563,19 +550,34 @@ class Steam return (string)$host; } + /** + * Get user's language. + * + * @throws FireflyException + */ + public function getLanguage(): string // get preference + { + $preference = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data; + if (!is_string($preference)) { + throw new FireflyException(sprintf('Preference "language" must be a string, but is unexpectedly a "%s".', gettype($preference))); + } + + return str_replace('-', '_', $preference); + } + public function getLastActivities(array $accounts): array { $list = []; - $set = auth()->user()->transactions() - ->whereIn('transactions.account_id', $accounts) - ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) - ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line + $set = auth()->user()->transactions() + ->whereIn('transactions.account_id', $accounts) + ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) + ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line ; /** @var Transaction $entry */ foreach ($set as $entry) { - $date = new Carbon($entry->max_date, config('app.timezone')); + $date = new Carbon($entry->max_date, config('app.timezone')); $date->setTimezone(config('app.timezone')); $list[(int)$entry->account_id] = $date; } @@ -605,21 +607,6 @@ class Steam return $locale; } - /** - * Get user's language. - * - * @throws FireflyException - */ - public function getLanguage(): string // get preference - { - $preference = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data; - if (!is_string($preference)) { - throw new FireflyException(sprintf('Preference "language" must be a string, but is unexpectedly a "%s".', gettype($preference))); - } - - return str_replace('-', '_', $preference); - } - public function getLocaleArray(string $locale): array { return [ @@ -650,9 +637,9 @@ class Steam public function getSafeUrl(string $unknownUrl, string $safeUrl): string { // Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl)); - $returnUrl = $safeUrl; - $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); - $safeHost = parse_url($safeUrl, PHP_URL_HOST); + $returnUrl = $safeUrl; + $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); + $safeHost = parse_url($safeUrl, PHP_URL_HOST); if (null !== $unknownHost && $unknownHost === $safeHost) { $returnUrl = $unknownUrl; @@ -681,36 +668,6 @@ class Steam return $amount; } - /** - * https://framework.zend.com/downloads/archives - * - * Convert a scientific notation to float - * Additionally fixed a problem with PHP <= 5.2.x with big integers - */ - public function floatalize(string $value): string - { - $value = strtoupper($value); - if (!str_contains($value, 'E')) { - return $value; - } - Log::debug(sprintf('Floatalizing %s', $value)); - - $number = substr($value, 0, (int)strpos($value, 'E')); - if (str_contains($number, '.')) { - $post = strlen(substr($number, (int)strpos($number, '.') + 1)); - $mantis = substr($value, (int)strpos($value, 'E') + 1); - if ($mantis < 0) { - $post += abs((int)$mantis); - } - - // TODO careless float could break financial math. - return number_format((float)$value, $post, '.', ''); - } - - // TODO careless float could break financial math. - return number_format((float)$value, 0, '.', ''); - } - public function opposite(?string $amount = null): ?string { if (null === $amount) { @@ -768,6 +725,33 @@ class Steam return $amount; } + private function convertAllBalances(array $others, TransactionCurrency $primary, Carbon $date): string + { + $total = '0'; + $converter = new ExchangeRateConverter(); + $singleton = PreferencesSingleton::getInstance(); + foreach ($others as $key => $amount) { + $preference = $singleton->getPreference($key); + + try { + $currency = $preference ?? Amount::getTransactionCurrencyByCode($key); + } catch (FireflyException) { + continue; + } + if (null === $preference) { + $singleton->setPreference($key, $currency); + } + $current = $amount; + if ($currency->id !== $primary->id) { + $current = $converter->convert($currency, $primary, $date, $amount); + Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current)); + } + $total = bcadd($current, $total); + } + + return $total; + } + private function getCurrencies(Collection $accounts): array { $currencies = []; @@ -776,8 +760,8 @@ class Steam $primary = Amount::getPrimaryCurrency(); $currencies[$primary->id] = $primary; - $ids = $accounts->pluck('id')->toArray(); - $result = AccountMeta::whereIn('account_id', $ids)->where('name', 'currency_id')->get(); + $ids = $accounts->pluck('id')->toArray(); + $result = AccountMeta::whereIn('account_id', $ids)->where('name', 'currency_id')->get(); /** @var AccountMeta $item */ foreach ($result as $item) { @@ -787,7 +771,7 @@ class Steam } } // collect those currencies, skip primary because we already have it. - $set = TransactionCurrency::whereIn('id', $accountPreferences)->where('id', '!=', $primary->id)->get(); + $set = TransactionCurrency::whereIn('id', $accountPreferences)->where('id', '!=', $primary->id)->get(); foreach ($set as $item) { $currencies[$item->id] = $item; } @@ -798,7 +782,7 @@ class Steam $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency']; if ($currencyPresent) { $currencyId = $account->meta['currency']->id; - $currencies[$currencyId] ??= $account->meta['currency']; + $currencies[$currencyId] ??= $account->meta['currency']; $accountCurrencies[$accountId] = $account->meta['currency']; } if (!$currencyPresent && !array_key_exists($accountId, $accountPreferences)) { @@ -811,4 +795,16 @@ class Steam return $accountCurrencies; } + + private function groupAndSumTransactions(array $array, string $group, string $field): array + { + $return = []; + + foreach ($array as $item) { + $groupKey = $item[$group] ?? 'unknown'; + $return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string)$item[$field]); + } + + return $return; + } } diff --git a/app/Support/System/GeneratesInstallationId.php b/app/Support/System/GeneratesInstallationId.php index 20cd0a303c..732237214f 100644 --- a/app/Support/System/GeneratesInstallationId.php +++ b/app/Support/System/GeneratesInstallationId.php @@ -49,7 +49,7 @@ trait GeneratesInstallationId if (null === $config) { $uuid4 = Uuid::uuid4(); - $uniqueId = (string) $uuid4; + $uniqueId = (string)$uuid4; app('log')->info(sprintf('Created Firefly III installation ID %s', $uniqueId)); app('fireflyconfig')->set('installation_id', $uniqueId); } diff --git a/app/Support/System/OAuthKeys.php b/app/Support/System/OAuthKeys.php index 53e353481f..1c1f2276cf 100644 --- a/app/Support/System/OAuthKeys.php +++ b/app/Support/System/OAuthKeys.php @@ -31,7 +31,6 @@ use Illuminate\Support\Facades\Crypt; use Laravel\Passport\Console\KeysCommand; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; - use function Safe\file_get_contents; use function Safe\file_put_contents; @@ -43,6 +42,78 @@ class OAuthKeys private const string PRIVATE_KEY = 'oauth_private_key'; private const string PUBLIC_KEY = 'oauth_public_key'; + public static function generateKeys(): void + { + Artisan::registerCommand(new KeysCommand()); + Artisan::call('firefly-iii:laravel-passport-keys'); + } + + public static function hasKeyFiles(): bool + { + $private = storage_path('oauth-private.key'); + $public = storage_path('oauth-public.key'); + + return file_exists($private) && file_exists($public); + } + + public static function keysInDatabase(): bool + { + $privateKey = ''; + $publicKey = ''; + // better check if keys are in the database: + if (app('fireflyconfig')->has(self::PRIVATE_KEY) && app('fireflyconfig')->has(self::PUBLIC_KEY)) { + try { + $privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; + $publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; + } catch (ContainerExceptionInterface | FireflyException | NotFoundExceptionInterface $e) { + app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage())); + app('log')->error($e->getTraceAsString()); + } + } + if ('' !== $privateKey && '' !== $publicKey) { + return true; + } + + return false; + } + + /** + * @throws FireflyException + */ + public static function restoreKeysFromDB(): bool + { + $privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; + $publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; + + try { + $privateContent = Crypt::decrypt($privateKey); + $publicContent = Crypt::decrypt($publicKey); + } catch (DecryptException $e) { + app('log')->error('Could not decrypt pub/private keypair.'); + app('log')->error($e->getMessage()); + + // delete config vars from DB: + app('fireflyconfig')->delete(self::PRIVATE_KEY); + app('fireflyconfig')->delete(self::PUBLIC_KEY); + + return false; + } + $private = storage_path('oauth-private.key'); + $public = storage_path('oauth-public.key'); + file_put_contents($private, $privateContent); + file_put_contents($public, $publicContent); + + return true; + } + + public static function storeKeysInDB(): void + { + $private = storage_path('oauth-private.key'); + $public = storage_path('oauth-public.key'); + app('fireflyconfig')->set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private))); + app('fireflyconfig')->set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public))); + } + public static function verifyKeysRoutine(): void { if (!self::keysInDatabase() && !self::hasKeyFiles()) { @@ -60,76 +131,4 @@ class OAuthKeys self::storeKeysInDB(); } } - - public static function keysInDatabase(): bool - { - $privateKey = ''; - $publicKey = ''; - // better check if keys are in the database: - if (app('fireflyconfig')->has(self::PRIVATE_KEY) && app('fireflyconfig')->has(self::PUBLIC_KEY)) { - try { - $privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; - $publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; - } catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) { - app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage())); - app('log')->error($e->getTraceAsString()); - } - } - if ('' !== $privateKey && '' !== $publicKey) { - return true; - } - - return false; - } - - public static function hasKeyFiles(): bool - { - $private = storage_path('oauth-private.key'); - $public = storage_path('oauth-public.key'); - - return file_exists($private) && file_exists($public); - } - - public static function generateKeys(): void - { - Artisan::registerCommand(new KeysCommand()); - Artisan::call('firefly-iii:laravel-passport-keys'); - } - - public static function storeKeysInDB(): void - { - $private = storage_path('oauth-private.key'); - $public = storage_path('oauth-public.key'); - app('fireflyconfig')->set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private))); - app('fireflyconfig')->set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public))); - } - - /** - * @throws FireflyException - */ - public static function restoreKeysFromDB(): bool - { - $privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; - $publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; - - try { - $privateContent = Crypt::decrypt($privateKey); - $publicContent = Crypt::decrypt($publicKey); - } catch (DecryptException $e) { - app('log')->error('Could not decrypt pub/private keypair.'); - app('log')->error($e->getMessage()); - - // delete config vars from DB: - app('fireflyconfig')->delete(self::PRIVATE_KEY); - app('fireflyconfig')->delete(self::PUBLIC_KEY); - - return false; - } - $private = storage_path('oauth-private.key'); - $public = storage_path('oauth-public.key'); - file_put_contents($private, $privateContent); - file_put_contents($public, $publicContent); - - return true; - } } diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index 39cc1594db..49c9a6d11e 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -29,10 +29,10 @@ use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Facades\Amount; use Illuminate\Support\Facades\Log; +use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; -use Override; /** * Contains all amount formatting routines. @@ -48,6 +48,17 @@ class AmountFormat extends AbstractExtension ]; } + #[Override] + public function getFunctions(): array + { + return [ + $this->formatAmountByAccount(), + $this->formatAmountBySymbol(), + $this->formatAmountByCurrency(), + $this->formatAmountByCode(), + ]; + } + protected function formatAmount(): TwigFilter { return new TwigFilter( @@ -61,30 +72,6 @@ class AmountFormat extends AbstractExtension ); } - protected function formatAmountPlain(): TwigFilter - { - return new TwigFilter( - 'formatAmountPlain', - static function (string $string): string { - $currency = Amount::getPrimaryCurrency(); - - return Amount::formatAnything($currency, $string, false); - }, - ['is_safe' => ['html']] - ); - } - - #[Override] - public function getFunctions(): array - { - return [ - $this->formatAmountByAccount(), - $this->formatAmountBySymbol(), - $this->formatAmountByCurrency(), - $this->formatAmountByCode(), - ]; - } - /** * Will format the amount by the currency related to the given account. * @@ -107,50 +94,6 @@ class AmountFormat extends AbstractExtension ); } - /** - * Will format the amount by the currency related to the given account. - */ - protected function formatAmountBySymbol(): TwigFunction - { - return new TwigFunction( - 'formatAmountBySymbol', - static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string { - - if (null === $symbol) { - $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); - Log::error($message); - $currency = Amount::getPrimaryCurrency(); - } - if (null !== $symbol) { - $decimalPlaces ??= 2; - $coloured ??= true; - $currency = new TransactionCurrency(); - $currency->symbol = $symbol; - $currency->decimal_places = $decimalPlaces; - } - - return Amount::formatAnything($currency, $amount, $coloured); - }, - ['is_safe' => ['html']] - ); - } - - /** - * Will format the amount by the currency related to the given account. - */ - protected function formatAmountByCurrency(): TwigFunction - { - return new TwigFunction( - 'formatAmountByCurrency', - static function (TransactionCurrency $currency, string $amount, ?bool $coloured = null): string { - $coloured ??= true; - - return Amount::formatAnything($currency, $amount, $coloured); - }, - ['is_safe' => ['html']] - ); - } - /** * Use the code to format a currency. */ @@ -175,4 +118,61 @@ class AmountFormat extends AbstractExtension ['is_safe' => ['html']] ); } + + /** + * Will format the amount by the currency related to the given account. + */ + protected function formatAmountByCurrency(): TwigFunction + { + return new TwigFunction( + 'formatAmountByCurrency', + static function (TransactionCurrency $currency, string $amount, ?bool $coloured = null): string { + $coloured ??= true; + + return Amount::formatAnything($currency, $amount, $coloured); + }, + ['is_safe' => ['html']] + ); + } + + /** + * Will format the amount by the currency related to the given account. + */ + protected function formatAmountBySymbol(): TwigFunction + { + return new TwigFunction( + 'formatAmountBySymbol', + static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string { + + if (null === $symbol) { + $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); + Log::error($message); + $currency = Amount::getPrimaryCurrency(); + } + if (null !== $symbol) { + $decimalPlaces ??= 2; + $coloured ??= true; + $currency = new TransactionCurrency(); + $currency->symbol = $symbol; + $currency->decimal_places = $decimalPlaces; + } + + return Amount::formatAnything($currency, $amount, $coloured); + }, + ['is_safe' => ['html']] + ); + } + + protected function formatAmountPlain(): TwigFilter + { + return new TwigFilter( + 'formatAmountPlain', + static function (string $string): string { + $currency = Amount::getPrimaryCurrency(); + + return Amount::formatAnything($currency, $string, false); + }, + ['is_safe' => ['html']] + ); + } } diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 6f71d6f578..337e832312 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -37,7 +37,6 @@ use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; - use function Safe\parse_url; /** @@ -57,144 +56,6 @@ class General extends AbstractExtension ]; } - /** - * Show account balance. Only used on the front page of Firefly III. - */ - protected function balance(): TwigFilter - { - return new TwigFilter( - 'balance', - static function (?Account $account): string { - if (!$account instanceof Account) { - return '0'; - } - - /** @var Carbon $date */ - $date = session('end', today(config('app.timezone'))->endOfMonth()); - Log::debug(sprintf('twig balance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); - $info = Steam::finalAccountBalance($account, $date); - $currency = Steam::getAccountCurrency($account); - $primary = Amount::getPrimaryCurrency(); - $convertToPrimary = Amount::convertToPrimary(); - $usePrimary = $convertToPrimary && $primary->id !== $currency->id; - $currency ??= $primary; - $strings = []; - foreach ($info as $key => $balance) { - if ('balance' === $key) { - // balance in account currency. - if (!$usePrimary) { - $strings[] = app('amount')->formatAnything($currency, $balance, false); - } - - continue; - } - if ('pc_balance' === $key) { - // balance in primary currency. - if ($usePrimary) { - $strings[] = app('amount')->formatAnything($primary, $balance, false); - } - - continue; - } - // for multi currency accounts. - if ($usePrimary && $key !== $primary->code) { - $strings[] = app('amount')->formatAnything(Amount::getTransactionCurrencyByCode($key), $balance, false); - } - } - - return implode(', ', $strings); - // return app('steam')->balance($account, $date); - } - ); - } - - /** - * Used to convert 1024 to 1kb etc. - */ - protected function formatFilesize(): TwigFilter - { - return new TwigFilter( - 'filesize', - static function (int $size): string { - // less than one GB, more than one MB - if ($size < (1024 * 1024 * 2014) && $size >= (1024 * 1024)) { - return round($size / (1024 * 1024), 2).' MB'; - } - - // less than one MB - if ($size < (1024 * 1024)) { - return round($size / 1024, 2).' KB'; - } - - return $size.' bytes'; - } - ); - } - - /** - * Show icon with attachment. - * - * @SuppressWarnings("PHPMD.CyclomaticComplexity") - */ - protected function mimeIcon(): TwigFilter - { - return new TwigFilter( - 'mimeIcon', - static fn (string $string): string => match ($string) { - 'application/pdf' => 'fa-file-pdf-o', - 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/heic', 'image/heic-sequence', 'application/vnd.oasis.opendocument.image' => 'fa-file-image-o', - 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'application/x-iwork-pages-sffpages', 'application/vnd.sun.xml.writer', 'application/vnd.sun.xml.writer.template', 'application/vnd.sun.xml.writer.global', 'application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text-template', 'application/vnd.oasis.opendocument.text-web', 'application/vnd.oasis.opendocument.text-master' => 'fa-file-word-o', - 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'application/vnd.sun.xml.calc', 'application/vnd.sun.xml.calc.template', 'application/vnd.stardivision.calc', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet-template' => 'fa-file-excel-o', - 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.presentationml.template', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'application/vnd.sun.xml.impress', 'application/vnd.sun.xml.impress.template', 'application/vnd.stardivision.impress', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.presentation-template' => 'fa-file-powerpoint-o', - 'application/vnd.sun.xml.draw', 'application/vnd.sun.xml.draw.template', 'application/vnd.stardivision.draw', 'application/vnd.oasis.opendocument.chart' => 'fa-paint-brush', - 'application/vnd.oasis.opendocument.graphics', 'application/vnd.oasis.opendocument.graphics-template', 'application/vnd.sun.xml.math', 'application/vnd.stardivision.math', 'application/vnd.oasis.opendocument.formula', 'application/vnd.oasis.opendocument.database' => 'fa-calculator', - default => 'fa-file-o', - }, - ['is_safe' => ['html']] - ); - } - - protected function markdown(): TwigFilter - { - return new TwigFilter( - 'markdown', - static function (string $text): string { - $converter = new GithubFlavoredMarkdownConverter( - [ - 'allow_unsafe_links' => false, - 'max_nesting_level' => 5, - 'html_input' => 'escape', - ] - ); - - return (string)$converter->convert($text); - }, - ['is_safe' => ['html']] - ); - } - - /** - * Show URL host name - */ - protected function phpHostName(): TwigFilter - { - return new TwigFilter( - 'phphost', - static function (string $string): string { - $proto = parse_url($string, PHP_URL_SCHEME); - $host = parse_url($string, PHP_URL_HOST); - if (is_array($host)) { - $host = implode(' ', $host); - } - if (is_array($proto)) { - $proto = implode(' ', $proto); - } - - return e(sprintf('%s://%s', $proto, $host)); - } - ); - } - #[Override] public function getFunctions(): array { @@ -212,38 +73,6 @@ class General extends AbstractExtension ]; } - /** - * Basic example thing for some views. - */ - protected function phpdate(): TwigFunction - { - return new TwigFunction( - 'phpdate', - static fn (string $str): string => date($str) - ); - } - - /** - * Will return "active" when the current route matches the given argument - * exactly. - */ - protected function activeRouteStrict(): TwigFunction - { - return new TwigFunction( - 'activeRouteStrict', - static function (): string { - $args = func_get_args(); - $route = $args[0]; // name of the route. - - if (\Route::getCurrentRoute()->getName() === $route) { - return 'active'; - } - - return ''; - } - ); - } - /** * Will return "active" when a part of the route matches the argument. * ie. "accounts" will match "accounts.index". @@ -275,7 +104,7 @@ class General extends AbstractExtension 'activeRoutePartialObjectType', static function ($context): string { [, $route, $objectType] = func_get_args(); - $activeObjectType = $context['objectType'] ?? false; + $activeObjectType = $context['objectType'] ?? false; if ($objectType === $activeObjectType && false !== stripos( @@ -291,6 +120,193 @@ class General extends AbstractExtension ); } + /** + * Will return "active" when the current route matches the given argument + * exactly. + */ + protected function activeRouteStrict(): TwigFunction + { + return new TwigFunction( + 'activeRouteStrict', + static function (): string { + $args = func_get_args(); + $route = $args[0]; // name of the route. + + if (\Route::getCurrentRoute()->getName() === $route) { + return 'active'; + } + + return ''; + } + ); + } + + /** + * Show account balance. Only used on the front page of Firefly III. + */ + protected function balance(): TwigFilter + { + return new TwigFilter( + 'balance', + static function (?Account $account): string { + if (!$account instanceof Account) { + return '0'; + } + + /** @var Carbon $date */ + $date = session('end', today(config('app.timezone'))->endOfMonth()); + Log::debug(sprintf('twig balance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); + $info = Steam::finalAccountBalance($account, $date); + $currency = Steam::getAccountCurrency($account); + $primary = Amount::getPrimaryCurrency(); + $convertToPrimary = Amount::convertToPrimary(); + $usePrimary = $convertToPrimary && $primary->id !== $currency->id; + $currency ??= $primary; + $strings = []; + foreach ($info as $key => $balance) { + if ('balance' === $key) { + // balance in account currency. + if (!$usePrimary) { + $strings[] = app('amount')->formatAnything($currency, $balance, false); + } + + continue; + } + if ('pc_balance' === $key) { + // balance in primary currency. + if ($usePrimary) { + $strings[] = app('amount')->formatAnything($primary, $balance, false); + } + + continue; + } + // for multi currency accounts. + if ($usePrimary && $key !== $primary->code) { + $strings[] = app('amount')->formatAnything(Amount::getTransactionCurrencyByCode($key), $balance, false); + } + } + + return implode(', ', $strings); + // return app('steam')->balance($account, $date); + } + ); + } + + protected function carbonize(): TwigFunction + { + return new TwigFunction( + 'carbonize', + static fn(string $date): Carbon => new Carbon($date, config('app.timezone')) + ); + } + + /** + * Formats a string as a thing by converting it to a Carbon first. + */ + protected function formatDate(): TwigFunction + { + return new TwigFunction( + 'formatDate', + static function (string $date, string $format): string { + $carbon = new Carbon($date); + + return $carbon->isoFormat($format); + } + ); + } + + /** + * Used to convert 1024 to 1kb etc. + */ + protected function formatFilesize(): TwigFilter + { + return new TwigFilter( + 'filesize', + static function (int $size): string { + // less than one GB, more than one MB + if ($size < (1024 * 1024 * 2014) && $size >= (1024 * 1024)) { + return round($size / (1024 * 1024), 2) . ' MB'; + } + + // less than one MB + if ($size < (1024 * 1024)) { + return round($size / 1024, 2) . ' KB'; + } + + return $size . ' bytes'; + } + ); + } + + /** + * TODO Remove me when v2 hits. + */ + protected function getMetaField(): TwigFunction + { + return new TwigFunction( + 'accountGetMetaField', + static function (Account $account, string $field): string { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $result = $repository->getMetaValue($account, $field); + if (null === $result) { + return ''; + } + + return $result; + } + ); + } + + protected function getRootSearchOperator(): TwigFunction + { + return new TwigFunction( + 'getRootSearchOperator', + static function (string $operator): string { + $result = OperatorQuerySearch::getRootOperator($operator); + + return str_replace('-', 'not_', $result); + } + ); + } + + /** + * Will return true if the user is of role X. + */ + protected function hasRole(): TwigFunction + { + return new TwigFunction( + 'hasRole', + static function (string $role): bool { + $repository = app(UserRepositoryInterface::class); + if ($repository->hasRole(auth()->user(), $role)) { + return true; + } + + return false; + } + ); + } + + protected function markdown(): TwigFilter + { + return new TwigFilter( + 'markdown', + static function (string $text): string { + $converter = new GithubFlavoredMarkdownConverter( + [ + 'allow_unsafe_links' => false, + 'max_nesting_level' => 5, + 'html_input' => 'escape', + ] + ); + + return (string)$converter->convert($text); + }, + ['is_safe' => ['html']] + ); + } + /** * Will return "menu-open" when a part of the route matches the argument. * ie. "accounts" will match "accounts.index". @@ -313,75 +329,58 @@ class General extends AbstractExtension } /** - * Formats a string as a thing by converting it to a Carbon first. + * Show icon with attachment. + * + * @SuppressWarnings("PHPMD.CyclomaticComplexity") */ - protected function formatDate(): TwigFunction + protected function mimeIcon(): TwigFilter { - return new TwigFunction( - 'formatDate', - static function (string $date, string $format): string { - $carbon = new Carbon($date); + return new TwigFilter( + 'mimeIcon', + static fn(string $string): string => match ($string) { + 'application/pdf' => 'fa-file-pdf-o', + 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/heic', 'image/heic-sequence', 'application/vnd.oasis.opendocument.image' => 'fa-file-image-o', + 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'application/x-iwork-pages-sffpages', 'application/vnd.sun.xml.writer', 'application/vnd.sun.xml.writer.template', 'application/vnd.sun.xml.writer.global', 'application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text-template', 'application/vnd.oasis.opendocument.text-web', 'application/vnd.oasis.opendocument.text-master' => 'fa-file-word-o', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'application/vnd.sun.xml.calc', 'application/vnd.sun.xml.calc.template', 'application/vnd.stardivision.calc', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet-template' => 'fa-file-excel-o', + 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.presentationml.template', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'application/vnd.sun.xml.impress', 'application/vnd.sun.xml.impress.template', 'application/vnd.stardivision.impress', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.presentation-template' => 'fa-file-powerpoint-o', + 'application/vnd.sun.xml.draw', 'application/vnd.sun.xml.draw.template', 'application/vnd.stardivision.draw', 'application/vnd.oasis.opendocument.chart' => 'fa-paint-brush', + 'application/vnd.oasis.opendocument.graphics', 'application/vnd.oasis.opendocument.graphics-template', 'application/vnd.sun.xml.math', 'application/vnd.stardivision.math', 'application/vnd.oasis.opendocument.formula', 'application/vnd.oasis.opendocument.database' => 'fa-calculator', + default => 'fa-file-o', + }, + ['is_safe' => ['html']] + ); + } - return $carbon->isoFormat($format); + /** + * Show URL host name + */ + protected function phpHostName(): TwigFilter + { + return new TwigFilter( + 'phphost', + static function (string $string): string { + $proto = parse_url($string, PHP_URL_SCHEME); + $host = parse_url($string, PHP_URL_HOST); + if (is_array($host)) { + $host = implode(' ', $host); + } + if (is_array($proto)) { + $proto = implode(' ', $proto); + } + + return e(sprintf('%s://%s', $proto, $host)); } ); } /** - * TODO Remove me when v2 hits. + * Basic example thing for some views. */ - protected function getMetaField(): TwigFunction + protected function phpdate(): TwigFunction { return new TwigFunction( - 'accountGetMetaField', - static function (Account $account, string $field): string { - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $result = $repository->getMetaValue($account, $field); - if (null === $result) { - return ''; - } - - return $result; - } - ); - } - - /** - * Will return true if the user is of role X. - */ - protected function hasRole(): TwigFunction - { - return new TwigFunction( - 'hasRole', - static function (string $role): bool { - $repository = app(UserRepositoryInterface::class); - if ($repository->hasRole(auth()->user(), $role)) { - return true; - } - - return false; - } - ); - } - - protected function getRootSearchOperator(): TwigFunction - { - return new TwigFunction( - 'getRootSearchOperator', - static function (string $operator): string { - $result = OperatorQuerySearch::getRootOperator($operator); - - return str_replace('-', 'not_', $result); - } - ); - } - - protected function carbonize(): TwigFunction - { - return new TwigFunction( - 'carbonize', - static fn (string $date): Carbon => new Carbon($date, config('app.timezone')) + 'phpdate', + static fn(string $str): string => date($str) ); } } diff --git a/app/Support/Twig/Rule.php b/app/Support/Twig/Rule.php index c833745d40..7ed872df8b 100644 --- a/app/Support/Twig/Rule.php +++ b/app/Support/Twig/Rule.php @@ -23,34 +23,43 @@ declare(strict_types=1); namespace FireflyIII\Support\Twig; -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; use Config; use Override; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; /** * Class Rule. */ class Rule extends AbstractExtension { - #[Override] - public function getFunctions(): array + public function allActionTriggers(): TwigFunction { - return [ - $this->allJournalTriggers(), - $this->allRuleTriggers(), - $this->allActionTriggers(), - ]; + return new TwigFunction( + 'allRuleActions', + static function () { + // array of valid values for actions + $ruleActions = array_keys(Config::get('firefly.rule-actions')); + $possibleActions = []; + foreach ($ruleActions as $key) { + $possibleActions[$key] = (string)trans('firefly.rule_action_' . $key . '_choice'); + } + unset($ruleActions); + asort($possibleActions); + + return $possibleActions; + } + ); } public function allJournalTriggers(): TwigFunction { return new TwigFunction( 'allJournalTriggers', - static fn () => [ - 'store-journal' => (string) trans('firefly.rule_trigger_store_journal'), - 'update-journal' => (string) trans('firefly.rule_trigger_update_journal'), - 'manual-activation' => (string) trans('firefly.rule_trigger_manual'), + static fn() => [ + 'store-journal' => (string)trans('firefly.rule_trigger_store_journal'), + 'update-journal' => (string)trans('firefly.rule_trigger_update_journal'), + 'manual-activation' => (string)trans('firefly.rule_trigger_manual'), ] ); } @@ -64,7 +73,7 @@ class Rule extends AbstractExtension $possibleTriggers = []; foreach ($ruleTriggers as $key) { if ('user_action' !== $key) { - $possibleTriggers[$key] = (string) trans('firefly.rule_trigger_'.$key.'_choice'); + $possibleTriggers[$key] = (string)trans('firefly.rule_trigger_' . $key . '_choice'); } } unset($ruleTriggers); @@ -75,22 +84,13 @@ class Rule extends AbstractExtension ); } - public function allActionTriggers(): TwigFunction + #[Override] + public function getFunctions(): array { - return new TwigFunction( - 'allRuleActions', - static function () { - // array of valid values for actions - $ruleActions = array_keys(Config::get('firefly.rule-actions')); - $possibleActions = []; - foreach ($ruleActions as $key) { - $possibleActions[$key] = (string) trans('firefly.rule_action_'.$key.'_choice'); - } - unset($ruleActions); - asort($possibleActions); - - return $possibleActions; - } - ); + return [ + $this->allJournalTriggers(), + $this->allRuleTriggers(), + $this->allActionTriggers(), + ]; } } diff --git a/app/Support/Twig/TransactionGroupTwig.php b/app/Support/Twig/TransactionGroupTwig.php index 81cf8db231..e2957ec3a1 100644 --- a/app/Support/Twig/TransactionGroupTwig.php +++ b/app/Support/Twig/TransactionGroupTwig.php @@ -31,10 +31,9 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournalMeta; use Illuminate\Support\Facades\DB; +use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -use Override; - use function Safe\json_decode; /** @@ -76,6 +75,141 @@ class TransactionGroupTwig extends AbstractExtension ); } + public function journalGetMetaDate(): TwigFunction + { + return new TwigFunction( + 'journalGetMetaDate', + static function (int $journalId, string $metaField) { + /** @var null|TransactionJournalMeta $entry */ + $entry = DB::table('journal_meta') + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->first(); + if (null === $entry) { + return today(config('app.timezone')); + } + + return new Carbon(json_decode((string)$entry->data, false)); + } + ); + } + + public function journalGetMetaField(): TwigFunction + { + return new TwigFunction( + 'journalGetMetaField', + static function (int $journalId, string $metaField) { + /** @var null|TransactionJournalMeta $entry */ + $entry = DB::table('journal_meta') + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->first(); + if (null === $entry) { + return ''; + } + + return json_decode((string)$entry->data, true); + } + ); + } + + public function journalHasMeta(): TwigFunction + { + return new TwigFunction( + 'journalHasMeta', + static function (int $journalId, string $metaField) { + $count = DB::table('journal_meta') + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->count(); + + return 1 === $count; + } + ); + } + + /** + * Shows the amount for a single journal object. + */ + public function journalObjectAmount(): TwigFunction + { + return new TwigFunction( + 'journalObjectAmount', + function (TransactionJournal $journal): string { + $result = $this->normalJournalObjectAmount($journal); + // now append foreign amount, if any. + if ($this->journalObjectHasForeign($journal)) { + $foreign = $this->foreignJournalObjectAmount($journal); + $result = sprintf('%s (%s)', $result, $foreign); + } + + return $result; + }, + ['is_safe' => ['html']] + ); + } + + /** + * Generate foreign amount for transaction from a transaction group. + */ + private function foreignJournalArrayAmount(array $array): string + { + $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value; + $amount = $array['foreign_amount'] ?? '0'; + $colored = true; + + $sourceType = $array['source_account_type'] ?? 'invalid'; + $amount = $this->signAmount($amount, $type, $sourceType); + + if (TransactionTypeEnum::TRANSFER->value === $type) { + $colored = false; + } + $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int)$array['foreign_currency_decimal_places'], $amount, $colored); + if (TransactionTypeEnum::TRANSFER->value === $type) { + return sprintf('%s', $result); + } + + return $result; + } + + /** + * Generate foreign amount for journal from a transaction group. + */ + private function foreignJournalObjectAmount(TransactionJournal $journal): string + { + $type = $journal->transactionType->type; + + /** @var Transaction $first */ + $first = $journal->transactions()->where('amount', '<', 0)->first(); + $currency = $first->foreignCurrency; + $amount = '' === $first->foreign_amount ? '0' : $first->foreign_amount; + $colored = true; + $sourceType = $first->account->accountType()->first()->type; + + $amount = $this->signAmount($amount, $type, $sourceType); + + if (TransactionTypeEnum::TRANSFER->value === $type) { + $colored = false; + } + $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); + if (TransactionTypeEnum::TRANSFER->value === $type) { + return sprintf('%s', $result); + } + + return $result; + } + + private function journalObjectHasForeign(TransactionJournal $journal): bool + { + /** @var Transaction $first */ + $first = $journal->transactions()->where('amount', '<', 0)->first(); + + return '' !== $first->foreign_amount; + } + /** * Generate normal amount for transaction from a transaction group. */ @@ -91,7 +225,34 @@ class TransactionGroupTwig extends AbstractExtension $colored = false; } - $result = app('amount')->formatFlat($array['currency_symbol'], (int) $array['currency_decimal_places'], $amount, $colored); + $result = app('amount')->formatFlat($array['currency_symbol'], (int)$array['currency_decimal_places'], $amount, $colored); + if (TransactionTypeEnum::TRANSFER->value === $type) { + return sprintf('%s', $result); + } + + return $result; + } + + /** + * Generate normal amount for transaction from a transaction group. + */ + private function normalJournalObjectAmount(TransactionJournal $journal): string + { + $type = $journal->transactionType->type; + + /** @var Transaction $first */ + $first = $journal->transactions()->where('amount', '<', 0)->first(); + $currency = $journal->transactionCurrency; + $amount = $first->amount ?? '0'; + $colored = true; + $sourceType = $first->account->accountType()->first()->type; + + $amount = $this->signAmount($amount, $type, $sourceType); + + if (TransactionTypeEnum::TRANSFER->value === $type) { + $colored = false; + } + $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); if (TransactionTypeEnum::TRANSFER->value === $type) { return sprintf('%s', $result); } @@ -118,169 +279,4 @@ class TransactionGroupTwig extends AbstractExtension return $amount; } - - /** - * Generate foreign amount for transaction from a transaction group. - */ - private function foreignJournalArrayAmount(array $array): string - { - $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value; - $amount = $array['foreign_amount'] ?? '0'; - $colored = true; - - $sourceType = $array['source_account_type'] ?? 'invalid'; - $amount = $this->signAmount($amount, $type, $sourceType); - - if (TransactionTypeEnum::TRANSFER->value === $type) { - $colored = false; - } - $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int) $array['foreign_currency_decimal_places'], $amount, $colored); - if (TransactionTypeEnum::TRANSFER->value === $type) { - return sprintf('%s', $result); - } - - return $result; - } - - /** - * Shows the amount for a single journal object. - */ - public function journalObjectAmount(): TwigFunction - { - return new TwigFunction( - 'journalObjectAmount', - function (TransactionJournal $journal): string { - $result = $this->normalJournalObjectAmount($journal); - // now append foreign amount, if any. - if ($this->journalObjectHasForeign($journal)) { - $foreign = $this->foreignJournalObjectAmount($journal); - $result = sprintf('%s (%s)', $result, $foreign); - } - - return $result; - }, - ['is_safe' => ['html']] - ); - } - - /** - * Generate normal amount for transaction from a transaction group. - */ - private function normalJournalObjectAmount(TransactionJournal $journal): string - { - $type = $journal->transactionType->type; - - /** @var Transaction $first */ - $first = $journal->transactions()->where('amount', '<', 0)->first(); - $currency = $journal->transactionCurrency; - $amount = $first->amount ?? '0'; - $colored = true; - $sourceType = $first->account->accountType()->first()->type; - - $amount = $this->signAmount($amount, $type, $sourceType); - - if (TransactionTypeEnum::TRANSFER->value === $type) { - $colored = false; - } - $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); - if (TransactionTypeEnum::TRANSFER->value === $type) { - return sprintf('%s', $result); - } - - return $result; - } - - private function journalObjectHasForeign(TransactionJournal $journal): bool - { - /** @var Transaction $first */ - $first = $journal->transactions()->where('amount', '<', 0)->first(); - - return '' !== $first->foreign_amount; - } - - /** - * Generate foreign amount for journal from a transaction group. - */ - private function foreignJournalObjectAmount(TransactionJournal $journal): string - { - $type = $journal->transactionType->type; - - /** @var Transaction $first */ - $first = $journal->transactions()->where('amount', '<', 0)->first(); - $currency = $first->foreignCurrency; - $amount = '' === $first->foreign_amount ? '0' : $first->foreign_amount; - $colored = true; - $sourceType = $first->account->accountType()->first()->type; - - $amount = $this->signAmount($amount, $type, $sourceType); - - if (TransactionTypeEnum::TRANSFER->value === $type) { - $colored = false; - } - $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); - if (TransactionTypeEnum::TRANSFER->value === $type) { - return sprintf('%s', $result); - } - - return $result; - } - - public function journalHasMeta(): TwigFunction - { - return new TwigFunction( - 'journalHasMeta', - static function (int $journalId, string $metaField) { - $count = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->count() - ; - - return 1 === $count; - } - ); - } - - public function journalGetMetaDate(): TwigFunction - { - return new TwigFunction( - 'journalGetMetaDate', - static function (int $journalId, string $metaField) { - /** @var null|TransactionJournalMeta $entry */ - $entry = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->first() - ; - if (null === $entry) { - return today(config('app.timezone')); - } - - return new Carbon(json_decode((string) $entry->data, false)); - } - ); - } - - public function journalGetMetaField(): TwigFunction - { - return new TwigFunction( - 'journalGetMetaField', - static function (int $journalId, string $metaField) { - /** @var null|TransactionJournalMeta $entry */ - $entry = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->first() - ; - if (null === $entry) { - return ''; - } - - return json_decode((string) $entry->data, true); - } - ); - } } diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php index bb19890ff9..e4f429f07e 100644 --- a/app/Support/Twig/Translation.php +++ b/app/Support/Twig/Translation.php @@ -23,10 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Support\Twig; +use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; -use Override; /** * Class Budget. @@ -39,7 +39,7 @@ class Translation extends AbstractExtension return [ new TwigFilter( '_', - static fn ($name) => (string) trans(sprintf('firefly.%s', $name)), + static fn($name) => (string)trans(sprintf('firefly.%s', $name)), ['is_safe' => ['html']] ), ];