diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php index 9c9be0ae74..3739d82f5a 100644 --- a/app/Api/V1/Controllers/Chart/AccountController.php +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -116,7 +116,7 @@ class AccountController extends Controller ]; // TODO this code is also present in the V2 chart account controller so this method is due to be deprecated. $currentStart = clone $start; - $range = app('steam')->finalAccountBalanceInRange($account, $start, clone $end); + $range = app('steam')->finalAccountBalanceInRange($account, $start, clone $end, $this->convertToNative); // 2022-10-11 this method no longer converts to float. $previous = array_values($range)[0]; while ($currentStart <= $end) { diff --git a/app/Api/V1/Controllers/Controller.php b/app/Api/V1/Controllers/Controller.php index e4672dd757..18037b49bb 100644 --- a/app/Api/V1/Controllers/Controller.php +++ b/app/Api/V1/Controllers/Controller.php @@ -55,6 +55,7 @@ abstract class Controller extends BaseController /** @var array */ protected array $allowedSort; protected ParameterBag $parameters; + protected bool $convertToNative = false; /** * Controller constructor. @@ -68,7 +69,9 @@ abstract class Controller extends BaseController $this->parameters = $this->getParameters(); if (auth()->check()) { $language = app('steam')->getLanguage(); + $this->convertToNative = app('preferences')->get('convert_to_native', false)->data; app()->setLocale($language); + } return $next($request); diff --git a/app/Api/V2/Controllers/Chart/AccountController.php b/app/Api/V2/Controllers/Chart/AccountController.php index 379efc9d61..1a75860b32 100644 --- a/app/Api/V2/Controllers/Chart/AccountController.php +++ b/app/Api/V2/Controllers/Chart/AccountController.php @@ -118,7 +118,7 @@ class AccountController extends Controller 'native_entries' => [], ]; $currentStart = clone $params['start']; - $range = app('steam')->finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $currency); + $range = app('steam')->finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToNative); $previous = array_values($range)[0]['balance']; $previousNative = array_values($range)[0]['native_balance']; diff --git a/app/Api/V2/Controllers/Controller.php b/app/Api/V2/Controllers/Controller.php index 738e0747c6..0c3dc4869b 100644 --- a/app/Api/V2/Controllers/Controller.php +++ b/app/Api/V2/Controllers/Controller.php @@ -54,9 +54,10 @@ class Controller extends BaseController { use ValidatesUserGroupTrait; - protected const string CONTENT_TYPE = 'application/vnd.api+json'; + protected const string CONTENT_TYPE = 'application/vnd.api+json'; protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; protected ParameterBag $parameters; + protected bool $convertToNative = false; public function __construct() { @@ -77,12 +78,12 @@ class Controller extends BaseController */ private function getParameters(): ParameterBag { - $bag = new ParameterBag(); + $bag = new ParameterBag(); $bag->set('limit', 50); try { $page = (int) request()->get('page'); - } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { + } catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) { $page = 1; } @@ -112,7 +113,7 @@ class Controller extends BaseController if (null !== $date) { try { $obj = Carbon::parse((string) $date, config('app.timezone')); - } catch (InvalidDateException|InvalidFormatException $e) { + } catch (InvalidDateException | InvalidFormatException $e) { // don't care app('log')->warning(sprintf('Ignored invalid date "%s" in API v2 controller parameter check: %s', substr((string) $date, 0, 20), $e->getMessage())); } @@ -155,18 +156,18 @@ class Controller extends BaseController final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array { - $manager = new Manager(); - $baseUrl = request()->getSchemeAndHttpHost().'/api/v2'; + $manager = new Manager(); + $baseUrl = request()->getSchemeAndHttpHost() . '/api/v2'; // TODO add stuff to path? $manager->setSerializer(new JsonApiSerializer($baseUrl)); - $objects = $paginator->getCollection(); + $objects = $paginator->getCollection(); // the transformer, at this point, needs to collect information that ALL items in the collection // require, like meta-data and stuff like that, and save it for later. - $objects = $transformer->collectMetaData($objects); + $objects = $transformer->collectMetaData($objects); $paginator->setCollection($objects); $resource = new FractalCollection($objects, $transformer, $key); @@ -180,11 +181,11 @@ class Controller extends BaseController * * @param array|Model $object */ - final protected function jsonApiObject(string $key, array|Model $object, AbstractTransformer $transformer): array + final protected function jsonApiObject(string $key, array | Model $object, AbstractTransformer $transformer): array { // create some objects: - $manager = new Manager(); - $baseUrl = request()->getSchemeAndHttpHost().'/api/v2'; + $manager = new Manager(); + $baseUrl = request()->getSchemeAndHttpHost() . '/api/v2'; $manager->setSerializer(new JsonApiSerializer($baseUrl)); $transformer->collectMetaData(new Collection([$object])); diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index c16b13862e..5e21d6a0b3 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -30,7 +30,6 @@ use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -86,11 +85,11 @@ class AccountController extends Controller Log::debug('RevenueAccounts'); /** @var Carbon $start */ - $start = clone session('start', today(config('app.timezone'))->startOfMonth()); + $start = clone session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = clone session('end', today(config('app.timezone'))->endOfMonth()); - $cache = new CacheProperties(); + $end = clone session('end', today(config('app.timezone'))->endOfMonth()); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToNative); @@ -101,14 +100,14 @@ class AccountController extends Controller $start->subDay(); // prep some vars: - $currencies = []; - $chartData = []; - $tempData = []; - $default = Amount::getDefaultCurrency(); + $currencies = []; + $chartData = []; + $tempData = []; + $default = Amount::getDefaultCurrency(); // grab all accounts and names - $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::EXPENSE->value]); - $accountNames = $this->extractNames($accounts); + $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::EXPENSE->value]); + $accountNames = $this->extractNames($accounts); // grab all balances $startBalances = app('steam')->finalAccountsBalance($accounts, $start); @@ -140,13 +139,13 @@ class AccountController extends Controller continue; } Log::debug(sprintf('Will process expense array "%s" with amount %s', $key, $endBalance)); - $searchCode = $this->convertToNative ? $default->code : $key; + $searchCode = $this->convertToNative ? $default->code : $key; Log::debug(sprintf('Search code is %s', $searchCode)); // see if there is an accompanying start amount. // grab the difference and find the currency. $startBalance = ($startBalances[$account->id][$key] ?? '0'); Log::debug(sprintf('Start balance is %s', $startBalance)); - $diff = bcsub($endBalance, $startBalance); + $diff = bcsub($endBalance, $startBalance); $currencies[$searchCode] ??= $this->currencyRepository->findByCode($searchCode); if (0 !== bccomp($diff, '0')) { // store the values in a temporary array. @@ -164,10 +163,10 @@ class AccountController extends Controller foreach ($currencies as $currency) { $newCurrencies[$currency->id] = $currency; } - $currencies = $newCurrencies; + $currencies = $newCurrencies; // sort temp array by amount. - $amounts = array_column($tempData, 'diff_float'); + $amounts = array_column($tempData, 'diff_float'); array_multisort($amounts, SORT_DESC, $tempData); // loop all found currencies and build the data array for the chart. @@ -178,12 +177,12 @@ class AccountController extends Controller foreach ($currencies as $currencyId => $currency) { $dataSet = [ - 'label' => (string) trans('firefly.spent'), - 'type' => 'bar', - 'currency_symbol' => $currency->symbol, - 'currency_code' => $currency->code, - 'entries' => $this->expandNames($tempData), - ]; + 'label' => (string) trans('firefly.spent'), + 'type' => 'bar', + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, + 'entries' => $this->expandNames($tempData), + ]; $chartData[$currencyId] = $dataSet; } @@ -194,7 +193,7 @@ class AccountController extends Controller $chartData[$currencyId]['entries'][$name] = (float) $entry['difference']; } - $data = $this->generator->multiSet($chartData); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); @@ -216,7 +215,7 @@ class AccountController extends Controller */ public function expenseBudget(Account $account, Carbon $start, Carbon $end): JsonResponse { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty($start); $cache->addProperty($end); @@ -235,9 +234,9 @@ class AccountController extends Controller /** @var array $journal */ foreach ($journals as $journal) { - $budgetId = (int) $journal['budget_id']; - $key = sprintf('%d-%d', $budgetId, $journal['currency_id']); - $budgetIds[] = $budgetId; + $budgetId = (int) $journal['budget_id']; + $key = sprintf('%d-%d', $budgetId, $journal['currency_id']); + $budgetIds[] = $budgetId; if (!array_key_exists($key, $result)) { $result[$key] = [ 'total' => '0', @@ -250,7 +249,7 @@ class AccountController extends Controller $result[$key]['total'] = bcadd($journal['amount'], $result[$key]['total']); } - $names = $this->getBudgetNames($budgetIds); + $names = $this->getBudgetNames($budgetIds); foreach ($result as $row) { $budgetId = $row['budget_id']; @@ -259,7 +258,7 @@ class AccountController extends Controller $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } - $data = $this->generator->multiCurrencyPieChart($chartData); + $data = $this->generator->multiCurrencyPieChart($chartData); $cache->store($data); return response()->json($data); @@ -281,7 +280,7 @@ class AccountController extends Controller */ public function expenseCategory(Account $account, Carbon $start, Carbon $end): JsonResponse { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty($start); $cache->addProperty($end); @@ -299,7 +298,7 @@ class AccountController extends Controller /** @var array $journal */ foreach ($journals as $journal) { - $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); + $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); if (!array_key_exists($key, $result)) { $result[$key] = [ 'total' => '0', @@ -311,7 +310,7 @@ class AccountController extends Controller } $result[$key]['total'] = bcadd($journal['amount'], $result[$key]['total']); } - $names = $this->getCategoryNames(array_keys($result)); + $names = $this->getCategoryNames(array_keys($result)); foreach ($result as $row) { $categoryId = $row['category_id']; @@ -320,7 +319,7 @@ class AccountController extends Controller $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } - $data = $this->generator->multiCurrencyPieChart($chartData); + $data = $this->generator->multiCurrencyPieChart($chartData); $cache->store($data); return response()->json($data); @@ -333,9 +332,9 @@ class AccountController extends Controller * */ public function frontpage(AccountRepositoryInterface $repository): JsonResponse { - $start = clone session('start', today(config('app.timezone'))->startOfMonth()); - $end = clone session('end', today(config('app.timezone'))->endOfMonth()); - $defaultSet = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); + $start = clone session('start', today(config('app.timezone'))->startOfMonth()); + $end = clone session('end', today(config('app.timezone'))->endOfMonth()); + $defaultSet = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); Log::debug('Default set is ', $defaultSet); $frontpage = app('preferences')->get('frontpageAccounts', $defaultSet); $frontpageArray = !is_array($frontpage->data) ? [] : $frontpage->data; @@ -344,7 +343,7 @@ class AccountController extends Controller app('preferences')->set('frontpageAccounts', $defaultSet); Log::debug('frontpage set is empty!'); } - $accounts = $repository->getAccountsById($frontpageArray); + $accounts = $repository->getAccountsById($frontpageArray); return response()->json($this->accountBalanceChart($accounts, $start, $end)); } @@ -365,7 +364,7 @@ class AccountController extends Controller */ public function incomeCategory(Account $account, Carbon $start, Carbon $end): JsonResponse { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty($start); $cache->addProperty($end); @@ -385,7 +384,7 @@ class AccountController extends Controller /** @var array $journal */ foreach ($journals as $journal) { - $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); + $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); if (!array_key_exists($key, $result)) { $result[$key] = [ 'total' => '0', @@ -398,14 +397,14 @@ class AccountController extends Controller $result[$key]['total'] = bcadd($journal['amount'], $result[$key]['total']); } - $names = $this->getCategoryNames(array_keys($result)); + $names = $this->getCategoryNames(array_keys($result)); foreach ($result as $row) { $categoryId = $row['category_id']; $name = $names[$categoryId] ?? '(unknown)'; $label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } - $data = $this->generator->multiCurrencyPieChart($chartData); + $data = $this->generator->multiCurrencyPieChart($chartData); $cache->store($data); return response()->json($data); @@ -418,59 +417,103 @@ class AccountController extends Controller */ public function period(Account $account, Carbon $start, Carbon $end): JsonResponse { - $chartData = []; - $cache = new CacheProperties(); + Log::debug('Now in period()'); + $chartData = []; + $cache = new CacheProperties(); $cache->addProperty('chart.account.period'); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty($account->id); if ($cache->has()) { - return response()->json($cache->get()); - } - $currencies = $this->accountRepository->getUsedCurrencies($account); - - // if the account is not expense or revenue, just use the account's default currency. - if (!in_array($account->accountType->type, [AccountType::REVENUE, AccountType::EXPENSE], true)) { - $currencies = [$this->accountRepository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency()]; + // return response()->json($cache->get()); } - /** @var TransactionCurrency $currency */ - foreach ($currencies as $currency) { - $chartData[] = $this->periodByCurrency($start, $end, $account, $currency); + // collect and filter balances for the entire period. + $step = $this->calculateStep($start, $end); + Log::debug(sprintf('Step is %s', $step)); + $locale = app('steam')->getLocale(); + $return = []; + // fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041 + // have to make sure this chart is always based on the balance at the END of the period. + // This period depends on the size of the chart + $current = clone $start; + $current = app('navigation')->endOfX($current, $step, null); + $format = (string) trans('config.month_and_day_js', [], $locale); + $accountCurrency = $this->accountRepository->getAccountCurrency($account); + + Log::debug('One'); + $range = Steam::finalAccountBalanceInRange($account, $start, $end, $this->convertToNative); + Log::debug('Two'); + $range = Steam::filterAccountBalances($range, $account, $this->convertToNative, $accountCurrency); + Log::debug('Three'); + $previous = array_values($range)[0]; + $accountCurrency = $accountCurrency ?? $this->defaultCurrency; // do this AFTER getting the balances. + while ($end >= $current) { + $theDate = $current->format('Y-m-d'); + // each day contains multiple balances, and this may even be different over time. + $momentBalance = $range[$theDate] ?? $previous; + $return = $this->updateChartKeys($return, $momentBalance); + + // process each balance thing. + foreach($momentBalance as $key => $amount) { + $label = $current->isoFormat($format); + $return[$key]['entries'][$label] = $amount; + } + $current = app('navigation')->addPeriod($current, $step, 0); + // here too, to fix #8041, the data is corrected to the end of the period. + $current = app('navigation')->endOfX($current, $step, null); + $previous = $momentBalance; + } + // second loop (yes) to create nice array with info! Yay! + $chartData = []; + foreach($return as $key => $info) { + if(3 === strlen($key)) { + // assume it's a currency: + $setCurrency = $this->currencyRepository->findByCode($key); + $info['currency_symbol'] = $setCurrency->symbol; + $info['currency_code'] = $setCurrency->code; + $info['label'] = sprintf('%s (%s)', $account->name, $setCurrency->symbol); + } + if('balance' === $key) { + $info['currency_symbol'] = $accountCurrency->symbol; + $info['currency_code'] = $accountCurrency->code; + $info['label'] = sprintf('%s (%s)', $account->name, $accountCurrency->symbol); + } + if('native_balance' === $key) { + $info['currency_symbol'] = $this->defaultCurrency->symbol; + $info['currency_code'] = $this->defaultCurrency->code; + $info['label'] = sprintf('%s (%s) (%s)', $account->name, (string)trans('firefly.sum'), $this->defaultCurrency->symbol); + } + $chartData[] = $info; } - $data = $this->generator->multiSet($chartData); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); - } - /** - * @throws FireflyException - */ - private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array - { - Log::debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code)); - $locale = app('steam')->getLocale(); - $step = $this->calculateStep($start, $end); - $result = [ + + + var_dump($chartData);exit; + + + + + $result = [ 'label' => sprintf('%s (%s)', $account->name, $currency->symbol), 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, ]; - $entries = []; - $current = clone $start; - Log::debug(sprintf('Step is %s', $step)); + $entries = []; + $current = clone $start; + - // fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041 - // have to make sure this chart is always based on the balance at the END of the period. - // This period depends on the size of the chart - $current = app('navigation')->endOfX($current, $step, null); Log::debug(sprintf('$current date is %s', $current->format('Y-m-d'))); if ('1D' === $step) { // per day the entire period, balance for every day. $format = (string) trans('config.month_and_day_js', [], $locale); - $range = app('steam')->finalAccountBalanceInRange($account, $start, $end); + $range = app('steam')->finalAccountBalanceInRange($account, $start, $end, $this->convertToNative); $previous = array_values($range)[0]; while ($end >= $current) { $theDate = $current->format('Y-m-d'); @@ -489,7 +532,70 @@ class AccountController extends Controller $entries[$label] = $balance; $current = app('navigation')->addPeriod($current, $step, 0); // here too, to fix #8041, the data is corrected to the end of the period. - $current = app('navigation')->endOfX($current, $step, null); + $current = app('navigation')->endOfX($current, $step, null); + } + } + $result['entries'] = $entries; + + return $result; + + + /** @var TransactionCurrency $currency */ + foreach ($currencies as $currency) { + $chartData[] = $this->periodByCurrency($start, $end, $account, $currency); + } + + $data = $this->generator->multiSet($chartData); + $cache->store($data); + + return response()->json($data); + } + + /** + * @throws FireflyException + */ + private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array + { + Log::debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code)); + $locale = app('steam')->getLocale(); + $step = $this->calculateStep($start, $end); + $result = [ + 'label' => sprintf('%s (%s)', $account->name, $currency->symbol), + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, + ]; + $entries = []; + $current = clone $start; + Log::debug(sprintf('Step is %s', $step)); + + // fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041 + // have to make sure this chart is always based on the balance at the END of the period. + // This period depends on the size of the chart + $current = app('navigation')->endOfX($current, $step, null); + Log::debug(sprintf('$current date is %s', $current->format('Y-m-d'))); + if ('1D' === $step) { + // per day the entire period, balance for every day. + $format = (string) trans('config.month_and_day_js', [], $locale); + $range = app('steam')->finalAccountBalanceInRange($account, $start, $end, $this->convertToNative); + $previous = array_values($range)[0]; + while ($end >= $current) { + $theDate = $current->format('Y-m-d'); + $balance = $range[$theDate]['balance'] ?? $previous; + $label = $current->isoFormat($format); + $entries[$label] = (float) $balance; + $previous = $balance; + $current->addDay(); + } + } + if ('1W' === $step || '1M' === $step || '1Y' === $step) { + while ($end >= $current) { + Log::debug(sprintf('Current is: %s', $current->format('Y-m-d'))); + $balance = Steam::finalAccountBalance($account, $current)[$currency->code] ?? '0'; + $label = app('navigation')->periodShow($current, $step); + $entries[$label] = $balance; + $current = app('navigation')->addPeriod($current, $step, 0); + // here too, to fix #8041, the data is corrected to the end of the period. + $current = app('navigation')->endOfX($current, $step, null); } } $result['entries'] = $entries; @@ -517,11 +623,11 @@ class AccountController extends Controller public function revenueAccounts(): JsonResponse { /** @var Carbon $start */ - $start = clone session('start', today(config('app.timezone'))->startOfMonth()); + $start = clone session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = clone session('end', today(config('app.timezone'))->endOfMonth()); - $cache = new CacheProperties(); + $end = clone session('end', today(config('app.timezone'))->endOfMonth()); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToNative); @@ -532,14 +638,14 @@ class AccountController extends Controller $start->subDay(); // prep some vars: - $currencies = []; - $chartData = []; - $tempData = []; - $default = Amount::getDefaultCurrency(); + $currencies = []; + $chartData = []; + $tempData = []; + $default = Amount::getDefaultCurrency(); // grab all accounts and names - $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::REVENUE->value]); - $accountNames = $this->extractNames($accounts); + $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::REVENUE->value]); + $accountNames = $this->extractNames($accounts); // grab all balances $startBalances = app('steam')->finalAccountsBalance($accounts, $start); @@ -572,13 +678,13 @@ class AccountController extends Controller continue; } Log::debug(sprintf('Will process expense array "%s" with amount %s', $key, $endBalance)); - $searchCode = $this->convertToNative ? $default->code : $key; + $searchCode = $this->convertToNative ? $default->code : $key; Log::debug(sprintf('Search code is %s', $searchCode)); // see if there is an accompanying start amount. // grab the difference and find the currency. $startBalance = ($startBalances[$account->id][$key] ?? '0'); Log::debug(sprintf('Start balance is %s', $startBalance)); - $diff = bcsub($endBalance, $startBalance); + $diff = bcsub($endBalance, $startBalance); $currencies[$searchCode] ??= $this->currencyRepository->findByCode($searchCode); if (0 !== bccomp($diff, '0')) { // store the values in a temporary array. @@ -598,10 +704,10 @@ class AccountController extends Controller foreach ($currencies as $currency) { $newCurrencies[$currency->id] = $currency; } - $currencies = $newCurrencies; + $currencies = $newCurrencies; // sort temp array by amount. - $amounts = array_column($tempData, 'diff_float'); + $amounts = array_column($tempData, 'diff_float'); array_multisort($amounts, SORT_ASC, $tempData); // loop all found currencies and build the data array for the chart. @@ -612,12 +718,12 @@ class AccountController extends Controller foreach ($currencies as $currencyId => $currency) { $dataSet = [ - 'label' => (string) trans('firefly.earned'), - 'type' => 'bar', - 'currency_symbol' => $currency->symbol, - 'currency_code' => $currency->code, - 'entries' => $this->expandNames($tempData), - ]; + 'label' => (string) trans('firefly.earned'), + 'type' => 'bar', + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, + 'entries' => $this->expandNames($tempData), + ]; $chartData[$currencyId] = $dataSet; } @@ -628,9 +734,19 @@ class AccountController extends Controller $chartData[$currencyId]['entries'][$name] = bcmul($entry['difference'], '-1'); } - $data = $this->generator->multiSet($chartData); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); } + + private function updateChartKeys(array $array, array $balances): array + { + foreach (array_keys($balances) as $key) { + $array[$key] ??= [ + 'key' => $key, + ]; + } + return $array; + } } diff --git a/app/Support/Steam.php b/app/Support/Steam.php index fd5a8a9cd7..09bce4d50a 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -38,8 +38,8 @@ class Steam { 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)) { @@ -69,14 +69,15 @@ class Steam return $sum; } - public function finalAccountBalanceInRange(Account $account, Carbon $start, Carbon $end): array + public function finalAccountBalanceInRange(Account $account, Carbon $start, Carbon $end, bool $convertToNative): array { // expand period. $start->subDay()->startOfDay(); $end->addDay()->endOfDay(); + Log::debug(sprintf('finalAccountBalanceInRange(#%d, %s, %s)', $account->id, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); // set up cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('final-balance-in-range'); $cache->addProperty($start); @@ -85,71 +86,108 @@ class Steam // return $cache->get(); } - $balances = []; - $formatted = $start->format('Y-m-d'); - $startBalance = $this->finalAccountBalance($account, $start); - $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); - $currency = $this->getAccountCurrency($account) ?? $defaultCurrency; + $balances = []; + $formatted = $start->format('Y-m-d'); + $startBalance = $this->finalAccountBalance($account, $start); + $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); + $accountCurrency = $this->getAccountCurrency($account); + $hasCurrency = null !== $accountCurrency; + $currency = $accountCurrency ?? $defaultCurrency; + Log::debug(sprintf('Currency is %s', $currency->code)); + if (!$hasCurrency) { + Log::debug(sprintf('Also set start balance in %s', $defaultCurrency->code)); + $startBalance[$defaultCurrency->code] ??= '0'; + } $currencies = [ $currency->id => $currency, $defaultCurrency->id => $defaultCurrency, ]; - $startBalance[$defaultCurrency->code] ??= '0'; - $startBalance[$currency->code] ??= '0'; - $balances[$formatted] = $startBalance; + + + $startBalance[$currency->code] ??= '0'; + $balances[$formatted] = $startBalance; + Log::debug('Final start balance: ', $startBalance); // sums up the balance changes per day, for foreign, native and normal amounts. - $set = $account->transactions() - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) - ->groupBy('transaction_journals.date') - ->groupBy('transactions.transaction_currency_id') - ->groupBy('transactions.foreign_currency_id') - ->orderBy('transaction_journals.date', 'ASC') - ->whereNull('transaction_journals.deleted_at') - ->get( - [ // @phpstan-ignore-line - 'transaction_journals.date', - 'transactions.transaction_currency_id', - \DB::raw('SUM(transactions.amount) AS modified'), - 'transactions.foreign_currency_id', - \DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), - \DB::raw('SUM(transactions.native_amount) AS modified_native'), - ] - ) - ; + $set = $account->transactions() + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) + ->groupBy('transaction_journals.date') + ->groupBy('transactions.transaction_currency_id') + ->groupBy('transactions.foreign_currency_id') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') + ->get( + [ // @phpstan-ignore-line + 'transaction_journals.date', + 'transactions.transaction_currency_id', + \DB::raw('SUM(transactions.amount) AS modified'), + 'transactions.foreign_currency_id', + \DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), + \DB::raw('SUM(transactions.native_amount) AS modified_native'), + ] + ); - $currentBalance = $startBalance; + $currentBalance = $startBalance; /** @var Transaction $entry */ foreach ($set as $entry) { - // normal, native and foreign amount - $carbon = new Carbon($entry->date, $entry->date_tz); - $modified = (string) (null === $entry->modified ? '0' : $entry->modified); - $foreignModified = (string) (null === $entry->modified_foreign ? '0' : $entry->modified_foreign); - $nativeModified = (string) (null === $entry->modified_native ? '0' : $entry->modified_native); + $carbon = new Carbon($entry->date, $entry->date_tz); + $modified = (string) (null === $entry->modified ? '0' : $entry->modified); + $foreignModified = (string) (null === $entry->modified_foreign ? '0' : $entry->modified_foreign); + $nativeModified = (string) (null === $entry->modified_native ? '0' : $entry->modified_native); - // add "modified" to amount if the currency id matches the account currency id. - if ($entry->transaction_currency_id === $currency->id) { - $currentBalance['balance'] = bcadd($currentBalance['balance'], $modified); - $currentBalance[$currency->code] = bcadd($currentBalance[$currency->code], $modified); + // find currency of this entry. + $currencies[$entry->transaction_currency_id] = $currencies[$entry->transaction_currency_id] ?? TransactionCurrency::find($entry->transaction_currency_id); + $entryCurrency = $currencies[$entry->transaction_currency_id]; + + Log::debug(sprintf('Processing transaction(s) on date %s', $carbon->format('Y-m-d H:i:s'))); + + // if convert to native, if NOT convert to native. + if($convertToNative) { + Log::debug(sprintf('Amount is %s %s, foreign amount is %s, native amount is %s', $entryCurrency->code, $modified, $foreignModified, $nativeModified)); + // add to native balance. + $currentBalance['native_balance'] = bcadd($currentBalance['native_balance'], $nativeModified); + if($entry->foreign_currency_id === $defaultCurrency->id) { + $currentBalance['native_balance'] = bcadd($currentBalance['native_balance'], $foreignModified); + } + // add to balance if is the same. + if($entry->transaction_currency_id === $accountCurrency?->id) { + $currentBalance['balance'] = bcadd($currentBalance['balance'], $modified); + } + // add currency balance + $currentBalance[$entryCurrency->code] = bcadd($currentBalance[$entryCurrency->code], $modified); + } + if(!$convertToNative) { + Log::debug(sprintf('Amount is %s %s, foreign amount is %s, native amount is %s', $entryCurrency->code, $modified, $foreignModified, $nativeModified)); + // add to balance, as expected. + $currentBalance['balance'] = bcadd($currentBalance['balance'] ?? '0', $modified); + // add to GBP, as expected. + $currentBalance[$entryCurrency->code] = bcadd($currentBalance[$entryCurrency->code], $modified); } - // always add the native balance, even if it ends up at zero. - $currentBalance['native_balance'] = bcadd($currentBalance['native_balance'], $nativeModified); +// // add "modified" to amount if the currency id matches the account currency id. +// if ($entry->transaction_currency_id === $currency->id) { +// $currentBalance['balance'] = bcadd($currentBalance['balance'], $modified); +// $currentBalance[$currency->code] = bcadd($currentBalance[$currency->code], $modified); +// } +// +// // always add the native balance, even if it ends up at zero. +// $currentBalance['native_balance'] = bcadd($currentBalance['native_balance'], $nativeModified); - // add modified foreign to the array - if (null !== $entry->foreign_currency_id) { - $foreignId = $entry->foreign_currency_id; - $currencies[$foreignId] ??= TransactionCurrency::find($foreignId); - $foreignCurrency = $currencies[$foreignId]; - $currentBalance[$foreignCurrency->code] ??= '0'; - $currentBalance[$foreignCurrency->code] = bcadd($currentBalance[$foreignCurrency->code], $foreignModified); - } + // DO NOT add modified foreign to the array +// if (null !== $entry->foreign_currency_id) { +// $foreignId = $entry->foreign_currency_id; +// $currencies[$foreignId] ??= TransactionCurrency::find($foreignId); +// $foreignCurrency = $currencies[$foreignId]; +// $currentBalance[$foreignCurrency->code] ??= '0'; +// $currentBalance[$foreignCurrency->code] = bcadd($currentBalance[$foreignCurrency->code], $foreignModified); +// } $balances[$carbon->format('Y-m-d')] = $currentBalance; + Log::debug('Updated entry',$currentBalance); } $cache->store($balances); @@ -185,10 +223,10 @@ class Steam // Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision)); if (str_contains($number, '.')) { if ('-' !== $number[0]) { - return bcadd($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcadd($number, '0.' . str_repeat('0', $precision) . '5', $precision); } - return bcsub($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcsub($number, '0.' . str_repeat('0', $precision) . '5', $precision); } return $number; @@ -287,12 +325,11 @@ class Steam // first, the "balance", as described earlier. if ($convertToNative) { // normal balance - $return['balance'] = (string) $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) - ->where('transactions.transaction_currency_id', $native->id) - ->sum('transactions.amount') - ; + $return['balance'] = (string) $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->where('transactions.transaction_currency_id', $native->id) + ->sum('transactions.amount'); // plus virtual balance, if the account has a virtual_balance in the native currency if ($native->id === $accountCurrency?->id) { $return['balance'] = bcadd('' === (string) $account->virtual_balance ? '0' : $account->virtual_balance, $return['balance']); @@ -301,36 +338,33 @@ class Steam // native balance $return['native_balance'] = (string) $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) - ->whereNot('transactions.transaction_currency_id', $native->id) - ->sum('transactions.native_amount') - ; + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->whereNot('transactions.transaction_currency_id', $native->id) + ->sum('transactions.native_amount'); // plus native virtual balance. $return['native_balance'] = bcadd('' === (string) $account->native_virtual_balance ? '0' : $account->native_virtual_balance, $return['native_balance']); Log::debug(sprintf('native_balance is (all transactions to %s) %s (with virtual balance)', $native->code, $return['native_balance'])); // plus foreign transactions in THIS currency. $sum = (string) $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) - ->whereNot('transactions.transaction_currency_id', $native->id) - ->where('transactions.foreign_currency_id', $native->id) - ->sum('transactions.foreign_amount') - ; + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->whereNot('transactions.transaction_currency_id', $native->id) + ->where('transactions.foreign_currency_id', $native->id) + ->sum('transactions.foreign_amount'); $return['native_balance'] = bcadd($return['native_balance'], $sum); Log::debug(sprintf('Foreign amount transactions add (%s only) %s, total native_balance is now %s', $native->code, $sum, $return['native_balance'])); } // balance(s) in other (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'); + $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 the account has no own currency preference, drop balance in favor of native balance if ($hasCurrency && !$convertToNative) { @@ -346,30 +380,36 @@ class Steam if (!$hasCurrency && array_key_exists('balance', $return) && array_key_exists('native_balance', $return)) { Log::debug('Account has no currency preference, dropping balance in favor of native balance.'); - $sum = bcadd($return['balance'], $return['native_balance']); + $sum = bcadd($return['balance'], $return['native_balance']); Log::debug(sprintf('%s + %s = %s', $return['balance'], $return['native_balance'], $sum)); $return['native_balance'] = $sum; unset($return['balance']); } - - return array_merge($return, $others); + $final = array_merge($return, $others); + Log::debug('Return is', $final); + return $final; } - public function filterAccountBalances(array $total, Account $account, bool $convertToNative, ?TransactionCurrency $currency = null): array { + public function filterAccountBalances(array $total, Account $account, bool $convertToNative, ?TransactionCurrency $currency = null): array + { + Log::debug(sprintf('filterAccountBalances(#%d)', $account->id)); $return = []; - foreach($total as $key => $value) { + foreach ($total as $key => $value) { $return[$key] = $this->filterAccountBalance($value, $account, $convertToNative, $currency); } + Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id)); return $return; } - public function filterAccountBalance(array $set, Account $account, bool $convertToNative, ?TransactionCurrency $currency = null): array { - if(0 === count($set)) { + public function filterAccountBalance(array $set, Account $account, bool $convertToNative, ?TransactionCurrency $currency = null): array + { + Log::debug(sprintf('filterAccountBalance(#%d)', $account->id), $set); + if (0 === count($set)) { Log::debug(sprintf('Return empty array for account #%d', $account->id)); return []; } $defaultCurrency = app('amount')->getDefaultCurrency(); - if($convertToNative) { + if ($convertToNative) { if ($defaultCurrency->id === $currency?->id) { Log::debug(sprintf('Unset "native_balance" and "%s" for account #%d', $defaultCurrency->code, $account->id)); unset($set['native_balance'], $set[$defaultCurrency->code]); @@ -385,10 +425,10 @@ class Steam } } - if(!$convertToNative) { + if (!$convertToNative) { if (null === $currency) { Log::debug(sprintf('Unset native_balance and make defaultCurrency balance the balance for account #%d', $account->id)); - $set['balance'] = $set[$this->defaultCurrency->code] ?? '0'; + $set['balance'] = $set[$defaultCurrency->code] ?? '0'; unset($set['native_balance'], $set[$defaultCurrency->code]); } @@ -448,15 +488,15 @@ class Steam { $list = []; - $set = auth()->user()->transactions() - ->whereIn('transactions.account_id', $accounts) - ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) - ->get(['transactions.account_id', \DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line + $set = auth()->user()->transactions() + ->whereIn('transactions.account_id', $accounts) + ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) + ->get(['transactions.account_id', \DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line ; /** @var Transaction $entry */ foreach ($set as $entry) { - $date = new Carbon($entry->max_date, config('app.timezone')); + $date = new Carbon($entry->max_date, config('app.timezone')); $date->setTimezone(config('app.timezone')); $list[(int) $entry->account_id] = $date; } @@ -531,9 +571,9 @@ class Steam public function getSafeUrl(string $unknownUrl, string $safeUrl): string { // Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl)); - $returnUrl = $safeUrl; - $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); - $safeHost = parse_url($safeUrl, PHP_URL_HOST); + $returnUrl = $safeUrl; + $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); + $safeHost = parse_url($safeUrl, PHP_URL_HOST); if (null !== $unknownHost && $unknownHost === $safeHost) { $returnUrl = $unknownUrl; @@ -570,7 +610,7 @@ class Steam */ public function floatalize(string $value): string { - $value = strtoupper($value); + $value = strtoupper($value); if (!str_contains($value, 'E')) { return $value; }