diff --git a/.ci/phpmd/composer.lock b/.ci/phpmd/composer.lock index 89f7e74b07..1fedc00e61 100644 --- a/.ci/phpmd/composer.lock +++ b/.ci/phpmd/composer.lock @@ -146,16 +146,16 @@ }, { "name": "pdepend/pdepend", - "version": "2.16.1", + "version": "2.16.2", "source": { "type": "git", "url": "https://github.com/pdepend/pdepend.git", - "reference": "66ceb05eaa8bf358574143c974b04463911bc700" + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/66ceb05eaa8bf358574143c974b04463911bc700", - "reference": "66ceb05eaa8bf358574143c974b04463911bc700", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", "shasum": "" }, "require": { @@ -197,7 +197,7 @@ ], "support": { "issues": "https://github.com/pdepend/pdepend/issues", - "source": "https://github.com/pdepend/pdepend/tree/2.16.1" + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" }, "funding": [ { @@ -205,7 +205,7 @@ "type": "tidelift" } ], - "time": "2023-12-10T18:38:19+00:00" + "time": "2023-12-17T18:09:59+00:00" }, { "name": "phpmd/phpmd", diff --git a/app/Api/V2/Controllers/Chart/BalanceController.php b/app/Api/V2/Controllers/Chart/BalanceController.php index 45caf1405c..b0ff9636ba 100644 --- a/app/Api/V2/Controllers/Chart/BalanceController.php +++ b/app/Api/V2/Controllers/Chart/BalanceController.php @@ -32,6 +32,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Http\Api\AccountBalanceGrouped; use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use Illuminate\Http\JsonResponse; @@ -71,19 +72,12 @@ class BalanceController extends Controller $end->endOfDay(); /** @var Collection $accounts */ $accounts = $params['accounts']; + /** @var string $preferredRange */ $preferredRange = $params['period']; - // set some formats, based on input parameters. - $format = app('navigation')->preferredCarbonFormatByPeriod($preferredRange); - // prepare for currency conversion and data collection: - $ids = $accounts->pluck('id')->toArray(); /** @var TransactionCurrency $default */ $default = app('amount')->getDefaultCurrency(); - $converter = new ExchangeRateConverter(); - $currencies = [$default->id => $default,]; // currency cache - $data = []; - $chartData = []; // get journals for entire period: /** @var GroupCollectorInterface $collector */ @@ -93,151 +87,16 @@ class BalanceController extends Controller $collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::RECONCILIATION, TransactionType::TRANSFER]); $journals = $collector->getExtractedJournals(); - // set array for default currency (even if unused later on) - $defaultCurrencyId = $default->id; - $data[$defaultCurrencyId] = [ - 'currency_id' => (string)$defaultCurrencyId, - 'currency_symbol' => $default->symbol, - 'currency_code' => $default->code, - 'currency_name' => $default->name, - 'currency_decimal_places' => $default->decimal_places, - 'native_id' => (string)$defaultCurrencyId, - 'native_symbol' => $default->symbol, - 'native_code' => $default->code, - 'native_name' => $default->name, - 'native_decimal_places' => $default->decimal_places, - ]; + $object = new AccountBalanceGrouped(); + $object->setPreferredRange($preferredRange); + $object->setDefault($default); + $object->setAccounts($accounts); + $object->setJournals($journals); + $object->setStart($start); + $object->setEnd($end); + $object->groupByCurrencyAndPeriod(); + $chartData = $object->convertToChartData(); - - // loop. group by currency and by period. - /** @var array $journal */ - foreach ($journals as $journal) { - // format the date according to the period - $period = $journal['date']->format($format); - - // collect (and cache) currency information for this journal. - $currencyId = (int)$journal['currency_id']; - $currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId); - $currencies[$currencyId] = $currency; // may just re-assign itself, don't mind. - - // set the array with monetary info, if it does not exist. - $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'], - // native currency info (could be the same) - 'native_id' => (string)$default->id, - 'native_code' => $default->code, - 'native_symbol' => $default->symbol, - 'native_decimal_places' => $default->decimal_places, - ]; - - // set the array (in monetary info) with spent/earned in this $period, if it does not exist. - $data[$currencyId][$period] ??= [ - 'period' => $period, - 'spent' => '0', - 'earned' => '0', - 'native_spent' => '0', - 'native_earned' => '0', - ]; - // is this journal's amount in- our outgoing? - $key = 'spent'; - $amount = app('steam')->negative($journal['amount']); - // deposit = incoming - // transfer or reconcile or opening balance, and these accounts are the destination. - if ( - TransactionType::DEPOSIT === $journal['transaction_type_type'] - || - - ( - ( - TransactionType::TRANSFER === $journal['transaction_type_type'] - || TransactionType::RECONCILIATION === $journal['transaction_type_type'] - || TransactionType::OPENING_BALANCE === $journal['transaction_type_type'] - ) - && in_array($journal['destination_account_id'], $ids, true) - ) - ) { - $key = 'earned'; - $amount = app('steam')->positive($journal['amount']); - } - // get conversion rate - $rate = $converter->getCurrencyRate($currency, $default, $journal['date']); - $amountConverted = bcmul($amount, $rate); - - // perhaps transaction already has the foreign amount in the native currency. - if ((int)$journal['foreign_currency_id'] === $default->id) { - $amountConverted = $journal['foreign_amount'] ?? '0'; - $amountConverted = 'earned' === $key ? app('steam')->positive($amountConverted) : app('steam')->negative($amountConverted); - } - - // add normal entry - $data[$currencyId][$period][$key] = bcadd($data[$currencyId][$period][$key], $amount); - - // add converted entry - $convertedKey = sprintf('native_%s', $key); - $data[$currencyId][$period][$convertedKey] = bcadd($data[$currencyId][$period][$convertedKey], $amountConverted); - } - - // loop this data, make chart bars for each currency: - /** @var array $currency */ - foreach ($data as $currency) { - // income and expense array prepped: - $income = [ - 'label' => 'earned', - 'currency_id' => (string)$currency['currency_id'], - 'currency_symbol' => $currency['currency_symbol'], - 'currency_code' => $currency['currency_code'], - 'currency_decimal_places' => $currency['currency_decimal_places'], - 'native_id' => (string)$currency['native_id'], - 'native_symbol' => $currency['native_symbol'], - 'native_code' => $currency['native_code'], - 'native_decimal_places' => $currency['native_decimal_places'], - 'start' => $start->toAtomString(), - 'end' => $end->toAtomString(), - 'period' => $preferredRange, - 'entries' => [], - 'native_entries' => [], - ]; - $expense = [ - 'label' => 'spent', - 'currency_id' => (string)$currency['currency_id'], - 'currency_symbol' => $currency['currency_symbol'], - 'currency_code' => $currency['currency_code'], - 'currency_decimal_places' => $currency['currency_decimal_places'], - 'native_id' => (string)$currency['native_id'], - 'native_symbol' => $currency['native_symbol'], - 'native_code' => $currency['native_code'], - 'native_decimal_places' => $currency['native_decimal_places'], - 'start' => $start->toAtomString(), - 'end' => $end->toAtomString(), - 'period' => $preferredRange, - 'entries' => [], - 'native_entries' => [], - - ]; - // loop all possible periods between $start and $end, and add them to the correct dataset. - $currentStart = clone $start; - while ($currentStart <= $end) { - $key = $currentStart->format($format); - $label = $currentStart->toAtomString(); - // normal entries - $income['entries'][$label] = app('steam')->bcround(($currency[$key]['earned'] ?? '0'), $currency['currency_decimal_places']); - $expense['entries'][$label] = app('steam')->bcround(($currency[$key]['spent'] ?? '0'), $currency['currency_decimal_places']); - - // converted entries - $income['native_entries'][$label] = app('steam')->bcround(($currency[$key]['native_earned'] ?? '0'), $currency['native_decimal_places']); - $expense['native_entries'][$label] = app('steam')->bcround(($currency[$key]['native_spent'] ?? '0'), $currency['native_decimal_places']); - - // next loop - $currentStart = app('navigation')->addPeriod($currentStart, $preferredRange, 0); - } - - $chartData[] = $income; - $chartData[] = $expense; - } return response()->json($this->clean($chartData)); } diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php new file mode 100644 index 0000000000..fe9b09e36f --- /dev/null +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -0,0 +1,280 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Http\Api; + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionType; +use Illuminate\Support\Collection; + +/** + * Class AccountBalanceGrouped + */ +class AccountBalanceGrouped +{ + private array $accountIds; + private Collection $accounts; + private string $carbonFormat; + private array $currencies = []; + private array $data = []; + private TransactionCurrency $default; + private Carbon $end; + private array $journals = []; + private string $preferredRange; + private Carbon $start; + + /** + * Convert the given input to a chart compatible array. + * + * @return array + */ + public function convertToChartData(): array + { + $chartData = []; + // loop2: loop this data, make chart bars for each currency: + /** @var array $currency */ + foreach ($this->data as $currency) { + // income and expense array prepped: + $income = [ + 'label' => 'earned', + 'currency_id' => (string)$currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_code' => $currency['currency_code'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'native_id' => (string)$currency['native_id'], + 'native_symbol' => $currency['native_symbol'], + 'native_code' => $currency['native_code'], + 'native_decimal_places' => $currency['native_decimal_places'], + 'start' => $this->start->toAtomString(), + 'end' => $this->end->toAtomString(), + 'period' => $this->preferredRange, + 'entries' => [], + 'native_entries' => [], + ]; + $expense = [ + 'label' => 'spent', + 'currency_id' => (string)$currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_code' => $currency['currency_code'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'native_id' => (string)$currency['native_id'], + 'native_symbol' => $currency['native_symbol'], + 'native_code' => $currency['native_code'], + 'native_decimal_places' => $currency['native_decimal_places'], + 'start' => $this->start->toAtomString(), + 'end' => $this->end->toAtomString(), + 'period' => $this->preferredRange, + 'entries' => [], + 'native_entries' => [], + + ]; + // loop all possible periods between $start and $end, and add them to the correct dataset. + $currentStart = clone $this->start; + while ($currentStart <= $this->end) { + $key = $currentStart->format($this->carbonFormat); + $label = $currentStart->toAtomString(); + // normal entries + $income['entries'][$label] = app('steam')->bcround(($currency[$key]['earned'] ?? '0'), $currency['currency_decimal_places']); + $expense['entries'][$label] = app('steam')->bcround(($currency[$key]['spent'] ?? '0'), $currency['currency_decimal_places']); + + // converted entries + $income['native_entries'][$label] = app('steam')->bcround(($currency[$key]['native_earned'] ?? '0'), $currency['native_decimal_places']); + $expense['native_entries'][$label] = app('steam')->bcround(($currency[$key]['native_spent'] ?? '0'), $currency['native_decimal_places']); + + // next loop + $currentStart = app('navigation')->addPeriod($currentStart, $this->preferredRange, 0); + } + + $chartData[] = $income; + $chartData[] = $expense; + } + return $chartData; + } + + /** + * Group the given journals by currency and then by period. + * If they are part of a set of accounts this basically means it's balance chart. + * + * @return void + */ + public function groupByCurrencyAndPeriod(): void + { + $converter = new ExchangeRateConverter(); + // loop. group by currency and by period. + /** @var array $journal */ + foreach ($this->journals as $journal) { + // format the date according to the period + $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $currency = $this->currencies[$currencyId] ?? TransactionCurrency::find($currencyId); + $this->currencies[$currencyId] = $currency; // may just re-assign itself, don't mind. + + // set the array with monetary info, if it does not exist. + $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'], + // native currency info (could be the same) + 'native_id' => (string)$this->default->id, + 'native_code' => $this->default->code, + 'native_symbol' => $this->default->symbol, + 'native_decimal_places' => $this->default->decimal_places, + ]; + + // set the array (in monetary info) with spent/earned in this $period, if it does not exist. + $this->data[$currencyId][$period] ??= [ + 'period' => $period, + 'spent' => '0', + 'earned' => '0', + 'native_spent' => '0', + 'native_earned' => '0', + ]; + // is this journal's amount in- our outgoing? + $key = 'spent'; + $amount = app('steam')->negative($journal['amount']); + // deposit = incoming + // transfer or reconcile or opening balance, and these accounts are the destination. + if ( + TransactionType::DEPOSIT === $journal['transaction_type_type'] + || + + ( + ( + TransactionType::TRANSFER === $journal['transaction_type_type'] + || TransactionType::RECONCILIATION === $journal['transaction_type_type'] + || TransactionType::OPENING_BALANCE === $journal['transaction_type_type'] + ) + && in_array($journal['destination_account_id'], $this->accountIds, true) + ) + ) { + $key = 'earned'; + $amount = app('steam')->positive($journal['amount']); + } + // get conversion rate + try { + $rate = $converter->getCurrencyRate($currency, $this->default, $journal['date']); + } catch (FireflyException $e) { + app('log')->error($e->getMessage()); + $rate = '1'; + } + $amountConverted = bcmul($amount, $rate); + + // perhaps transaction already has the foreign amount in the native currency. + if ((int)$journal['foreign_currency_id'] === $this->default->id) { + $amountConverted = $journal['foreign_amount'] ?? '0'; + $amountConverted = 'earned' === $key ? app('steam')->positive($amountConverted) : app('steam')->negative($amountConverted); + } + + // add normal entry + $this->data[$currencyId][$period][$key] = bcadd($this->data[$currencyId][$period][$key], $amount); + + // add converted entry + $convertedKey = sprintf('native_%s', $key); + $this->data[$currencyId][$period][$convertedKey] = bcadd($this->data[$currencyId][$period][$convertedKey], $amountConverted); + } + + } + + /** + * @param Collection $accounts + * + * @return void + */ + public function setAccounts(Collection $accounts): void + { + $this->accounts = $accounts; + $this->accountIds = $accounts->pluck('id')->toArray(); + } + + /** + * @param TransactionCurrency $default + * + * @return void + */ + public function setDefault(TransactionCurrency $default): void + { + $this->default = $default; + $defaultCurrencyId = $default->id; + $this->currencies = [$default->id => $default,]; // currency cache + $this->data[$defaultCurrencyId] = [ + 'currency_id' => (string)$defaultCurrencyId, + 'currency_symbol' => $default->symbol, + 'currency_code' => $default->code, + 'currency_name' => $default->name, + 'currency_decimal_places' => $default->decimal_places, + 'native_id' => (string)$defaultCurrencyId, + 'native_symbol' => $default->symbol, + 'native_code' => $default->code, + 'native_name' => $default->name, + 'native_decimal_places' => $default->decimal_places, + ]; + } + + /** + * @param Carbon $end + * + * @return void + */ + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + /** + * @param array $journals + * + * @return void + */ + public function setJournals(array $journals): void + { + $this->journals = $journals; + } + + /** + * @param string $preferredRange + * + * @return void + */ + public function setPreferredRange(string $preferredRange): void + { + $this->preferredRange = $preferredRange; + $this->carbonFormat = app('navigation')->preferredCarbonFormatByPeriod($preferredRange); + + } + + /** + * @param Carbon $start + * + * @return void + */ + public function setStart(Carbon $start): void + { + $this->start = $start; + } + + +}