diff --git a/app/Api/V1/Controllers/Models/Account/ShowController.php b/app/Api/V1/Controllers/Models/Account/ShowController.php index e838b87e7f..602a557724 100644 --- a/app/Api/V1/Controllers/Models/Account/ShowController.php +++ b/app/Api/V1/Controllers/Models/Account/ShowController.php @@ -96,6 +96,7 @@ class ShowController extends Controller /** @var User $admin */ $admin = auth()->user(); $enrichment = new AccountEnrichment(); + $enrichment->setDate($this->parameters->get('date')); $enrichment->setUser($admin); $accounts = $enrichment->enrich($accounts); @@ -130,6 +131,7 @@ class ShowController extends Controller /** @var User $admin */ $admin = auth()->user(); $enrichment = new AccountEnrichment(); + $enrichment->setDate($this->parameters->get('date')); $enrichment->setUser($admin); $account = $enrichment->enrichSingle($account); diff --git a/app/Api/V1/Controllers/Models/Account/StoreController.php b/app/Api/V1/Controllers/Models/Account/StoreController.php index e72b7650ea..bbda9b50ef 100644 --- a/app/Api/V1/Controllers/Models/Account/StoreController.php +++ b/app/Api/V1/Controllers/Models/Account/StoreController.php @@ -75,6 +75,7 @@ class StoreController extends Controller /** @var User $admin */ $admin = auth()->user(); $enrichment = new AccountEnrichment(); + $enrichment->setDate(null); $enrichment->setUser($admin); $account = $enrichment->enrichSingle($account); diff --git a/app/Api/V1/Controllers/Models/Account/UpdateController.php b/app/Api/V1/Controllers/Models/Account/UpdateController.php index f1653bf1e3..5118b1116d 100644 --- a/app/Api/V1/Controllers/Models/Account/UpdateController.php +++ b/app/Api/V1/Controllers/Models/Account/UpdateController.php @@ -80,6 +80,7 @@ class UpdateController extends Controller /** @var User $admin */ $admin = auth()->user(); $enrichment = new AccountEnrichment(); + $enrichment->setDate(null); $enrichment->setUser($admin); $account = $enrichment->enrichSingle($account); diff --git a/app/Api/V1/Controllers/Models/PiggyBank/ListController.php b/app/Api/V1/Controllers/Models/PiggyBank/ListController.php index da2a3d2420..5e7fe7decb 100644 --- a/app/Api/V1/Controllers/Models/PiggyBank/ListController.php +++ b/app/Api/V1/Controllers/Models/PiggyBank/ListController.php @@ -83,6 +83,7 @@ class ListController extends Controller /** @var User $admin */ $admin = auth()->user(); $enrichment = new AccountEnrichment(); + $enrichment->setDate($this->parameters->get('date')); $enrichment->setUser($admin); $accounts = $enrichment->enrich($accounts); diff --git a/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php b/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php index 3220893923..ed52de7657 100644 --- a/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php +++ b/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php @@ -107,6 +107,7 @@ class ListController extends Controller /** @var User $admin */ $admin = auth()->user(); $enrichment = new AccountEnrichment(); + $enrichment->setDate($this->parameters->get('date')); $enrichment->setUser($admin); $accounts = $enrichment->enrich($accounts); diff --git a/app/Api/V1/Controllers/Search/AccountController.php b/app/Api/V1/Controllers/Search/AccountController.php index 22f35be905..cfcad8642a 100644 --- a/app/Api/V1/Controllers/Search/AccountController.php +++ b/app/Api/V1/Controllers/Search/AccountController.php @@ -88,6 +88,7 @@ class AccountController extends Controller /** @var User $admin */ $admin = auth()->user(); $enrichment = new AccountEnrichment(); + $enrichment->setDate($this->parameters->get('date')); $enrichment->setUser($admin); $accounts = $enrichment->enrich($accounts); diff --git a/app/Generator/Webhook/StandardMessageGenerator.php b/app/Generator/Webhook/StandardMessageGenerator.php index d63d7930b3..110bd0709c 100644 --- a/app/Generator/Webhook/StandardMessageGenerator.php +++ b/app/Generator/Webhook/StandardMessageGenerator.php @@ -176,6 +176,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface /** @var TransactionGroup $model */ $accounts = $this->collectAccounts($model); $enrichment = new AccountEnrichment(); + $enrichment->setDate(null); $enrichment->setUser($model->user); $accounts = $enrichment->enrich($accounts); foreach ($accounts as $account) { diff --git a/app/Http/Controllers/PiggyBank/IndexController.php b/app/Http/Controllers/PiggyBank/IndexController.php index 283b2aca7d..9a07387545 100644 --- a/app/Http/Controllers/PiggyBank/IndexController.php +++ b/app/Http/Controllers/PiggyBank/IndexController.php @@ -148,6 +148,7 @@ class IndexController extends Controller // enrich each account. $enrichment = new AccountEnrichment(); $enrichment->setUser(auth()->user()); + $enrichment->setDate($end); $return = []; /** @var PiggyBank $piggy */ diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php index f34e740077..542662fda1 100644 --- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Support\JsonApi\Enrichments; +use Carbon\Carbon; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; @@ -36,6 +37,7 @@ use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\UserGroup; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Steam; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; @@ -62,26 +64,29 @@ class AccountEnrichment implements EnrichmentInterface private User $user; private UserGroup $userGroup; private array $lastActivities; + private ?Carbon $date = null; + private bool $convertToPrimary = false; /** * TODO The account enricher must do conversion from and to the primary currency. */ public function __construct() { - $this->accountIds = []; - $this->openingBalances = []; - $this->currencies = []; - $this->accountTypeIds = []; - $this->accountTypes = []; - $this->meta = []; - $this->notes = []; - $this->lastActivities = []; - $this->locations = []; - $this->primaryCurrency = Amount::getPrimaryCurrency(); + $this->accountIds = []; + $this->openingBalances = []; + $this->currencies = []; + $this->accountTypeIds = []; + $this->accountTypes = []; + $this->meta = []; + $this->notes = []; + $this->lastActivities = []; + $this->locations = []; + $this->primaryCurrency = Amount::getPrimaryCurrency(); + $this->convertToPrimary = Amount::convertToPrimary(); } #[Override] - public function enrichSingle(array|Model $model): Account|array + public function enrichSingle(array | Model $model): Account | array { Log::debug(__METHOD__); $collection = new Collection([$model]); @@ -107,6 +112,7 @@ class AccountEnrichment implements EnrichmentInterface $this->collectLastActivities(); $this->collectLocations(); $this->collectOpeningBalances(); + $this->collectBalances(); $this->appendCollectedData(); return $this->collection; @@ -135,10 +141,9 @@ class AccountEnrichment implements EnrichmentInterface private function collectMetaData(): void { - $set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt']) - ->whereIn('account_id', $this->accountIds) - ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray() - ; + $set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt']) + ->whereIn('account_id', $this->accountIds) + ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray(); /** @var array $entry */ foreach ($set as $entry) { @@ -147,7 +152,7 @@ class AccountEnrichment implements EnrichmentInterface $this->currencies[(int) $entry['data']] = true; } } - $currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get(); + $currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get(); foreach ($currencies as $currency) { $this->currencies[(int) $currency->id] = $currency; } @@ -162,10 +167,9 @@ class AccountEnrichment implements EnrichmentInterface private function collectNotes(): void { $notes = Note::query()->whereIn('noteable_id', $this->accountIds) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); foreach ($notes as $note) { $this->notes[(int) $note['noteable_id']] = (string) $note['text']; } @@ -175,15 +179,14 @@ class AccountEnrichment implements EnrichmentInterface private function collectLocations(): void { $locations = Location::query()->whereIn('locatable_id', $this->accountIds) - ->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray() - ; + ->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray(); foreach ($locations as $location) { $this->locations[(int) $location['locatable_id']] = [ - 'latitude' => (float) $location['latitude'], - 'longitude' => (float) $location['longitude'], - 'zoom_level' => (int) $location['zoom_level'], - ]; + 'latitude' => (float) $location['latitude'], + 'longitude' => (float) $location['longitude'], + 'zoom_level' => (int) $location['zoom_level'], + ]; } Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations))); } @@ -198,20 +201,19 @@ class AccountEnrichment implements EnrichmentInterface ->setUserGroup($this->userGroup) ->setAccounts($this->collection) ->withAccountInformation() - ->setTypes([TransactionTypeEnum::OPENING_BALANCE->value]) - ; - $journals = $collector->getExtractedJournals(); + ->setTypes([TransactionTypeEnum::OPENING_BALANCE->value]); + $journals = $collector->getExtractedJournals(); foreach ($journals as $journal) { $this->openingBalances[(int) $journal['source_account_id']] = [ - 'amount' => Steam::negative($journal['amount']), - 'date' => $journal['date'], - ]; + 'amount' => Steam::negative($journal['amount']), + 'date' => $journal['date'], + ]; $this->openingBalances[(int) $journal['destination_account_id']] = [ - 'amount' => Steam::positive($journal['amount']), - 'date' => $journal['date'], - ]; + 'amount' => Steam::positive($journal['amount']), + 'date' => $journal['date'], + ]; } } @@ -228,32 +230,30 @@ class AccountEnrichment implements EnrichmentInterface private function appendCollectedData(): void { - $accountTypes = $this->accountTypes; - $meta = $this->meta; - $currencies = $this->currencies; $notes = $this->notes; $openingBalances = $this->openingBalances; $locations = $this->locations; $lastActivities = $this->lastActivities; - $this->collection = $this->collection->map(function (Account $item) use ($accountTypes, $meta, $currencies, $notes, $openingBalances, $locations, $lastActivities) { - $item->full_account_type = $accountTypes[(int) $item->account_type_id] ?? null; + $this->collection = $this->collection->map(function (Account $item) use ($notes, $openingBalances, $locations, $lastActivities) { + $item->full_account_type = $this->accountTypes[(int) $item->account_type_id] ?? null; $accountMeta = [ - 'currency' => null, - 'location' => [ + 'currency' => null, + 'location' => [ 'latitude' => null, 'longitude' => null, 'zoom_level' => null, ], + 'opening_balance_date' => null, ]; - if (array_key_exists((int) $item->id, $meta)) { - foreach ($meta[(int) $item->id] as $name => $value) { + if (array_key_exists((int) $item->id, $this->meta)) { + foreach ($this->meta[(int) $item->id] as $name => $value) { $accountMeta[$name] = $value; } } // also add currency, if present. if (array_key_exists('currency_id', $accountMeta)) { $currencyId = (int) $accountMeta['currency_id']; - $accountMeta['currency'] = $currencies[$currencyId]; + $accountMeta['currency'] = $this->currencies[$currencyId]; } // if notes, add notes. @@ -266,6 +266,64 @@ class AccountEnrichment implements EnrichmentInterface $accountMeta['opening_balance_amount'] = $openingBalances[$item->id]['amount']; } + // add balances + // get currencies: + $currency = $this->primaryCurrency; // assume primary currency + if (null !== $accountMeta['currency']) { + $currency = $accountMeta['currency']; + } + + // get the current balance: + $date = $this->getDate(); + $finalBalance = Steam::finalAccountBalance($item, $date, $this->primaryCurrency, $this->convertToPrimary); + Log::debug(sprintf('Call finalAccountBalance(%s) with date/time "%s"', var_export($this->convertToPrimary, true), $date->toIso8601String()), $finalBalance); + + // collect current balances: + $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); + $openingBalance = Steam::bcround($accountMeta['opening_balance_amount'] ?? '0', $currency->decimal_places); + $virtualBalance = Steam::bcround($account->virtual_balance ?? '0', $currency->decimal_places); + $debtAmount = $accountMeta['current_debt'] ?? null; + + // set some pc_ default values to NULL: + $pcCurrentBalance = null; + $pcOpeningBalance = null; + $pcVirtualBalance = null; + $pcDebtAmount = null; + + // convert to primary currency if needed: + if ($this->convertToPrimary && $currency->id !== $this->primaryCurrency->id) { + Log::debug(sprintf('Convert to primary, from %s to %s', $currency->code, $this->primaryCurrency->code)); + $converter = new ExchangeRateConverter(); + $pcCurrentBalance = $converter->convert($currency, $this->primaryCurrency, $date, $currentBalance); + $pcOpeningBalance = $converter->convert($currency, $this->primaryCurrency, $date, $openingBalance); + $pcVirtualBalance = $converter->convert($currency, $this->primaryCurrency, $date, $virtualBalance); + $pcDebtAmount = null === $debtAmount ? null : $converter->convert($currency, $this->primaryCurrency, $date, $debtAmount); + } + if ($this->convertToPrimary && $currency->id === $this->primaryCurrency->id) { + $pcCurrentBalance = $currentBalance; + $pcOpeningBalance = $openingBalance; + $pcVirtualBalance = $virtualBalance; + $pcDebtAmount = $debtAmount; + } + + // set opening balance(s) to NULL if the date is null + if (null === $accountMeta['opening_balance_date']) { + $openingBalance = null; + $pcOpeningBalance = null; + } + $accountMeta['balances'] = [ + 'current_balance' => $currentBalance, + 'pc_current_balance' => $pcCurrentBalance, + 'opening_balance' => $openingBalance, + 'pc_opening_balance' => $pcOpeningBalance, + 'virtual_balance' => $virtualBalance, + 'pc_virtual_balance' => $pcVirtualBalance, + 'debt_amount' => $debtAmount, + 'pc_debt_amount' => $pcDebtAmount, + ]; + // end add balances + + // if location, add location: if (array_key_exists($item->id, $locations)) { $accountMeta['location'] = $locations[$item->id]; @@ -273,7 +331,7 @@ class AccountEnrichment implements EnrichmentInterface if (array_key_exists($item->id, $lastActivities)) { $accountMeta['last_activity'] = $lastActivities[$item->id]; } - $item->meta = $accountMeta; + $item->meta = $accountMeta; return $item; }); @@ -283,4 +341,21 @@ class AccountEnrichment implements EnrichmentInterface { $this->lastActivities = Steam::getLastActivities($this->accountIds); } + + private function collectBalances(): void {} + + public function setDate(?Carbon $date): void + { + $this->date = $date; + } + + public function getDate(): Carbon + { + if (null === $this->date) { + return today(); + } + return $this->date; + } + + } diff --git a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php index 6077ac05c8..a034cf3590 100644 --- a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php +++ b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php @@ -61,11 +61,11 @@ class SubscriptionEnrichment implements EnrichmentInterface $paidDates = $this->paidDates; $payDates = $this->payDates; $this->collection = $this->collection->map(function (Bill $item) use ($notes, $objectGroups, $paidDates, $payDates) { - $id = (int)$item->id; - $currency = $item->transactionCurrency; - $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); + $id = (int) $item->id; + $currency = $item->transactionCurrency; + $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); - $meta = [ + $meta = [ 'notes' => null, 'object_group_id' => null, 'object_group_title' => null, @@ -76,20 +76,20 @@ class SubscriptionEnrichment implements EnrichmentInterface 'nem' => $nem, 'nem_diff' => $this->getNextExpectedMatchDiff($nem, $payDates[$id] ?? []), ]; - $amounts = [ - 'amount_min' => Steam::bcround($item->amount_min, $currency->decimal_places), - 'amount_max' => Steam::bcround($item->amount_max, $currency->decimal_places), - 'average' => Steam::bcround(bcdiv(bcadd($item->amount_min, $item->amount_max), '2'), $currency->decimal_places), + $amounts = [ + 'amount_min' => Steam::bcround($item->amount_min, $currency->decimal_places), + 'amount_max' => Steam::bcround($item->amount_max, $currency->decimal_places), + 'average' => Steam::bcround(bcdiv(bcadd($item->amount_min, $item->amount_max), '2'), $currency->decimal_places), 'pc_amount_min' => null, 'pc_amount_max' => null, 'pc_average' => null, ]; - if($this->convertToPrimary && $currency->id === $this->primaryCurrency->id) { + if ($this->convertToPrimary && $currency->id === $this->primaryCurrency->id) { $amounts['pc_amount_min'] = $amounts['amount_min']; $amounts['pc_amount_max'] = $amounts['amount_max']; $amounts['pc_average'] = $amounts['average']; } - if($this->convertToPrimary && $currency->id !== $this->primaryCurrency->id) { + if ($this->convertToPrimary && $currency->id !== $this->primaryCurrency->id) { $amounts['pc_amount_min'] = Steam::bcround($item->native_amount_min, $this->primaryCurrency->decimal_places); $amounts['pc_amount_max'] = Steam::bcround($item->native_amount_max, $this->primaryCurrency->decimal_places); $amounts['pc_average'] = Steam::bcround(bcdiv(bcadd($item->native_amount_min, $item->native_amount_max), '2'), $this->primaryCurrency->decimal_places); @@ -117,7 +117,7 @@ class SubscriptionEnrichment implements EnrichmentInterface return $collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection([$model]); @@ -129,12 +129,11 @@ class SubscriptionEnrichment implements EnrichmentInterface private function collectNotes(): void { $notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); foreach ($notes as $note) { - $this->notes[(int)$note['noteable_id']] = (string)$note['text']; + $this->notes[(int) $note['noteable_id']] = (string) $note['text']; } Log::debug(sprintf('Enrich with %d note(s)', count($this->notes))); } @@ -154,30 +153,29 @@ class SubscriptionEnrichment implements EnrichmentInterface { /** @var Bill $bill */ foreach ($this->collection as $bill) { - $this->subscriptionIds[] = (int)$bill->id; + $this->subscriptionIds[] = (int) $bill->id; } $this->subscriptionIds = array_unique($this->subscriptionIds); } private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->subscriptionIds) - ->where('object_groupable_type', Bill::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->subscriptionIds) + ->where('object_groupable_type', Bill::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { - $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; + $this->mappedObjects[(int) $entry->object_groupable_id] = (int) $entry->object_group_id; } $groups = ObjectGroup::whereIn('id', $ids)->get(['id', 'title', 'order'])->toArray(); foreach ($groups as $group) { - $group['id'] = (int)$group['id']; - $group['order'] = (int)$group['order']; - $this->objectGroups[(int)$group['id']] = $group; + $group['id'] = (int) $group['id']; + $group['order'] = (int) $group['order']; + $this->objectGroups[(int) $group['id']] = $group; } } @@ -195,13 +193,13 @@ class SubscriptionEnrichment implements EnrichmentInterface // 2023-07-18 this particular date is used to search for the last paid date. // 2023-07-18 the cloned $searchDate is used to grab the correct transactions. /** @var Carbon $start */ - $start = clone $this->start; - $searchStart = clone $start; + $start = clone $this->start; + $searchStart = clone $start; $start->subDay(); /** @var Carbon $end */ - $end = clone $this->end; - $searchEnd = clone $end; + $end = clone $this->end; + $searchEnd = clone $end; // move the search dates to the start of the day. $searchStart->startOfDay(); @@ -210,13 +208,13 @@ class SubscriptionEnrichment implements EnrichmentInterface Log::debug(sprintf('Search parameters are: start: %s, end: %s', $searchStart->format('Y-m-d H:i:s'), $searchEnd->format('Y-m-d H:i:s'))); // Get from database when bills were paid. - $set = $this->user->transactionJournals() - ->whereIn('bill_id', $this->subscriptionIds) - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') - ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') - ->where('transactions.amount', '>', 0) - ->before($searchEnd)->after($searchStart)->get( + $set = $this->user->transactionJournals() + ->whereIn('bill_id', $this->subscriptionIds) + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') + ->where('transactions.amount', '>', 0) + ->before($searchEnd)->after($searchStart)->get( [ 'transaction_journals.id', 'transaction_journals.date', @@ -224,58 +222,79 @@ class SubscriptionEnrichment implements EnrichmentInterface 'transaction_journals.transaction_group_id', 'transactions.transaction_currency_id', 'currency.code AS transaction_currency_code', + 'currency.symbol AS transaction_currency_symbol', 'currency.decimal_places AS transaction_currency_decimal_places', 'transactions.foreign_currency_id', 'foreign_currency.code AS foreign_currency_code', + 'foreign_currency.symbol AS foreign_currency_symbol', 'foreign_currency.decimal_places AS foreign_currency_decimal_places', 'transactions.amount', 'transactions.foreign_amount', ] - ) - ; + ); Log::debug(sprintf('Count %d entries in set', $set->count())); // for each bill, do a loop. - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); /** @var Bill $subscription */ foreach ($this->collection as $subscription) { // Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date. Log::debug(sprintf('Grab last paid date from function, return %s if it comes up with nothing.', $start->format('Y-m-d'))); - $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); + $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); Log::debug(sprintf('Result of lastPaidDate is %s', $lastPaidDate->format('Y-m-d'))); // At this point the "next match" is exactly after the last time the bill was paid. - $result = []; - $filtered = $set->filter(function (TransactionJournal $journal) use ($subscription) { + $result = []; + $filtered = $set->filter(function (TransactionJournal $journal) use ($subscription) { return (int) $journal->bill_id === (int) $subscription->id; }); foreach ($filtered as $entry) { - $array = [ - 'transaction_group_id' => (string)$entry->transaction_group_id, - 'transaction_journal_id' => (string)$entry->id, - 'date' => $entry->date->toAtomString(), - 'date_object' => $entry->date, - 'bill_id' => $entry->bill_id, - 'currency_id' => $entry->transaction_currency_id, - 'currency_code' => $entry->transaction_currency_code, - 'currency_decimal_places' => $entry->transaction_currency_decimal_places, - 'amount' => Steam::bcround($entry->amount, $entry->transaction_currency_decimal_places), + $array = [ + 'transaction_group_id' => (string) $entry->transaction_group_id, + 'transaction_journal_id' => (string) $entry->id, + 'date' => $entry->date->toAtomString(), + 'date_object' => $entry->date, + 'subscription_id' => (string) $entry->bill_id, + 'currency_id' => (string) $entry->transaction_currency_id, + 'currency_code' => $entry->transaction_currency_code, + 'currency_symbol' => $entry->transaction_currency_symbol, + 'currency_decimal_places' => $entry->transaction_currency_decimal_places, + 'primary_currency_id' => (string) $this->primaryCurrency->id, + 'primary_currency_code' => $this->primaryCurrency->code, + 'primary_currency_symbol' => $this->primaryCurrency->symbol, + 'primary_currency_decimal_places' => $this->primaryCurrency->decimal_places, + 'amount' => Steam::bcround($entry->amount, $entry->transaction_currency_decimal_places), + 'pc_amount' => null, + 'foreign_amount' => null, + 'pc_foreign_amount' => null, + ]; if (null !== $entry->foreign_amount && null !== $entry->foreign_currency_code) { - $array['foreign_currency_id'] = $entry->foreign_currency_id; + $array['foreign_currency_id'] = (string) $entry->foreign_currency_id; $array['foreign_currency_code'] = $entry->foreign_currency_code; + $array['foreign_currency_symbol'] = $entry->foreign_currency_symbol; $array['foreign_currency_decimal_places'] = $entry->foreign_currency_decimal_places; $array['foreign_amount'] = Steam::bcround($entry->foreign_amount, $entry->foreign_currency_decimal_places); } - if ($this->convertToPrimary) { - $array['amount'] = $converter->convert($entry->transactionCurrency, $this->primaryCurrency, $entry->date, $entry->amount); - $array['currency_id'] = $this->primaryCurrency->id; - $array['currency_code'] = $this->primaryCurrency->code; - $array['currency_decimal_places'] = $this->primaryCurrency->decimal_places; - + // convert to primary, but is already primary. + if ($this->convertToPrimary && (int) $entry->transaction_currency_id === $this->primaryCurrency->id) { + $array['pc_amount'] = $array['amount']; + } + // convert to primary, but is NOT already primary. + if ($this->convertToPrimary && (int) $entry->transaction_currency_id !== $this->primaryCurrency->id) { + $array['pc_amount'] = $converter->convert($entry->transactionCurrency, $this->primaryCurrency, $entry->date, $entry->amount); + } + // convert to primary, but foreign is already primary. + if ($this->convertToPrimary && (int) $entry->foreign_currency_id === $this->primaryCurrency->id) { + $array['pc_foreign_amount'] = $array['foreign_amount']; + } + // convert to primary, but foreign is NOT already primary. + if ($this->convertToPrimary && null !== $entry->foreign_currency_id && (int) $entry->foreign_currency_id !== $this->primaryCurrency->id) { + // TODO this is very database intensive. + $foreignCurrency = TransactionCurrency::find($entry->foreign_currency_id); + $array['pc_foreign_amount'] = $converter->convert($foreignCurrency, $this->primaryCurrency, $entry->date, $entry->amount); } - $result[] = $array; } $this->paidDates[(int) $subscription->id] = $result; @@ -306,7 +325,7 @@ class SubscriptionEnrichment implements EnrichmentInterface return $default; } - $latest = $filtered->first()->date; + $latest = $filtered->first()->date; /** @var TransactionJournal $journal */ foreach ($filtered as $journal) { @@ -352,12 +371,12 @@ class SubscriptionEnrichment implements EnrichmentInterface /** @var Bill $subscription */ foreach ($this->collection as $subscription) { - $id = (int)$subscription->id; - $lastPaidDate = $this->getLastPaidDate($paidDates[$id] ?? []); - $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); - $payDatesFormatted = []; + $id = (int) $subscription->id; + $lastPaidDate = $this->getLastPaidDate($paidDates[$id] ?? []); + $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); + $payDatesFormatted = []; foreach ($payDates as $string) { - $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); + $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); if (!$date instanceof Carbon) { $date = today(config('app.timezone')); } @@ -387,7 +406,7 @@ class SubscriptionEnrichment implements EnrichmentInterface if (!$nemDate instanceof Carbon) { $nemDate = today(config('app.timezone')); } - $nem = $nemDate; + $nem = $nemDate; // nullify again when it's outside the current view range. if ( @@ -416,7 +435,7 @@ class SubscriptionEnrichment implements EnrichmentInterface $current = $payDates[0] ?? null; if (null !== $current && !$nem->isToday()) { - $temp2 = Carbon::parse($current, config('app.timezone')); + $temp2 = Carbon::parse($current, config('app.timezone')); if (!$temp2 instanceof Carbon) { $temp2 = today(config('app.timezone')); } diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index e3131e661e..40fe6acf7e 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -30,9 +30,6 @@ use FireflyIII\Models\Account; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Facades\Amount; -use FireflyIII\Support\Facades\Steam; -use FireflyIII\Support\Http\Api\ExchangeRateConverter; -use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\ParameterBag; /** @@ -63,17 +60,19 @@ class AccountTransformer extends AbstractTransformer public function transform(Account $account): array { if (null === $account->meta) { - $account->meta = []; + $account->meta = [ + 'currency' => null, + ]; } // get account type: - $accountType = (string)config(sprintf('firefly.shortNamesByFullName.%s', $account->full_account_type)); - $liabilityType = (string)config(sprintf('firefly.shortLiabilityNameByFullName.%s', $account->full_account_type)); + $accountType = (string) config(sprintf('firefly.shortNamesByFullName.%s', $account->full_account_type)); + $liabilityType = (string) config(sprintf('firefly.shortLiabilityNameByFullName.%s', $account->full_account_type)); $liabilityType = '' === $liabilityType ? null : strtolower($liabilityType); $liabilityDirection = $account->meta['liability_direction'] ?? null; $accountRole = $this->getAccountRole($account, $accountType); - $hasCurrencySettings = array_key_exists('currency_id', $account->meta) && (int)$account->meta['currency_id'] > 0; - $includeNetWorth = 1 === (int)($account->meta['include_net_worth'] ?? 0); + $hasCurrencySettings = null !== $account->meta['currency']; + $includeNetWorth = 1 === (int) ($account->meta['include_net_worth'] ?? 0); $longitude = $account->meta['location']['longitude'] ?? null; $latitude = $account->meta['location']['latitude'] ?? null; $zoomLevel = $account->meta['location']['zoom_level'] ?? null; @@ -83,6 +82,12 @@ class AccountTransformer extends AbstractTransformer $date = $this->getDate(); $date->endOfDay(); + // get primary currency as fallback: + $currency = $this->primary; // assume primary currency + if ($hasCurrencySettings) { + $currency = $account->meta['currency']; + } + // no order for some accounts: if (!in_array(strtolower($accountType), ['liability', 'liabilities', 'asset'], true)) { $order = null; @@ -90,56 +95,11 @@ class AccountTransformer extends AbstractTransformer // get some listed information from the account meta-data: [$creditCardType, $monthlyPaymentDate] = $this->getCCInfo($account, $accountRole, $accountType); - [$openingBalance, $openingBalanceDate] = $this->getOpeningBalance($account, $accountType); + $openingBalanceDate = $this->getOpeningBalance($account, $accountType); [$interest, $interestPeriod] = $this->getInterest($account, $accountType); - // get currencies: - $currency = $this->primary; // assume primary currency - if ($hasCurrencySettings) { - $currency = $account->meta['currency']; - } - - // get the current balance: - $finalBalance = Steam::finalAccountBalance($account, $date, $this->primary, $this->convertToPrimary); - Log::debug(sprintf('Call finalAccountBalance(%s) with date/time "%s"', var_export($this->convertToPrimary, true), $date->toIso8601String()), $finalBalance); - - // collect current balances: - $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); - $openingBalance = Steam::bcround($openingBalance ?? '0', $currency->decimal_places); - $virtualBalance = Steam::bcround($account->virtual_balance ?? '0', $currency->decimal_places); - $debtAmount = $account->meta['current_debt'] ?? null; - - // TODO this currency conversion must not be happening here. - // set some pc_ default values to NULL: - $pcCurrentBalance = null; - $pcOpeningBalance = null; - $pcVirtualBalance = null; - $pcDebtAmount = null; - - // convert to primary currency if needed: - if ($this->convertToPrimary && $currency->id !== $this->primary->id) { - Log::debug(sprintf('Convert to primary, from %s to %s', $currency->code, $this->primary->code)); - $converter = new ExchangeRateConverter(); - $pcCurrentBalance = $converter->convert($currency, $this->primary, $date, $currentBalance); - $pcOpeningBalance = $converter->convert($currency, $this->primary, $date, $openingBalance); - $pcVirtualBalance = $converter->convert($currency, $this->primary, $date, $virtualBalance); - $pcDebtAmount = null === $debtAmount ? null : $converter->convert($currency, $this->primary, $date, $debtAmount); - } - if ($this->convertToPrimary && $currency->id === $this->primary->id) { - $pcCurrentBalance = $currentBalance; - $pcOpeningBalance = $openingBalance; - $pcVirtualBalance = $virtualBalance; - $pcDebtAmount = $debtAmount; - } - - // set opening balance(s) to NULL if the date is null - if (null === $openingBalanceDate) { - $openingBalance = null; - $pcOpeningBalance = null; - } - return [ - 'id' => (string)$account->id, + 'id' => (string) $account->id, 'created_at' => $account->created_at->toAtomString(), 'updated_at' => $account->updated_at->toAtomString(), 'active' => $account->active, @@ -152,27 +112,27 @@ class AccountTransformer extends AbstractTransformer 'object_has_currency_setting' => $hasCurrencySettings, // currency is object specific or primary, already determined above. - 'currency_id' => (string)$currency['id'], + 'currency_id' => (string) $currency['id'], 'currency_code' => $currency['code'], 'currency_symbol' => $currency['symbol'], 'currency_decimal_places' => $currency['decimal_places'], - 'primary_currency_id' => (string)$this->primary->id, + 'primary_currency_id' => (string) $this->primary->id, 'primary_currency_code' => $this->primary->code, 'primary_currency_symbol' => $this->primary->symbol, 'primary_currency_decimal_places' => $this->primary->decimal_places, // balances, structured for 6.3.0. - 'current_balance' => $currentBalance, - 'pc_current_balance' => $pcCurrentBalance, + 'current_balance' => $account->meta['balances']['current_balance'], + 'pc_current_balance' => $account->meta['balances']['pc_current_balance'], - 'opening_balance' => $openingBalance, - 'pc_opening_balance' => $pcOpeningBalance, + 'opening_balance' => $account->meta['balances']['opening_balance'], + 'pc_opening_balance' => $account->meta['balances']['pc_opening_balance'], - 'virtual_balance' => $virtualBalance, - 'pc_virtual_balance' => $pcVirtualBalance, + 'virtual_balance' => $account->meta['balances']['virtual_balance'], + 'pc_virtual_balance' => $account->meta['balances']['pc_virtual_balance'], - 'debt_amount' => $debtAmount, - 'pc_debt_amount' => $pcDebtAmount, + 'debt_amount' => $account->meta['balances']['debt_amount'], + 'pc_debt_amount' => $account->meta['balances']['pc_debt_amount'], 'current_balance_date' => $date->toAtomString(), 'notes' => $account->meta['notes'] ?? null, @@ -203,7 +163,7 @@ class AccountTransformer extends AbstractTransformer private function getAccountRole(Account $account, string $accountType): ?string { $accountRole = $account->meta['account_role'] ?? null; - if ('asset' !== $accountType || '' === (string)$accountRole) { + if ('asset' !== $accountType || '' === (string) $accountRole) { return null; } @@ -239,7 +199,7 @@ class AccountTransformer extends AbstractTransformer } $monthlyPaymentDate = $object->toAtomString(); } - if (10 !== strlen((string)$monthlyPaymentDate)) { + if (10 !== strlen((string) $monthlyPaymentDate)) { $monthlyPaymentDate = Carbon::parse($monthlyPaymentDate, config('app.timezone'))->toAtomString(); } } @@ -247,15 +207,10 @@ class AccountTransformer extends AbstractTransformer return [$creditCardType, $monthlyPaymentDate]; } - private function getOpeningBalance(Account $account, string $accountType): array + private function getOpeningBalance(Account $account, string $accountType): ?string { - $openingBalance = null; $openingBalanceDate = null; -// $pcOpeningBalance = null; if (in_array($accountType, ['asset', 'liabilities'], true)) { - // grab from meta. - $openingBalance = $account->meta['opening_balance_amount'] ?? null; - $pcOpeningBalance = null; $openingBalanceDate = $account->meta['opening_balance_date'] ?? null; } if (null !== $openingBalanceDate) { @@ -265,15 +220,9 @@ class AccountTransformer extends AbstractTransformer } $openingBalanceDate = $object->toAtomString(); - // NOW do conversion. -// if ($this->convertToPrimary && null !== $account->meta['currency']) { -// $converter = new ExchangeRateConverter(); -// $pcOpeningBalance = $converter->convert($account->meta['currency'], $this->primary, $object, $openingBalance); -// } - } - return [$openingBalance, $openingBalanceDate]; + return $openingBalanceDate; } private function getInterest(Account $account, string $accountType): array