Optimize queries for statistics.

This commit is contained in:
James Cole
2025-09-26 06:05:37 +02:00
parent 08879d31ba
commit 4ec2fcdb8a
92 changed files with 6499 additions and 6514 deletions

View File

@@ -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);

View File

@@ -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,
];
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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('<span class="text-success money-positive">%s</span>', $result);
}
if (-1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-danger money-negative">%s</span>', $result);
}
return sprintf('<span class="money-neutral">%s</span>', $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']
@@ -384,4 +110,278 @@ class Amount
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('<span class="text-success money-positive">%s</span>', $result);
}
if (-1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-danger money-negative">%s</span>', $result);
}
return sprintf('<span class="money-neutral">%s</span>', $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;
}
}

View File

@@ -102,13 +102,6 @@ class RemoteUserGuard implements Guard
$this->user = $retrievedUser;
}
public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
return !$this->check();
}
public function check(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
@@ -116,17 +109,11 @@ class RemoteUserGuard implements Guard
return $this->user() instanceof User;
}
public function user(): ?User
public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
$user = $this->user;
if (!$user instanceof User) {
Log::debug('User is NULL');
return null;
}
return $user;
return !$this->check();
}
public function hasUser(): bool
@@ -157,6 +144,19 @@ class RemoteUserGuard implements Guard
Log::error(sprintf('Did not set user at %s', __METHOD__));
}
public function user(): ?User
{
Log::debug(sprintf('Now at %s', __METHOD__));
$user = $this->user;
if (!$user instanceof User) {
Log::debug('User is NULL');
return null;
}
return $user;
}
/**
* @throws FireflyException
*

View File

@@ -54,8 +54,7 @@ class Balance
->orderBy('transaction_journals.order', 'asc')
->orderBy('transaction_journals.description', 'desc')
->orderBy('transactions.amount', 'desc')
->where('transaction_journals.date', '<=', $date)
;
->where('transaction_journals.date', '<=', $date);
$result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']);
foreach ($result as $entry) {

View File

@@ -46,8 +46,7 @@ class AccountList implements BinderInterface
->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.*'])
;
->get(['accounts.*']);
}
if ('allAssetAccounts' !== $value) {
$incoming = array_map('\intval', explode(',', $value));
@@ -58,8 +57,7 @@ class AccountList implements BinderInterface
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('accounts.id', $list)
->orderBy('accounts.name', 'ASC')
->get(['accounts.*'])
;
->get(['accounts.*']);
}
if ($collection->count() > 0) {

View File

@@ -43,8 +43,7 @@ class BudgetList implements BinderInterface
return auth()->user()->budgets()->where('active', true)
->orderBy('order', 'ASC')
->orderBy('name', 'ASC')
->get()
;
->get();
}
$list = array_unique(array_map('\intval', explode(',', $value)));
@@ -59,8 +58,7 @@ class BudgetList implements BinderInterface
$collection = auth()->user()->budgets()
->where('active', true)
->whereIn('id', $list)
->get()
;
->get();
// add empty budget if applicable.
if (in_array(0, $list, true)) {

View File

@@ -42,8 +42,7 @@ class CategoryList implements BinderInterface
if ('allCategories' === $value) {
return auth()->user()->categories()
->orderBy('name', 'ASC')
->get()
;
->get();
}
$list = array_unique(array_map('\intval', explode(',', $value)));
@@ -54,8 +53,7 @@ class CategoryList implements BinderInterface
/** @var Collection $collection */
$collection = auth()->user()->categories()
->whereIn('id', $list)
->get()
;
->get();
// add empty category if applicable.
if (in_array(0, $list, true)) {

View File

@@ -44,8 +44,7 @@ class TagList implements BinderInterface
if ('allTags' === $value) {
return auth()->user()->tags()
->orderBy('tag', 'ASC')
->get()
;
->get();
}
$list = array_unique(array_map('\strtolower', explode(',', $value)));
app('log')->debug('List of tags is', $list);

View File

@@ -43,8 +43,7 @@ class UserGroupAccount implements BinderInterface
$user = auth()->user();
$account = Account::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
->first();
if (null !== $account) {
return $account;
}

View File

@@ -43,8 +43,7 @@ class UserGroupBill implements BinderInterface
$user = auth()->user();
$currency = Bill::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
->first();
if (null !== $currency) {
return $currency;
}

View File

@@ -40,8 +40,7 @@ class UserGroupExchangeRate implements BinderInterface
$user = auth()->user();
$rate = CurrencyExchangeRate::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
->first();
if (null !== $rate) {
return $rate;
}

View File

@@ -40,8 +40,7 @@ class UserGroupTransaction implements BinderInterface
$user = auth()->user();
$group = TransactionGroup::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
->first();
if (null !== $group) {
return $group;
}

View File

@@ -27,7 +27,6 @@ use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use JsonException;
use function Safe\json_encode;
/**
@@ -78,6 +77,14 @@ class CacheProperties
return Cache::has($this->hash);
}
/**
* @param mixed $data
*/
public function store($data): void
{
Cache::forever($this->hash, $data);
}
private function hash(): void
{
$content = '';
@@ -91,12 +98,4 @@ class CacheProperties
}
$this->hash = substr(hash('sha256', $content), 0, 16);
}
/**
* @param mixed $data
*/
public function store($data): void
{
Cache::forever($this->hash, $data);
}
}

View File

@@ -37,27 +37,6 @@ class Calculator
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;

View File

