diff --git a/app/Api/V2/Controllers/Autocomplete/AccountController.php b/app/Api/V2/Controllers/Autocomplete/AccountController.php index 97d22e015a..2f6dafa2d5 100644 --- a/app/Api/V2/Controllers/Autocomplete/AccountController.php +++ b/app/Api/V2/Controllers/Autocomplete/AccountController.php @@ -68,8 +68,8 @@ class AccountController extends Controller */ public function accounts(AutocompleteRequest $request): JsonResponse { - $queryParameters = $request->getParameters(); - $result = $this->repository->searchAccount($queryParameters['query'], $queryParameters['account_types'], $queryParameters['size']); + $params = $request->getParameters(); + $result = $this->repository->searchAccount($params['query'], $params['account_types'], $params['page'], $params['size']); $return = []; /** @var Account $account */ @@ -89,6 +89,7 @@ class AccountController extends Controller 'title' => $account->name, 'meta' => [ 'type' => $account->accountType->type, + // TODO is multi currency property. 'currency_id' => null === $currency ? null : (string) $currency->id, 'currency_code' => $currency?->code, 'currency_symbol' => $currency?->symbol, diff --git a/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php b/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php index 227e9d11c6..faa6743b05 100644 --- a/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php +++ b/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php @@ -23,15 +23,12 @@ declare(strict_types=1); namespace FireflyIII\Api\V2\Request\Autocomplete; -use FireflyIII\JsonApi\Rules\IsValidFilter; -use FireflyIII\JsonApi\Rules\IsValidPage; +use FireflyIII\Models\AccountType; use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\Support\Http\Api\ParsesQueryFilters; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; -use LaravelJsonApi\Core\Query\QueryParameters; -use LaravelJsonApi\Validation\Rule as JsonApiRule; /** * Class AutocompleteRequest @@ -51,35 +48,44 @@ class AutocompleteRequest extends FormRequest */ public function getParameters(): array { - $queryParameters = QueryParameters::cast($this->all()); - - return [ - 'date' => $this->dateOrToday($queryParameters, 'date'), - 'query' => $this->arrayOfStrings($queryParameters, 'query'), - 'size' => $this->integerFromQueryParams($queryParameters, 'size', 50), - 'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')), + $array = [ + 'date' => $this->date('date'), + 'query' => $this->clearString((string) $this->get('query')), + 'size' => $this->integerFromValue('size'), + 'page' => $this->integerFromValue('page'), + 'account_types' => $this->arrayFromValue($this->get('account_types')), + 'transaction_types' => $this->arrayFromValue($this->get('transaction_types')), ]; + $array['size'] = $array['size'] < 1 || $array['size'] > 100 ? 15 : $array['size']; + $array['page'] = max($array['page'], 1); + if (null === $array['account_types']) { + $array['account_types'] = []; + } + if (null === $array['transaction_types']) { + $array['transaction_types'] = []; + } + + // remove 'initial balance' from allowed types. its internal + $array['account_types'] = array_diff($array['account_types'], [AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION, AccountType::CREDITCARD]); + $array['account_types'] = $this->getAccountTypeParameter($array['account_types']); + return $array; } public function rules(): array { + $valid = array_keys($this->types); return [ - 'fields' => JsonApiRule::notSupported(), - 'filter' => ['nullable', 'array', new IsValidFilter(['query', 'date', 'account_types'])], - 'include' => JsonApiRule::notSupported(), - 'page' => ['nullable', 'array', new IsValidPage(['size'])], - 'sort' => JsonApiRule::notSupported(), + 'date' => 'nullable|date|after:1900-01-01|before:2100-01-01', + 'query' => 'nullable|string', + 'size' => 'nullable|integer|min:1|max:100', + 'page' => 'nullable|integer|min:1', + 'account_types' => sprintf('nullable|in:%s', join(',', $valid)), + 'transaction_types' => 'nullable|in:todo', ]; } - private function getAccountTypeParameter(mixed $types): array + private function getAccountTypeParameter(array $types): array { - if (is_string($types) && str_contains($types, ',')) { - $types = explode(',', $types); - } - if (!is_iterable($types)) { - $types = [$types]; - } $return = []; foreach ($types as $type) { $return = array_merge($return, $this->mapAccountTypes($type)); diff --git a/app/Console/Commands/Correction/FixUnevenAmount.php b/app/Console/Commands/Correction/FixUnevenAmount.php index 2124d72e83..5e518dd538 100644 --- a/app/Console/Commands/Correction/FixUnevenAmount.php +++ b/app/Console/Commands/Correction/FixUnevenAmount.php @@ -51,7 +51,11 @@ class FixUnevenAmount extends Command $this->convertOldStyleTransfers(); $this->fixUnevenAmounts(); $this->matchCurrencies(); - AccountBalanceCalculator::forceRecalculateAll(); + if(config('firefly.feature_flags.running_balance_column')) { + $this->friendlyInfo('Will recalculate transaction running balance columns. This may take a LONG time. Please be patient.'); + AccountBalanceCalculator::recalculateAll(true); + $this->friendlyInfo('Done recalculating transaction running balance columns.'); + } return 0; } diff --git a/app/Console/Commands/Upgrade/CorrectAccountBalance.php b/app/Console/Commands/Upgrade/CorrectAccountBalance.php index 75227d8f8c..191dcb85df 100644 --- a/app/Console/Commands/Upgrade/CorrectAccountBalance.php +++ b/app/Console/Commands/Upgrade/CorrectAccountBalance.php @@ -33,9 +33,10 @@ use Illuminate\Console\Command; class CorrectAccountBalance extends Command { use ShowsFriendlyMessages; + public const string CONFIG_NAME = '610_correct_balances'; - protected $description = 'Recalculate all account balance amounts'; - protected $signature = 'firefly-iii:correct-account-balance {--F|force : Force the execution of this command.}'; + protected $description = 'Recalculate all account balance amounts'; + protected $signature = 'firefly-iii:correct-account-balance {--F|force : Force the execution of this command.}'; public function handle(): int { @@ -44,23 +45,29 @@ class CorrectAccountBalance extends Command return 0; } + if(config('firefly.feature_flags.running_balance_column')) { + $this->friendlyInfo('Will recalculate account balances. This may take a LONG time. Please be patient.'); + $this->markAsExecuted(); + $this->correctBalanceAmounts(); + $this->friendlyInfo('Done recalculating account balances.'); + return 0; + } $this->friendlyWarning('This command has been disabled.'); - $this->markAsExecuted(); - // $this->correctBalanceAmounts(); return 0; } private function correctBalanceAmounts(): void { - AccountBalanceCalculator::recalculateAll(); + return; + AccountBalanceCalculator::recalculateAll(true); } private function isExecuted(): bool { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - return (bool)$configVar?->data; + return (bool) $configVar?->data; } private function markAsExecuted(): void diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php index 896afc947f..344f5fc785 100644 --- a/app/Http/Controllers/DebugController.php +++ b/app/Http/Controllers/DebugController.php @@ -95,7 +95,7 @@ class DebugController extends Controller // also do some recalculations. Artisan::call('firefly-iii:trigger-credit-recalculation'); - AccountBalanceCalculator::forceRecalculateAll(); + AccountBalanceCalculator::recalculateAll(true); try { Artisan::call('twig:clean'); diff --git a/app/Repositories/UserGroups/Account/AccountRepository.php b/app/Repositories/UserGroups/Account/AccountRepository.php index 5ed6de96bb..69dddebc9c 100644 --- a/app/Repositories/UserGroups/Account/AccountRepository.php +++ b/app/Repositories/UserGroups/Account/AccountRepository.php @@ -67,8 +67,7 @@ class AccountRepository implements AccountRepositoryInterface $q1->where('account_meta.name', '=', 'account_number'); $q1->where('account_meta.data', '=', $json); } - ) - ; + ); if (0 !== count($types)) { $dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); @@ -95,7 +94,7 @@ class AccountRepository implements AccountRepositoryInterface public function findByName(string $name, array $types): ?Account { - $query = $this->userGroup->accounts(); + $query = $this->userGroup->accounts(); if (0 !== count($types)) { $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); @@ -119,8 +118,8 @@ class AccountRepository implements AccountRepositoryInterface public function getAccountCurrency(Account $account): ?TransactionCurrency { - $type = $account->accountType->type; - $list = config('firefly.valid_currency_account_types'); + $type = $account->accountType->type; + $list = config('firefly.valid_currency_account_types'); // return null if not in this list. if (!in_array($type, $list, true)) { @@ -242,18 +241,17 @@ class AccountRepository implements AccountRepositoryInterface } } // reset the rest to zero. - $all = [AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::CREDITCARD, AccountType::MORTGAGE]; + $all = [AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::CREDITCARD, AccountType::MORTGAGE]; $this->user->accounts()->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereNotIn('account_types.type', $all) - ->update(['order' => 0]) - ; + ->whereNotIn('account_types.type', $all) + ->update(['order' => 0]); } public function getAccountsByType(array $types, ?array $sort = [], ?array $filters = []): Collection { - $sortable = ['name', 'active']; // TODO yes this is a duplicate array. - $res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types); - $query = $this->userGroup->accounts(); + $sortable = ['name', 'active']; // TODO yes this is a duplicate array. + $res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types); + $query = $this->userGroup->accounts(); if (0 !== count($types)) { $query->accountTypeIn($types); } @@ -298,34 +296,36 @@ class AccountRepository implements AccountRepositoryInterface return $query->get(['accounts.*']); } - public function searchAccount(array $query, array $types, int $limit): Collection + public function searchAccount(string $query, array $types, int $page, int $limit): Collection { // search by group, not by user $dbQuery = $this->userGroup->accounts() - ->where('active', true) - ->orderBy('accounts.order', 'ASC') - ->orderBy('accounts.account_type_id', 'ASC') - ->orderBy('accounts.name', 'ASC') - ->with(['accountType']) - ; - if (count($query) > 0) { - // split query on spaces just in case: + ->where('active', true) + ->orderBy('accounts.updated_at', 'ASC') + ->orderBy('accounts.order', 'ASC') + ->orderBy('accounts.account_type_id', 'ASC') + ->orderBy('accounts.name', 'ASC') + ->with(['accountType']); + + // split query on spaces just in case: + if('' !== trim($query)) { $dbQuery->where(function (EloquentBuilder $q) use ($query): void { - foreach ($query as $line) { - $parts = explode(' ', $line); - foreach ($parts as $part) { - $search = sprintf('%%%s%%', $part); - $q->orWhereLike('name', $search); - } + $parts = explode(' ', $query); + foreach ($parts as $part) { + $search = sprintf('%%%s%%', $part); + $q->orWhereLike('name', $search); } }); } + if (0 !== count($types)) { $dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $dbQuery->whereIn('account_types.type', $types); } - return $dbQuery->take($limit)->get(['accounts.*']); + $dbQuery->skip(($page-1) * $limit)->take($limit); + return $dbQuery->get(['accounts.*']); + } #[\Override] @@ -352,19 +352,18 @@ class AccountRepository implements AccountRepositoryInterface public function getAccountTypes(Collection $accounts): Collection { return AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id') - ->whereIn('accounts.id', $accounts->pluck('id')->toArray()) - ->get(['accounts.id', 'account_types.type']) - ; + ->whereIn('accounts.id', $accounts->pluck('id')->toArray()) + ->get(['accounts.id', 'account_types.type']); } #[\Override] public function getLastActivity(Collection $accounts): array { return Transaction::whereIn('account_id', $accounts->pluck('id')->toArray()) - ->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id') - ->groupBy('transactions.account_id') - ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line - ; + ->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id') + ->groupBy('transactions.account_id') + ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line + ; } #[\Override] @@ -373,8 +372,7 @@ class AccountRepository implements AccountRepositoryInterface $groupIds = []; $return = []; $set = DB::table('object_groupables')->where('object_groupable_type', Account::class) - ->whereIn('object_groupable_id', $accounts->pluck('id')->toArray())->get() - ; + ->whereIn('object_groupable_id', $accounts->pluck('id')->toArray())->get(); /** @var \stdClass $row */ foreach ($set as $row) { diff --git a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php index dfcd0b6f3f..b624363da6 100644 --- a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php +++ b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php @@ -80,7 +80,7 @@ interface AccountRepositoryInterface */ public function resetAccountOrder(): void; - public function searchAccount(array $query, array $types, int $limit): Collection; + public function searchAccount(string $query, array $types,int $page, int $limit): Collection; public function setUser(User $user): void; diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php index 058a7eb8eb..82098bafa9 100644 --- a/app/Support/Models/AccountBalanceCalculator.php +++ b/app/Support/Models/AccountBalanceCalculator.php @@ -24,10 +24,10 @@ declare(strict_types=1); namespace FireflyIII\Support\Models; use Carbon\Carbon; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountBalance; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -47,20 +47,15 @@ class AccountBalanceCalculator } /** - * Recalculate all balances. + * Recalculate all account and transaction balances. */ - public static function forceRecalculateAll(): void - { - Transaction::whereNull('deleted_at')->update(['balance_dirty' => true]); - $object = new self(); - $object->optimizedCalculation(new Collection()); - } - - /** - * Recalculate all balances. - */ - public static function recalculateAll(): void + public static function recalculateAll(bool $forced): void { + if ($forced) { + Transaction::whereNull('deleted_at')->update(['balance_dirty' => true]); + // also delete account balances. + AccountBalance::whereNotNull('created_at')->delete(); + } $object = new self(); $object->optimizedCalculation(new Collection()); } @@ -68,7 +63,7 @@ class AccountBalanceCalculator public static function recalculateForJournal(TransactionJournal $transactionJournal): void { Log::debug(__METHOD__); - $object = new self(); + $object = new self(); // recalculate the involved accounts: $accounts = new Collection(); @@ -84,18 +79,17 @@ class AccountBalanceCalculator 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') + $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) - ; + ->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); @@ -108,9 +102,9 @@ class AccountBalanceCalculator private function getAccountBalanceByAccount(int $account, int $currency): AccountBalance { - $query = AccountBalance::where('title', 'balance')->where('account_id', $account)->where('transaction_currency_id', $currency); + $query = AccountBalance::where('title', 'balance')->where('account_id', $account)->where('transaction_currency_id', $currency); - $entry = $query->first(); + $entry = $query->first(); if (null !== $entry) { // Log::debug(sprintf('Found account balance "balance" for account #%d and currency #%d: %s', $account, $currency, $entry->balance)); @@ -130,12 +124,6 @@ class AccountBalanceCalculator private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void { Log::debug('start of optimizedCalculation'); - if (false === config('firefly.feature_flags.running_balance_column')) { - Log::debug('optimizedCalculation is disabled, return.'); - - return; - } - if ($accounts->count() > 0) { Log::debug(sprintf('Limited to %d account(s)', $accounts->count())); } @@ -143,15 +131,14 @@ class AccountBalanceCalculator $balances = []; $count = 0; $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->whereNull('transaction_journals.deleted_at') // this order is the same as GroupCollector, but in the exact reverse. - ->orderBy('transaction_journals.date', 'asc') - ->orderBy('transaction_journals.order', 'desc') - ->orderBy('transaction_journals.id', 'asc') - ->orderBy('transaction_journals.description', 'asc') - ->orderBy('transactions.amount', 'asc') - ; + ->orderBy('transaction_journals.date', 'asc') + ->orderBy('transaction_journals.order', 'desc') + ->orderBy('transaction_journals.id', 'asc') + ->orderBy('transaction_journals.description', 'asc') + ->orderBy('transactions.amount', 'asc'); if ($accounts->count() > 0) { $query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray()); } @@ -160,17 +147,20 @@ class AccountBalanceCalculator $query->where('transaction_journals.date', '>=', $notBefore); } - $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); + $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); + + // the balance value is an array. + // first entry is the balance, second is the date. /** @var Transaction $entry */ foreach ($set as $entry) { // start with empty array: $balances[$entry->account_id] ??= []; - $balances[$entry->account_id][$entry->transaction_currency_id] ??= $this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore); + $balances[$entry->account_id][$entry->transaction_currency_id] ??= [$this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore), null]; // before and after are easy: - $before = $balances[$entry->account_id][$entry->transaction_currency_id]; - $after = bcadd($before, $entry->amount); + $before = $balances[$entry->account_id][$entry->transaction_currency_id][0]; + $after = bcadd($before, $entry->amount); if (true === $entry->balance_dirty || $accounts->count() > 0) { // update the transaction: $entry->balance_before = $before; @@ -181,19 +171,20 @@ class AccountBalanceCalculator } // then update the array: - $balances[$entry->account_id][$entry->transaction_currency_id] = $after; + $balances[$entry->account_id][$entry->transaction_currency_id] = [$after, $entry->date]; } Log::debug(sprintf('end of optimizedCalculation, corrected %d balance(s)', $count)); // then update all transactions. - // ?? something with accounts? + // save all collected balances in their respective account objects. + $this->storeAccountBalances($balances); } private function getAccountBalanceByJournal(string $title, int $account, int $journal, int $currency): AccountBalance { - $query = AccountBalance::where('title', $title)->where('account_id', $account)->where('transaction_journal_id', $journal)->where('transaction_currency_id', $currency); + $query = AccountBalance::where('title', $title)->where('account_id', $account)->where('transaction_journal_id', $journal)->where('transaction_currency_id', $currency); - $entry = $query->first(); + $entry = $query->first(); if (null !== $entry) { return $entry; } @@ -208,145 +199,180 @@ class AccountBalanceCalculator return $entry; } - private function recalculateLatest(?Account $account): void +// private function recalculateLatest(?Account $account): void +// { +// $query = Transaction::groupBy(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']); +// +// if (null !== $account) { +// $query->where('transactions.account_id', $account->id); +// } +// $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]); +// +// // reset account balances: +// $this->resetAccountBalancesByAccount('balance', $account); +// +// /** @var \stdClass $row */ +// foreach ($result as $row) { +// $account = (int) $row->account_id; +// $transactionCurrency = (int) $row->transaction_currency_id; +// $foreignCurrency = (int) $row->foreign_currency_id; +// $sumAmount = (string) $row->sum_amount; +// $sumForeignAmount = (string) $row->sum_foreign_amount; +// $sumAmount = '' === $sumAmount ? '0' : $sumAmount; +// $sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount; +// +// // at this point SQLite may return scientific notation because why not. Terrible. +// $sumAmount = app('steam')->floatalize($sumAmount); +// $sumForeignAmount = app('steam')->floatalize($sumForeignAmount); +// +// // first create for normal currency: +// $entry = $this->getAccountBalanceByAccount($account, $transactionCurrency); +// +// try { +// $entry->balance = bcadd((string) $entry->balance, $sumAmount); +// } catch (\ValueError $e) { +// $message = sprintf('[a] Could not add "%s" to "%s": %s', $entry->balance, $sumAmount, $e->getMessage()); +// Log::error($message); +// +// throw new FireflyException($message, 0, $e); +// } +// $entry->save(); +// +// // then do foreign amount, if present: +// if ($foreignCurrency > 0) { +// $entry = $this->getAccountBalanceByAccount($account, $foreignCurrency); +// +// try { +// $entry->balance = bcadd((string) $entry->balance, $sumForeignAmount); +// } catch (\ValueError $e) { +// $message = sprintf('[b] Could not add "%s" to "%s": %s', $entry->balance, $sumForeignAmount, $e->getMessage()); +// Log::error($message); +// +// throw new FireflyException($message, 0, $e); +// } +// $entry->save(); +// } +// } +// Log::debug(sprintf('Recalculated %d account balance(s)', $result->count())); +// } + +// private function resetAccountBalancesByAccount(string $title, ?Account $account): void +// { +// if (null === $account) { +// $count = AccountBalance::whereNotNull('updated_at')->where('title', $title)->update(['balance' => '0']); +// Log::debug(sprintf('Set %d account balance(s) to zero.', $count)); +// +// return; +// } +// $count = AccountBalance::where('account_id', $account->id)->where('title', $title)->update(['balance' => '0']); +// Log::debug(sprintf('Set %d account balance(s) of account #%d to zero.', $count, $account->id)); +// } + +// /** +// * Als je alles opnieuw doet, verzamel je alle transactions en het bedrag en zet je dat neer als "balance after +// * journal". Dat betekent, netjes op volgorde van datum en doorrekenen. +// * +// * Zodra je een transaction journal verplaatst (datum) moet je dat journal en alle latere journals opnieuw doen. +// * Maar dan moet je van de account wel een beginnetje hebben, namelijk de balance tot en met dat moment. +// * +// * 1. Dus dan search je eerst naar die SUM, som alle transactions eerder dan (niet inclusief) de journal. +// * 2. En vanaf daar pak je alle journals op of na de journal (dus ook de journal zelf) en begin je door te tellen. +// * 3. Elke voorbij gaande journal entry "balance_after_journal" geef je een update of voeg je toe. +// */ +// private function recalculateJournals(?Account $account, ?TransactionJournal $transactionJournal): void +// { +// $query = Transaction::groupBy(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']); +// $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'); +// $query->orderBy('transaction_journals.date', 'asc'); +// $amounts = []; +// if (null !== $account) { +// $query->where('transactions.account_id', $account->id); +// } +// if (null !== $account && null !== $transactionJournal) { +// $query->where('transaction_journals.date', '>=', $transactionJournal->date); +// $amounts = $this->getStartAmounts($account, $transactionJournal); +// } +// $result = $query->get(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]); +// +// /** @var \stdClass $row */ +// foreach ($result as $row) { +// $account = (int) $row->account_id; +// $transactionCurrency = (int) $row->transaction_currency_id; +// $foreignCurrency = (int) $row->foreign_currency_id; +// $sumAmount = (string) $row->sum_amount; +// $sumForeignAmount = (string) $row->sum_foreign_amount; +// $journalId = (int) $row->id; +// +// // check for empty strings +// $sumAmount = '' === $sumAmount ? '0' : $sumAmount; +// $sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount; +// +// // new amounts: +// $amounts[$account][$transactionCurrency] = bcadd($amounts[$account][$transactionCurrency] ?? '0', $sumAmount); +// $amounts[$account][$foreignCurrency] = bcadd($amounts[$account][$foreignCurrency] ?? '0', $sumForeignAmount); +// +// // first create for normal currency: +// $entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $transactionCurrency); +// $entry->balance = $amounts[$account][$transactionCurrency]; +// $entry->save(); +// +// // then do foreign amount, if present: +// if ($foreignCurrency > 0) { +// $entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $foreignCurrency); +// $entry->balance = $amounts[$account][$foreignCurrency]; +// $entry->save(); +// } +// } +// +// // select transactions.account_id, transaction_journals.id, transactions.transaction_currency_id, transactions.foreign_currency_id, sum(transactions.amount), sum(transactions.foreign_amount) +// // +// // from transactions +// // +// // left join transaction_journals ON transaction_journals.id = transactions.transaction_journal_id +// // +// // group by account_id, transaction_journals.id, transaction_currency_id, foreign_currency_id +// // order by transaction_journals.date desc +// } + +// private function getStartAmounts(Account $account, TransactionJournal $journal): array +// { +// exit('here we are 1'); +// +// return []; +// } + + + private function storeAccountBalances(array $balances): void { - $query = Transaction::groupBy(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']); - - if (null !== $account) { - $query->where('transactions.account_id', $account->id); - } - $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]); - - // reset account balances: - $this->resetAccountBalancesByAccount('balance', $account); - - /** @var \stdClass $row */ - foreach ($result as $row) { - $account = (int) $row->account_id; - $transactionCurrency = (int) $row->transaction_currency_id; - $foreignCurrency = (int) $row->foreign_currency_id; - $sumAmount = (string) $row->sum_amount; - $sumForeignAmount = (string) $row->sum_foreign_amount; - $sumAmount = '' === $sumAmount ? '0' : $sumAmount; - $sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount; - - // at this point SQLite may return scientific notation because why not. Terrible. - $sumAmount = app('steam')->floatalize($sumAmount); - $sumForeignAmount = app('steam')->floatalize($sumForeignAmount); - - // first create for normal currency: - $entry = $this->getAccountBalanceByAccount($account, $transactionCurrency); - - try { - $entry->balance = bcadd((string) $entry->balance, $sumAmount); - } catch (\ValueError $e) { - $message = sprintf('[a] Could not add "%s" to "%s": %s', $entry->balance, $sumAmount, $e->getMessage()); - Log::error($message); - - throw new FireflyException($message, 0, $e); + /** + * @var int $accountId + * @var array $currencies + */ + foreach ($balances as $accountId => $currencies) { + /** @var Account $account */ + $account = Account::find($accountId); + if (null === $account) { + Log::error(sprintf('Could not find account #%d, will not save account balance.', $accountId)); + continue; } - $entry->save(); - // then do foreign amount, if present: - if ($foreignCurrency > 0) { - $entry = $this->getAccountBalanceByAccount($account, $foreignCurrency); - - try { - $entry->balance = bcadd((string) $entry->balance, $sumForeignAmount); - } catch (\ValueError $e) { - $message = sprintf('[b] Could not add "%s" to "%s": %s', $entry->balance, $sumForeignAmount, $e->getMessage()); - Log::error($message); - - throw new FireflyException($message, 0, $e); + /** + * @var int $currencyId + * @var array $balance + */ + foreach ($currencies as $currencyId => $balance) { + /** @var TransactionCurrency $currency */ + $currency = TransactionCurrency::find($currencyId); + if (null === $currency) { + Log::error(sprintf('Could not find currency #%d, will not save account balance.', $currencyId)); + continue; } - $entry->save(); + /** @var AccountBalance $object */ + $object = $account->accountBalances()->firstOrCreate(['title' => 'running_balance', 'balance' => '0', 'transaction_currency_id' => $currencyId, 'date' => $balance[1]]); + $object->balance = $balance[0]; + $object->date = $balance[1]; + $object->save(); } } - Log::debug(sprintf('Recalculated %d account balance(s)', $result->count())); - } - - private function resetAccountBalancesByAccount(string $title, ?Account $account): void - { - if (null === $account) { - $count = AccountBalance::whereNotNull('updated_at')->where('title', $title)->update(['balance' => '0']); - Log::debug(sprintf('Set %d account balance(s) to zero.', $count)); - - return; - } - $count = AccountBalance::where('account_id', $account->id)->where('title', $title)->update(['balance' => '0']); - Log::debug(sprintf('Set %d account balance(s) of account #%d to zero.', $count, $account->id)); - } - - /** - * Als je alles opnieuw doet, verzamel je alle transactions en het bedrag en zet je dat neer als "balance after - * journal". Dat betekent, netjes op volgorde van datum en doorrekenen. - * - * Zodra je een transaction journal verplaatst (datum) moet je dat journal en alle latere journals opnieuw doen. - * Maar dan moet je van de account wel een beginnetje hebben, namelijk de balance tot en met dat moment. - * - * 1. Dus dan search je eerst naar die SUM, som alle transactions eerder dan (niet inclusief) de journal. - * 2. En vanaf daar pak je alle journals op of na de journal (dus ook de journal zelf) en begin je door te tellen. - * 3. Elke voorbij gaande journal entry "balance_after_journal" geef je een update of voeg je toe. - */ - private function recalculateJournals(?Account $account, ?TransactionJournal $transactionJournal): void - { - $query = Transaction::groupBy(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']); - $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'); - $query->orderBy('transaction_journals.date', 'asc'); - $amounts = []; - if (null !== $account) { - $query->where('transactions.account_id', $account->id); - } - if (null !== $account && null !== $transactionJournal) { - $query->where('transaction_journals.date', '>=', $transactionJournal->date); - $amounts = $this->getStartAmounts($account, $transactionJournal); - } - $result = $query->get(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]); - - /** @var \stdClass $row */ - foreach ($result as $row) { - $account = (int) $row->account_id; - $transactionCurrency = (int) $row->transaction_currency_id; - $foreignCurrency = (int) $row->foreign_currency_id; - $sumAmount = (string) $row->sum_amount; - $sumForeignAmount = (string) $row->sum_foreign_amount; - $journalId = (int) $row->id; - - // check for empty strings - $sumAmount = '' === $sumAmount ? '0' : $sumAmount; - $sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount; - - // new amounts: - $amounts[$account][$transactionCurrency] = bcadd($amounts[$account][$transactionCurrency] ?? '0', $sumAmount); - $amounts[$account][$foreignCurrency] = bcadd($amounts[$account][$foreignCurrency] ?? '0', $sumForeignAmount); - - // first create for normal currency: - $entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $transactionCurrency); - $entry->balance = $amounts[$account][$transactionCurrency]; - $entry->save(); - - // then do foreign amount, if present: - if ($foreignCurrency > 0) { - $entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $foreignCurrency); - $entry->balance = $amounts[$account][$foreignCurrency]; - $entry->save(); - } - } - - // select transactions.account_id, transaction_journals.id, transactions.transaction_currency_id, transactions.foreign_currency_id, sum(transactions.amount), sum(transactions.foreign_amount) - // - // from transactions - // - // left join transaction_journals ON transaction_journals.id = transactions.transaction_journal_id - // - // group by account_id, transaction_journals.id, transaction_currency_id, foreign_currency_id - // order by transaction_journals.date desc - } - - private function getStartAmounts(Account $account, TransactionJournal $journal): array - { - exit('here we are 1'); - - return []; } } diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index a181aec0d2..95553306ff 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -847,7 +847,8 @@ class FireflyValidator extends Validator ->where('trigger', $trigger) ->where('response', $response) ->where('delivery', $delivery) - ->where('url', $url)->count(); + ->where('url', $url)->count() + ; } return false;