@@ -84,28 +84,43 @@ class FrontpageChartGenerator
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.
* If limits are present, each limit is processed individually.
*/
private function processBudget(array $data, Budget $budget): array
public function setEnd(Carbon $end): void
{
Log::debug(sprintf('Now processing budget #%d ("%s")', $budget->id, $budget->name));
// get all limits:
$limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end);
Log::debug(sprintf('Found %d limit(s) for budget #%d.', $limits->count(), $budget->id));
// if no limits
if (0 === $limits->count()) {
$result = $this->noBudgetLimits($data, $budget);
Log::debug(sprintf('Now DONE processing budget #%d ("%s")', $budget->id, $budget->name));
return $result;
$this->end = $end;
}
$result = $this->budgetLimits($data, $budget, $limits);
Log::debug(sprintf('Now DONE processing budget #%d ("%s")', $budget->id, $budget->name));
return $result;
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;
}
/**
@@ -128,19 +143,27 @@ class FrontpageChartGenerator
}
/**
* If a budget has budget limit, each limit is processed individually.
* 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.
* If limits are present, each limit is processed individually.
*/
private function budgetLimits(array $data, Budget $budget, Collection $limits): array
private function processBudget(array $data, Budget $budget): array
{
Log::debug('Start processing budget limits.');
Log::debug(sprintf('Now processing budget #%d ("%s")', $budget->id, $budget->name));
// get all limits:
$limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end);
Log::debug(sprintf('Found %d limit(s) for budget #%d.', $limits->count(), $budget->id));
// if no limits
if (0 === $limits->count()) {
$result = $this->noBudgetLimits($data, $budget);
Log::debug(sprintf('Now DONE processing budget #%d ("%s")', $budget->id, $budget->name));
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$data = $this->processLimit($data, $budget, $limit);
return $result;
}
Log::debug('Done processing budget limits.');
$result = $this->budgetLimits($data, $budget, $limits);
Log::debug(sprintf('Now DONE processing budget #%d ("%s")', $budget->id, $budget->name));
return $data;
return $result;
}
/**
@@ -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);
}
}

View File

@@ -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;
@@ -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;
}
}

View File

@@ -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()
{

View File

@@ -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;

View File

@@ -141,6 +141,92 @@ class ExportDataGenerator
return $return;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function get(string $key, mixed $default = null): mixed
{
return null;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function has(mixed $key): mixed
{
return null;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setExportAccounts(bool $exportAccounts): void
{
$this->exportAccounts = $exportAccounts;
}
public function setExportBills(bool $exportBills): void
{
$this->exportBills = $exportBills;
}
public function setExportBudgets(bool $exportBudgets): void
{
$this->exportBudgets = $exportBudgets;
}
public function setExportCategories(bool $exportCategories): void
{
$this->exportCategories = $exportCategories;
}
public function setExportPiggies(bool $exportPiggies): void
{
$this->exportPiggies = $exportPiggies;
}
public function setExportRecurring(bool $exportRecurring): void
{
$this->exportRecurring = $exportRecurring;
}
public function setExportRules(bool $exportRules): void
{
$this->exportRules = $exportRules;
}
public function setExportTags(bool $exportTags): void
{
$this->exportTags = $exportTags;
}
public function setExportTransactions(bool $exportTransactions): void
{
$this->exportTransactions = $exportTransactions;
}
public function setStart(Carbon $start): void
{
$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
@@ -222,11 +308,6 @@ class ExportDataGenerator
return $string;
}
public function setUser(User $user): void
{
$this->user = $user;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -588,14 +669,6 @@ class ExportDataGenerator
return $string;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function get(string $key, mixed $default = null): mixed
{
return null;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -828,11 +901,6 @@ class ExportDataGenerator
return $string;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
private function mergeTags(array $tags): string
{
if (0 === count($tags)) {
@@ -845,72 +913,4 @@ class ExportDataGenerator
return implode(',', $smol);
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function has(mixed $key): mixed
{
return null;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setExportAccounts(bool $exportAccounts): void
{
$this->exportAccounts = $exportAccounts;
}
public function setExportBills(bool $exportBills): void
{
$this->exportBills = $exportBills;
}
public function setExportBudgets(bool $exportBudgets): void
{
$this->exportBudgets = $exportBudgets;
}
public function setExportCategories(bool $exportCategories): void
{
$this->exportCategories = $exportCategories;
}
public function setExportPiggies(bool $exportPiggies): void
{
$this->exportPiggies = $exportPiggies;
}
public function setExportRecurring(bool $exportRecurring): void
{
$this->exportRecurring = $exportRecurring;
}
public function setExportRules(bool $exportRules): void
{
$this->exportRules = $exportRules;
}
public function setExportTags(bool $exportTags): void
{
$this->exportTags = $exportTags;
}
public function setExportTransactions(bool $exportTransactions): void
{
$this->exportTransactions = $exportTransactions;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
}

View File

@@ -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.
@@ -46,34 +46,6 @@ class FireflyConfig
Configuration::where('name', $name)->forceDelete();
}
public function has(string $name): bool
{
return 1 === Configuration::where('name', $name)->count();
}
public function getEncrypted(string $name, mixed $default = null): ?Configuration
{
$result = $this->get($name, $default);
if (!$result instanceof Configuration) {
return null;
}
if ('' === $result->data) {
Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
return $result;
}
try {
$result->data = decrypt($result->data);
} catch (DecryptException $e) {
Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
return $result;
}
return $result;
}
/**
* @param null|bool|int|string $default
*
@@ -106,6 +78,56 @@ class FireflyConfig
return $this->set($name, $default);
}
public function getEncrypted(string $name, mixed $default = null): ?Configuration
{
$result = $this->get($name, $default);
if (!$result instanceof Configuration) {
return null;
}
if ('' === $result->data) {
Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
return $result;
}
try {
$result->data = decrypt($result->data);
} catch (DecryptException $e) {
Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
return $result;
}
return $result;
}
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);
}
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 {
@@ -135,28 +157,6 @@ class FireflyConfig
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 {

View File

@@ -57,37 +57,6 @@ 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;
}
/**
* Grouped dropdown list of all accounts that are valid as the destination of a withdrawal.
*/
@@ -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;
}
}

View File

@@ -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
*
@@ -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 TransactionCurrency $currency */
foreach ($list as $currency) {
$array[$currency->id] = $currency->name.' ('.$currency->symbol.')';
/** @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;
return $this->select($name, $array, $value, $options);
}
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
/**
* 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);
// 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));
// 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);
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;
}
}

View File

@@ -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;
}
/**
@@ -80,19 +91,6 @@ trait FormSupport
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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
// 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;
$this->ignoreSettings = $ignoreSettings;
}
// 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;
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
// 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;
public function summarize(): void
{
if (false === $this->enabled()) {
return;
}
// 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);
// 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';
}
/**
* @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);
$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 $euro->id;
}
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;
}
}

View File

@@ -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,6 +407,189 @@ 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 = [];
@@ -203,6 +610,40 @@ 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 = [];
@@ -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;
}
}

View File

@@ -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.
*/

View File

@@ -53,29 +53,29 @@ use Override;
*/
class AccountEnrichment implements EnrichmentInterface
{
private array $ids = [];
private array $accountTypeIds = [];
private array $accountTypes = [];
private array $balances = [];
private Collection $collection;
private readonly bool $convertToPrimary;
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 readonly TransactionCurrency $primaryCurrency;
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;
private array $lastActivities = [];
private ?Carbon $date = null;
private ?Carbon $start = null;
private ?Carbon $end = null;
private readonly bool $convertToPrimary;
private array $balances = [];
private array $startBalances = [];
private array $endBalances = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $sort = [];
/**
* 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();
}
private function collectMetaData(): void
return $this->date;
}
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,6 +160,11 @@ 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) {
@@ -357,11 +285,6 @@ class AccountEnrichment implements EnrichmentInterface
});
}
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,13 +294,82 @@ 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'])
;
->get(['object_groupable_id', 'object_group_id']);
$ids = array_unique($set->pluck('object_group_id')->toArray());
@@ -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();
return $this->date;
/** @var AccountType $type */
foreach ($types as $type) {
$this->accountTypes[(int)$type->id] = $type->type;
}
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
@@ -437,11 +437,6 @@ class AccountEnrichment implements EnrichmentInterface
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', []);

View File

@@ -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()
{
@@ -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;
}
}
}

View File

@@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log;
class BudgetEnrichment implements EnrichmentInterface
{
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;
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 = [];
public function __construct() {}
@@ -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,28 +100,6 @@ 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) {
@@ -177,14 +165,25 @@ 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
@@ -192,8 +191,7 @@ class BudgetEnrichment implements EnrichmentInterface
$set = DB::table('object_groupables')
->whereIn('object_groupable_id', $this->ids)
->where('object_groupable_type', Budget::class)
->get(['object_groupable_id', 'object_group_id'])
;
->get(['object_groupable_id', 'object_group_id']);
$ids = array_unique($set->pluck('object_group_id')->toArray());

View File

@@ -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()
{
@@ -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) {
@@ -179,6 +149,43 @@ class BudgetLimitEnrichment implements EnrichmentInterface
}
}
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 filterToBudget(array $expenses, int $budget): array
{
$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) {
@@ -193,12 +200,4 @@ class BudgetLimitEnrichment implements EnrichmentInterface
return $second;
}, $first), $this->expenses);
}
private function filterToBudget(array $expenses, int $budget): array
{
$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;
}
}

View File

@@ -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
{
@@ -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,14 +111,13 @@ class CategoryEnrichment implements EnrichmentInterface
});
}
public function setEnd(?Carbon $end): void
private function collectIds(): void
{
$this->end = $end;
/** @var Category $category */
foreach ($this->collection as $category) {
$this->ids[] = (int)$category->id;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
$this->ids = array_unique($this->ids);
}
private function collectNotes(): void
@@ -125,8 +125,7 @@ class CategoryEnrichment implements EnrichmentInterface
$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()
;
->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'];
}

View File

@@ -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 $accountCurrencies = [];
private array $notes = [];
private array $mappedObjects = [];
private readonly TransactionCurrency $primaryCurrency;
private array $accountIds = []; // @phpstan-ignore-line
private array $accounts = []; // @phpstan-ignore-line
private array $amounts = [];
private array $accounts = [];
private Collection $collection;
private array $currencies = [];
private array $currencyIds = [];
private array $ids = [];
// private array $accountCurrencies = [];
private array $mappedObjects = [];
private array $notes = [];
private array $objectGroups = [];
private readonly TransactionCurrency $primaryCurrency;
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -97,69 +97,6 @@ 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) {
@@ -229,13 +166,77 @@ class PiggyBankEnrichment implements EnrichmentInterface
});
}
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()
;
->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'];
}
@@ -247,8 +248,7 @@ class PiggyBankEnrichment implements EnrichmentInterface
$set = DB::table('object_groupables')
->whereIn('object_groupable_id', $this->ids)
->where('object_groupable_type', PiggyBank::class)
->get(['object_groupable_id', 'object_group_id'])
;
->get(['object_groupable_id', 'object_group_id']);
$ids = array_unique($set->pluck('object_group_id')->toArray());
@@ -264,8 +264,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
}
}
private function collectCurrentAmounts(): void {}
/**
* Returns the suggested amount the user should save per month, or "".
*/

View File

@@ -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;
@@ -86,6 +86,30 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
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;
if (array_key_exists($piggyId, $this->accountIds)) {
$accountId = $this->accountIds[$piggyId];
if (array_key_exists($accountId, $this->accountCurrencies)) {
$currency = $this->accountCurrencies[$accountId];
}
}
$meta = [
'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
'currency' => $currency,
];
$item->meta = $meta;
return $item;
});
}
private function collectIds(): void
{
/** @var PiggyBankEvent $event */
@@ -125,28 +149,4 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$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;
if (array_key_exists($piggyId, $this->accountIds)) {
$accountId = $this->accountIds[$piggyId];
if (array_key_exists($accountId, $this->accountCurrencies)) {
$currency = $this->accountCurrencies[$accountId];
}
}
$meta = [
'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
'currency' => $currency,
];
$item->meta = $meta;
return $item;
});
}
}

View File

@@ -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 array $accounts = [];
private Collection $collection;
private array $ids = [];
// private array $transactionTypeIds = [];
// private array $transactionTypes = [];
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 array $recurrenceIds = [];
private array $repetitions = [];
private array $sourceAccountIds = [];
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 readonly TransactionCurrency $primaryCurrency;
private bool $convertToPrimary = false;
public function __construct()
{
@@ -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.
@@ -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,6 +196,179 @@ 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);
@@ -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;
/** @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';
}
$ids = Arr::pluck($budgetIds, 'budget_id');
$categories = Budget::whereIn('id', $ids)->get();
$mapped = [];
foreach ($categories as $category) {
$mapped[(int)$category->id] = $category;
$language = (string)$language;
$this->language = $language;
}
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 processTransactions(array $transactions): array
{
$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']);
}
}
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'];
$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;
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
unset($transaction['transaction_currency_id']);
$return[] = $transaction;
}
return $return;
}
}

View File

@@ -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 $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()
{
@@ -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'];
$this->end = $end;
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
@@ -175,13 +172,39 @@ 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
@@ -189,8 +212,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
$set = DB::table('object_groupables')
->whereIn('object_groupable_id', $this->subscriptionIds)
->where('object_groupable_type', Bill::class)
->get(['object_groupable_id', 'object_group_id'])
;
->get(['object_groupable_id', 'object_group_id']);
$ids = array_unique($set->pluck('object_group_id')->toArray());
@@ -258,8 +280,7 @@ 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.
@@ -329,63 +350,6 @@ class SubscriptionEnrichment implements EnrichmentInterface
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
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;
}
$latest = $filtered->first()->date;
/** @var TransactionJournal $journal */
foreach ($filtered as $journal) {
if ($journal->date->gte($latest)) {
$latest = $journal->date;
}
}
return $latest;
}
private function getLastPaidDate(array $paidData): ?Carbon
{
// Log::debug('getLastPaidDate()');
$return = null;
foreach ($paidData as $entry) {
if (null !== $return) {
/** @var Carbon $current */
$current = $entry['date_object'];
if ($current->gt($return)) {
$return = clone $current;
}
Log::debug(sprintf('[a] Last paid date is: %s', $return->format('Y-m-d')));
}
if (null === $return) {
/** @var Carbon $return */
$return = $entry['date_object'];
Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
}
}
// Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
return $return;
}
private function collectPayDates(): void
{
if (!$this->start instanceof Carbon || !$this->end instanceof Carbon) {
@@ -411,6 +375,15 @@ class SubscriptionEnrichment implements EnrichmentInterface
}
}
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) {
@@ -420,6 +393,30 @@ class SubscriptionEnrichment implements EnrichmentInterface
}, $entries);
}
private function getLastPaidDate(array $paidData): ?Carbon
{
// Log::debug('getLastPaidDate()');
$return = null;
foreach ($paidData as $entry) {
if (null !== $return) {
/** @var Carbon $current */
$current = $entry['date_object'];
if ($current->gt($return)) {
$return = clone $current;
}
Log::debug(sprintf('[a] Last paid date is: %s', $return->format('Y-m-d')));
}
if (null === $return) {
/** @var Carbon $return */
$return = $entry['date_object'];
Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
}
}
// Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
return $return;
}
private function getNextExpectedMatch(array $payDates): ?Carbon
{
// next expected match

View File

@@ -52,9 +52,9 @@ class TransactionGroupEnrichment implements EnrichmentInterface
private array $locations = [];
private array $metaData = [];
private array $notes = [];
private array $tags = [];
private User $user; // @phpstan-ignore-line
private readonly TransactionCurrency $primaryCurrency;
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,93 +82,29 @@ 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'];
}
}
$this->journalIds = array_unique($this->journalIds);
Log::debug(__METHOD__);
if (is_array($model)) {
$collection = new Collection()->push($model);
$collection = $this->enrich($collection);
return $collection->first();
}
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)));
throw new FireflyException('Cannot enrich single model.');
}
private function collectTags(): void
public function setUser(User $user): 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'];
}
$this->user = $user;
$this->userGroup = $user->userGroup;
}
private function collectMetaData(): void
public function setUserGroup(UserGroup $userGroup): 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
@@ -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'];
}
}
}

View File

@@ -43,13 +43,12 @@ 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 $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 = [];
@@ -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;
});
}
}

View File

@@ -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');
@@ -79,8 +119,7 @@ class AccountBalanceCalculator
->orderBy('transaction_journals.order', 'desc')
->orderBy('transaction_journals.id', 'asc')
->orderBy('transaction_journals.description', 'asc')
->orderBy('transactions.amount', 'asc')
;
->orderBy('transactions.amount', 'asc');
if ($accounts->count() > 0) {
$query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray());
}
@@ -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);
}
}

View File

@@ -88,24 +88,6 @@ 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) {
@@ -159,180 +141,6 @@ class Navigation
return $periods;
}
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;
}
public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
{
$currentEnd = clone $end;
// Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
$functionMap = [
'1D' => 'endOfDay',
'daily' => 'endOfDay',
'1W' => 'addWeek',
'week' => 'addWeek',
'weekly' => 'addWeek',
'1M' => 'addMonth',
'month' => 'addMonth',
'monthly' => 'addMonth',
'3M' => 'addQuarter',
'quarter' => 'addQuarter',
'quarterly' => 'addQuarter',
'6M' => 'addMonths',
'half-year' => 'addMonths',
'half_year' => 'addMonths',
'year' => 'addYear',
'yearly' => 'addYear',
'1Y' => 'addYear',
];
$modifierMap = ['half-year' => 6, 'half_year' => 6, '6M' => 6];
$subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'half_year', '1Y', 'year', 'yearly'];
if ('custom' === $repeatFreq) {
// if the repeat frequency is "custom", use the current session start/end to see how large the range is,
// and use that to "add" another period.
// if there is no session data available use "30 days" as a default.
$diffInDays = 30;
if (null !== session('start') && null !== session('end')) {
Log::debug('Session data available.');
/** @var Carbon $tStart */
$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);
}
Log::debug(sprintf('Diff in days is %d', $diffInDays));
$currentEnd->addDays($diffInDays);
return $currentEnd;
}
if ('MTD' === $repeatFreq) {
$today = today();
if ($today->isSameMonth($end)) {
return $today->endOfDay();
}
return $end->endOfMonth();
}
$result = match ($repeatFreq) {
'last7' => $currentEnd->addDays(7)->startOfDay(),
'last30' => $currentEnd->addDays(30)->startOfDay(),
'last90' => $currentEnd->addDays(90)->startOfDay(),
'last365' => $currentEnd->addDays(365)->startOfDay(),
'MTD' => $currentEnd->startOfMonth()->startOfDay(),
'QTD' => $currentEnd->firstOfQuarter()->startOfDay(),
'YTD' => $currentEnd->startOfYear()->startOfDay(),
default => null,
};
if (null !== $result) {
return $result;
}
unset($result);
if (!array_key_exists($repeatFreq, $functionMap)) {
Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));
return $end;
}
$function = $functionMap[$repeatFreq];
if (array_key_exists($repeatFreq, $modifierMap)) {
$currentEnd->{$function}($modifierMap[$repeatFreq]); // @phpstan-ignore-line
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
$currentEnd->endOfDay();
return $currentEnd;
}
$currentEnd->{$function}(); // @phpstan-ignore-line
$currentEnd->endOfDay();
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
// Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String()));
return $currentEnd;
}
public function daysUntilEndOfMonth(Carbon $date): int
{
$endOfMonth = $date->copy()->endOfMonth();
@@ -395,6 +203,103 @@ class Navigation
return (int)$diff;
}
public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
{
$currentEnd = clone $end;
// Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
$functionMap = [
'1D' => 'endOfDay',
'daily' => 'endOfDay',
'1W' => 'addWeek',
'week' => 'addWeek',
'weekly' => 'addWeek',
'1M' => 'addMonth',
'month' => 'addMonth',
'monthly' => 'addMonth',
'3M' => 'addQuarter',
'quarter' => 'addQuarter',
'quarterly' => 'addQuarter',
'6M' => 'addMonths',
'half-year' => 'addMonths',
'half_year' => 'addMonths',
'year' => 'addYear',
'yearly' => 'addYear',
'1Y' => 'addYear',
];
$modifierMap = ['half-year' => 6, 'half_year' => 6, '6M' => 6];
$subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'half_year', '1Y', 'year', 'yearly'];
if ('custom' === $repeatFreq) {
// if the repeat frequency is "custom", use the current session start/end to see how large the range is,
// and use that to "add" another period.
// if there is no session data available use "30 days" as a default.
$diffInDays = 30;
if (null !== session('start') && null !== session('end')) {
Log::debug('Session data available.');
/** @var Carbon $tStart */
$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);
}
Log::debug(sprintf('Diff in days is %d', $diffInDays));
$currentEnd->addDays($diffInDays);
return $currentEnd;
}
if ('MTD' === $repeatFreq) {
$today = today();
if ($today->isSameMonth($end)) {
return $today->endOfDay()->milli(0);
}
return $end->endOfMonth();
}
$result = match ($repeatFreq) {
'last7' => $currentEnd->addDays(7)->startOfDay(),
'last30' => $currentEnd->addDays(30)->startOfDay(),
'last90' => $currentEnd->addDays(90)->startOfDay(),
'last365' => $currentEnd->addDays(365)->startOfDay(),
'MTD' => $currentEnd->startOfMonth()->startOfDay(),
'QTD' => $currentEnd->firstOfQuarter()->startOfDay(),
'YTD' => $currentEnd->startOfYear()->startOfDay(),
default => null,
};
if (null !== $result) {
return $result;
}
unset($result);
if (!array_key_exists($repeatFreq, $functionMap)) {
Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));
return $end;
}
$function = $functionMap[$repeatFreq];
if (array_key_exists($repeatFreq, $modifierMap)) {
$currentEnd->{$function}($modifierMap[$repeatFreq])->milli(0); // @phpstan-ignore-line
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
$currentEnd->endOfDay()->milli(0);
return $currentEnd;
}
$currentEnd->{$function}(); // @phpstan-ignore-line
$currentEnd->endOfDay()->milli(0);
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
// Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String()));
return $currentEnd;
}
public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon
{
$functionMap = [
@@ -486,26 +391,22 @@ class Navigation
return $entries;
}
/**
* 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".
*/
public function preferredCarbonFormat(Carbon $start, Carbon $end): string
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
{
$format = 'Y-m-d';
$diff = $start->diffInMonths($end, true);
// Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
if ($diff >= 1.001 && $diff < 12.001) {
// Log::debug(sprintf('Return Y-m because %s', $diff));
$format = 'Y-m';
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]);
}
if ($diff >= 12.001) {
// Log::debug(sprintf('Return Y because %s', $diff));
return 'Y';
}
Log::debug(
'Any error occurred to calculate the next date.',
['date' => $epoch, 'periodicity' => $periodicity->name, 'skipInterval' => $skipInterval]
);
return $format;
return $epoch;
}
public function periodShow(Carbon $theDate, string $repeatFrequency): string
@@ -538,10 +439,33 @@ class Navigation
// 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".
*/
public function preferredCarbonFormat(Carbon $start, Carbon $end): string
{
$format = 'Y-m-d';
$diff = $start->diffInMonths($end, true);
// Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
if ($diff >= 1.001 && $diff < 12.001) {
// Log::debug(sprintf('Return Y-m because %s', $diff));
$format = 'Y-m';
}
if ($diff >= 12.001) {
// Log::debug(sprintf('Return Y because %s', $diff));
return 'Y';
}
return $format;
}
/**
* Same as preferredCarbonFormat but by string
*/
@@ -631,6 +555,83 @@ 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
*/

View File

@@ -39,6 +39,92 @@ 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));
@@ -144,90 +230,4 @@ trait RecalculatesAvailableBudgetsTrait
$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;
}
}

View File

@@ -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;
/**
@@ -121,92 +120,6 @@ class ParseDateString
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],
];
}
}

View File

@@ -53,8 +53,40 @@ class Preferences
$q->whereNull('user_group_id');
$q->orWhere('user_group_id', $user->user_group_id);
})
->get()
;
->get();
}
public function beginsWith(User $user, string $search): Collection
{
$value = sprintf('%s%%', $search);
return Preference::where('user_id', $user->id)->whereLike('name', $value)->get();
}
public function delete(string $name): bool
{
$fullName = sprintf('preference%s%s', auth()->user()->id, $name);
if (Cache::has($fullName)) {
Cache::forget($fullName);
}
Preference::where('user_id', auth()->user()->id)->where('name', $name)->delete();
return true;
}
/**
* Find by name, has no user ID in it, because the method is called from an unauthenticated route any way.
*/
public function findByName(string $name): Collection
{
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
@@ -71,123 +103,6 @@ class Preferences
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;
}
public function delete(string $name): bool
{
$fullName = sprintf('preference%s%s', auth()->user()->id, $name);
if (Cache::has($fullName)) {
Cache::forget($fullName);
}
Preference::where('user_id', auth()->user()->id)->where('name', $name)->delete();
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.
*/
public function findByName(string $name): Collection
{
return Preference::where('name', $name)->get();
}
public function getArrayForUser(User $user, array $list): array
{
$result = [];
@@ -197,8 +112,7 @@ class Preferences
$q->orWhere('user_group_id', $user->user_group_id);
})
->whereIn('name', $list)
->get(['id', 'name', 'data'])
;
->get(['id', 'name', 'data']);
/** @var Preference $preference */
foreach ($preferences as $preference) {
@@ -265,6 +179,41 @@ class Preferences
return $result;
}
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 */
@@ -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;
}
}

View File

@@ -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,6 +107,43 @@ class BudgetReportGenerator
$this->percentageReport();
}
public function getReport(): array
{
return $this->report;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
public function setBudgets(Collection $budgets): void
{
$this->budgets = $budgets;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* @throws FireflyException
*/
public function setUser(User $user): void
{
$this->repository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$this->nbRepository->setUser($user);
$this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup);
}
/**
* Start the budgets block on the default report by processing every budget.
*/
@@ -157,78 +157,6 @@ class BudgetReportGenerator
}
}
/**
* 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.
*/
@@ -313,40 +241,112 @@ class BudgetReportGenerator
}
}
public function getReport(): array
/**
* Process expenses etc. for a single budget for the budgets block on the default report.
*/
private function processBudget(Budget $budget): void
{
return $this->report;
}
$budgetId = $budget->id;
$this->report['budgets'][$budgetId] ??= [
'budget_id' => $budgetId,
'budget_name' => $budget->name,
'no_budget' => false,
'budget_limits' => [],
];
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
// get all budget limits for budget in period:
$limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end);
public function setBudgets(Collection $budgets): void
{
$this->budgets = $budgets;
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$this->processLimit($budget, $limit);
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* @throws FireflyException
* Process each set of transactions for each row of expenses.
*/
public function setUser(User $user): void
private function processBudgetExpenses(array $expenses, array $budget): void
{
$this->repository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$this->nbRepository->setUser($user);
$this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup);
$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);
}
}

View File

@@ -83,40 +83,25 @@ 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;
}
private function processCurrencyArray(int $currencyId, array $currencyRow): void
public function setEnd(Carbon $end): void
{
$this->report['sums'][$currencyId] ??= [
'spent' => '0',
'earned' => '0',
'sum' => '0',
'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'],
];
/**
* @var int $categoryId
* @var array $categoryRow
*/
foreach ($currencyRow['categories'] as $categoryId => $categoryRow) {
$this->processCategoryRow($currencyId, $currencyRow, $categoryId, $categoryRow);
$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
@@ -164,24 +149,39 @@ class CategoryReportGenerator
}
}
public function setAccounts(Collection $accounts): void
private function processCurrencyArray(int $currencyId, array $currencyRow): void
{
$this->accounts = $accounts;
$this->report['sums'][$currencyId] ??= [
'spent' => '0',
'earned' => '0',
'sum' => '0',
'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'],
];
/**
* @var int $categoryId
* @var array $categoryRow
*/
foreach ($currencyRow['categories'] as $categoryId => $categoryRow) {
$this->processCategoryRow($currencyId, $currencyRow, $categoryId, $categoryRow);
}
}
public function setEnd(Carbon $end): void
/**
* Process one of the spent arrays from the operations method.
*/
private function processOpsArray(array $data): 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);
/**
* @var int $currencyId
* @var array $currencyRow
*/
foreach ($data as $currencyId => $currencyRow) {
$this->processCurrencyArray($currencyId, $currencyRow);
}
}
}

View File

@@ -43,13 +43,6 @@ 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)));
@@ -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);
}
}

View File

@@ -100,8 +100,7 @@ trait UserGroupTrait
{
$memberships = GroupMembership::where('user_id', $this->user->id)
->where('user_group_id', $userGroupId)
->count()
;
->count();
if (0 === $memberships) {
throw new FireflyException(sprintf('User #%d has no access to administration #%d', $this->user->id, $userGroupId));
}

View File

@@ -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.
*/
@@ -132,71 +146,21 @@ 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');
return true;
return (
null === $this->get($longitudeKey)
&& null === $this->get($latitudeKey)
&& null === $this->get($zoomLevelKey))
&& (
'PUT' === $this->method()
|| ('POST' === $this->method() && $this->routeIs('*.update'))
);
}
// 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;
}
/**
* 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
{
@@ -238,19 +202,55 @@ trait AppendsLocationData
return false;
}
private function isValidEmptyPUT(?string $prefix): bool
private function isValidPost(?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');
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;
}
}

View File

@@ -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.
*/
@@ -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;
}
}

View File

@@ -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
{

View File

@@ -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;
/**
@@ -65,8 +64,7 @@ class AccountSearch implements GenericSearchInterface
$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)
;
->whereIn('account_types.type', $this->types);
$like = sprintf('%%%s%%', $this->query);
$originalQuery = $this->query;

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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 = '';
@@ -194,6 +178,22 @@ class QueryParser implements QueryParserInterface
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) {

View File

@@ -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;
}
}

View File

@@ -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
*/
@@ -75,18 +149,6 @@ class Steam
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.
@@ -267,8 +429,7 @@ class Steam
'transactions.transaction_currency_id',
DB::raw('SUM(transactions.amount) AS sum_of_day'),
]
)
;
);
$currentBalance = $startBalance;
$converter = new ExchangeRateConverter();
@@ -321,169 +482,34 @@ 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')));
// 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']));
$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);
}
// 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']));
// TODO careless float could break financial math.
return number_format((float)$value, $post, '.', '');
}
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
@@ -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,6 +550,21 @@ 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 = [];
@@ -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 [
@@ -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 = [];
@@ -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;
}
}

View File

@@ -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,22 +42,18 @@ class OAuthKeys
private const string PRIVATE_KEY = 'oauth_private_key';
private const string PUBLIC_KEY = 'oauth_public_key';
public static function verifyKeysRoutine(): void
public static function generateKeys(): void
{
if (!self::keysInDatabase() && !self::hasKeyFiles()) {
self::generateKeys();
self::storeKeysInDB();
Artisan::registerCommand(new KeysCommand());
Artisan::call('firefly-iii:laravel-passport-keys');
}
return;
}
if (self::keysInDatabase() && !self::hasKeyFiles()) {
self::restoreKeysFromDB();
public static function hasKeyFiles(): bool
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
return;
}
if (!self::keysInDatabase() && self::hasKeyFiles()) {
self::storeKeysInDB();
}
return file_exists($private) && file_exists($public);
}
public static function keysInDatabase(): bool
@@ -82,28 +77,6 @@ class OAuthKeys
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
*/
@@ -132,4 +105,30 @@ class OAuthKeys
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()) {
self::generateKeys();
self::storeKeysInDB();
return;
}
if (self::keysInDatabase() && !self::hasKeyFiles()) {
self::restoreKeysFromDB();
return;
}
if (!self::keysInDatabase() && self::hasKeyFiles()) {
self::storeKeysInDB();
}
}
}

View File

@@ -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,6 +94,47 @@ class AmountFormat extends AbstractExtension
);
}
/**
* Use the code to format a currency.
*/
protected function formatAmountByCode(): TwigFunction
{
// formatAmountByCode
return new TwigFunction(
'formatAmountByCode',
static function (string $amount, string $code, ?bool $coloured = null): string {
$coloured ??= true;
try {
$currency = Amount::getTransactionCurrencyByCode($code);
} catch (FireflyException) {
Log::error(sprintf('Could not find currency with code "%s". Fallback to primary currency.', $code));
$currency = Amount::getPrimaryCurrency();
Log::error(sprintf('Fallback currency is "%s".', $currency->code));
}
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']]
);
}
/**
* Will format the amount by the currency related to the given account.
*/
@@ -135,42 +163,14 @@ class AmountFormat extends AbstractExtension
);
}
/**
* Will format the amount by the currency related to the given account.
*/
protected function formatAmountByCurrency(): TwigFunction
protected function formatAmountPlain(): TwigFilter
{
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.
*/
protected function formatAmountByCode(): TwigFunction
{
// formatAmountByCode
return new TwigFunction(
'formatAmountByCode',
static function (string $amount, string $code, ?bool $coloured = null): string {
$coloured ??= true;
try {
$currency = Amount::getTransactionCurrencyByCode($code);
} catch (FireflyException) {
Log::error(sprintf('Could not find currency with code "%s". Fallback to primary currency.', $code));
return new TwigFilter(
'formatAmountPlain',
static function (string $string): string {
$currency = Amount::getPrimaryCurrency();
Log::error(sprintf('Fallback currency is "%s".', $currency->code));
}
return Amount::formatAnything($currency, $amount, $coloured);
return Amount::formatAnything($currency, $string, false);
},
['is_safe' => ['html']]
);

View File

@@ -37,7 +37,6 @@ use Override;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use function Safe\parse_url;
/**
@@ -57,6 +56,91 @@ class General extends AbstractExtension
];
}
#[Override]
public function getFunctions(): array
{
return [
$this->phpdate(),
$this->activeRouteStrict(),
$this->activeRoutePartial(),
$this->activeRoutePartialObjectType(),
$this->menuOpenRoutePartial(),
$this->formatDate(),
$this->getMetaField(),
$this->hasRole(),
$this->getRootSearchOperator(),
$this->carbonize(),
];
}
/**
* Will return "active" when a part of the route matches the argument.
* ie. "accounts" will match "accounts.index".
*/
protected function activeRoutePartial(): TwigFunction
{
return new TwigFunction(
'activeRoutePartial',
static function (): string {
$args = func_get_args();
$route = $args[0]; // name of the route.
$name = Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return 'active';
}
return '';
}
);
}
/**
* This function will return "active" when the current route matches the first argument (even partly)
* but, the variable $objectType has been set and matches the second argument.
*/
protected function activeRoutePartialObjectType(): TwigFunction
{
return new TwigFunction(
'activeRoutePartialObjectType',
static function ($context): string {
[, $route, $objectType] = func_get_args();
$activeObjectType = $context['objectType'] ?? false;
if ($objectType === $activeObjectType
&& false !== stripos(
(string)Route::getCurrentRoute()->getName(),
(string)$route
)) {
return 'active';
}
return '';
},
['needs_context' => true]
);
}
/**
* 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.
*/
@@ -108,6 +192,29 @@ class General extends AbstractExtension
);
}
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.
*/
@@ -131,6 +238,96 @@ class General extends AbstractExtension
);
}
/**
* 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".
*/
protected function menuOpenRoutePartial(): TwigFunction
{
return new TwigFunction(
'menuOpenRoutePartial',
static function (): string {
$args = func_get_args();
$route = $args[0]; // name of the route.
$name = Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return 'menu-open';
}
return '';
}
);
}
/**
* Show icon with attachment.
*
@@ -154,25 +351,6 @@ class General extends AbstractExtension
);
}
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
*/
@@ -195,23 +373,6 @@ class General extends AbstractExtension
);
}
#[Override]
public function getFunctions(): array
{
return [
$this->phpdate(),
$this->activeRouteStrict(),
$this->activeRoutePartial(),
$this->activeRoutePartialObjectType(),
$this->menuOpenRoutePartial(),
$this->formatDate(),
$this->getMetaField(),
$this->hasRole(),
$this->getRootSearchOperator(),
$this->carbonize(),
];
}
/**
* Basic example thing for some views.
*/
@@ -222,166 +383,4 @@ class General extends AbstractExtension
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".
*/
protected function activeRoutePartial(): TwigFunction
{
return new TwigFunction(
'activeRoutePartial',
static function (): string {
$args = func_get_args();
$route = $args[0]; // name of the route.
$name = Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return 'active';
}
return '';
}
);
}
/**
* This function will return "active" when the current route matches the first argument (even partly)
* but, the variable $objectType has been set and matches the second argument.
*/
protected function activeRoutePartialObjectType(): TwigFunction
{
return new TwigFunction(
'activeRoutePartialObjectType',
static function ($context): string {
[, $route, $objectType] = func_get_args();
$activeObjectType = $context['objectType'] ?? false;
if ($objectType === $activeObjectType
&& false !== stripos(
(string)Route::getCurrentRoute()->getName(),
(string)$route
)) {
return 'active';
}
return '';
},
['needs_context' => true]
);
}
/**
* Will return "menu-open" when a part of the route matches the argument.
* ie. "accounts" will match "accounts.index".
*/
protected function menuOpenRoutePartial(): TwigFunction
{
return new TwigFunction(
'menuOpenRoutePartial',
static function (): string {
$args = func_get_args();
$route = $args[0]; // name of the route.
$name = Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return 'menu-open';
}
return '';
}
);
}
/**
* 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);
}
);
}
/**
* 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;
}
);
}
/**
* 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'))
);
}
}

View File

@@ -23,24 +23,33 @@ 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
@@ -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(),
];
}
}

View File

@@ -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,70 +75,60 @@ class TransactionGroupTwig extends AbstractExtension
);
}
/**
* Generate normal amount for transaction from a transaction group.
*/
private function normalJournalArrayAmount(array $array): string
public function journalGetMetaDate(): TwigFunction
{
$type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
$amount = $array['amount'] ?? '0';
$colored = true;
$sourceType = $array['source_account_type'] ?? 'invalid';
$amount = $this->signAmount($amount, $type, $sourceType);
if (TransactionTypeEnum::TRANSFER->value === $type) {
$colored = false;
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'));
}
$result = app('amount')->formatFlat($array['currency_symbol'], (int) $array['currency_decimal_places'], $amount, $colored);
if (TransactionTypeEnum::TRANSFER->value === $type) {
return sprintf('<span class="text-info money-transfer">%s</span>', $result);
return new Carbon(json_decode((string)$entry->data, false));
}
);
}
return $result;
}
private function signAmount(string $amount, string $transactionType, string $sourceType): string
public function journalGetMetaField(): TwigFunction
{
// withdrawals stay negative
if (TransactionTypeEnum::WITHDRAWAL->value !== $transactionType) {
$amount = bcmul($amount, '-1');
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 '';
}
// opening balance and it comes from initial balance? its expense.
if (TransactionTypeEnum::OPENING_BALANCE->value === $transactionType && AccountTypeEnum::INITIAL_BALANCE->value !== $sourceType) {
$amount = bcmul($amount, '-1');
return json_decode((string)$entry->data, true);
}
);
}
// reconciliation and it comes from reconciliation?
if (TransactionTypeEnum::RECONCILIATION->value === $transactionType && AccountTypeEnum::RECONCILIATION->value !== $sourceType) {
return bcmul($amount, '-1');
}
return $amount;
}
/**
* Generate foreign amount for transaction from a transaction group.
*/
private function foreignJournalArrayAmount(array $array): string
public function journalHasMeta(): TwigFunction
{
$type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
$amount = $array['foreign_amount'] ?? '0';
$colored = true;
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();
$sourceType = $array['source_account_type'] ?? 'invalid';
$amount = $this->signAmount($amount, $type, $sourceType);
if (TransactionTypeEnum::TRANSFER->value === $type) {
$colored = false;
return 1 === $count;
}
$result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int) $array['foreign_currency_decimal_places'], $amount, $colored);
if (TransactionTypeEnum::TRANSFER->value === $type) {
return sprintf('<span class="text-info money-transfer">%s</span>', $result);
}
return $result;
);
}
/**
@@ -164,25 +153,21 @@ class TransactionGroupTwig extends AbstractExtension
}
/**
* Generate normal amount for transaction from a transaction group.
* Generate foreign amount for transaction from a transaction group.
*/
private function normalJournalObjectAmount(TransactionJournal $journal): string
private function foreignJournalArrayAmount(array $array): string
{
$type = $journal->transactionType->type;
/** @var Transaction $first */
$first = $journal->transactions()->where('amount', '<', 0)->first();
$currency = $journal->transactionCurrency;
$amount = $first->amount ?? '0';
$type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
$amount = $array['foreign_amount'] ?? '0';
$colored = true;
$sourceType = $first->account->accountType()->first()->type;
$sourceType = $array['source_account_type'] ?? 'invalid';
$amount = $this->signAmount($amount, $type, $sourceType);
if (TransactionTypeEnum::TRANSFER->value === $type) {
$colored = false;
}
$result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored);
$result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int)$array['foreign_currency_decimal_places'], $amount, $colored);
if (TransactionTypeEnum::TRANSFER->value === $type) {
return sprintf('<span class="text-info money-transfer">%s</span>', $result);
}
@@ -190,14 +175,6 @@ class TransactionGroupTwig extends AbstractExtension
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.
*/
@@ -225,62 +202,81 @@ class TransactionGroupTwig extends AbstractExtension
return $result;
}
public function journalHasMeta(): TwigFunction
private function journalObjectHasForeign(TransactionJournal $journal): bool
{
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()
;
/** @var Transaction $first */
$first = $journal->transactions()->where('amount', '<', 0)->first();
return 1 === $count;
}
);
return '' !== $first->foreign_amount;
}
public function journalGetMetaDate(): TwigFunction
/**
* Generate normal amount for transaction from a transaction group.
*/
private function normalJournalArrayAmount(array $array): string
{
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'));
$type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
$amount = $array['amount'] ?? '0';
$colored = true;
$sourceType = $array['source_account_type'] ?? 'invalid';
$amount = $this->signAmount($amount, $type, $sourceType);
if (TransactionTypeEnum::TRANSFER->value === $type) {
$colored = false;
}
return new Carbon(json_decode((string) $entry->data, false));
}
);
$result = app('amount')->formatFlat($array['currency_symbol'], (int)$array['currency_decimal_places'], $amount, $colored);
if (TransactionTypeEnum::TRANSFER->value === $type) {
return sprintf('<span class="text-info money-transfer">%s</span>', $result);
}
public function journalGetMetaField(): TwigFunction
return $result;
}
/**
* Generate normal amount for transaction from a transaction group.
*/
private function normalJournalObjectAmount(TransactionJournal $journal): string
{
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 '';
$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('<span class="text-info money-transfer">%s</span>', $result);
}
return json_decode((string) $entry->data, true);
return $result;
}
);
private function signAmount(string $amount, string $transactionType, string $sourceType): string
{
// withdrawals stay negative
if (TransactionTypeEnum::WITHDRAWAL->value !== $transactionType) {
$amount = bcmul($amount, '-1');
}
// opening balance and it comes from initial balance? its expense.
if (TransactionTypeEnum::OPENING_BALANCE->value === $transactionType && AccountTypeEnum::INITIAL_BALANCE->value !== $sourceType) {
$amount = bcmul($amount, '-1');
}
// reconciliation and it comes from reconciliation?
if (TransactionTypeEnum::RECONCILIATION->value === $transactionType && AccountTypeEnum::RECONCILIATION->value !== $sourceType) {
return bcmul($amount, '-1');
}
return $amount;
}
}

View File

@@ -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.