2024-05-10 06:43:18 +02:00
< ? php
2024-11-25 04:18:55 +01:00
2024-05-10 06:43:18 +02:00
/*
* AccountEnricher . php
* Copyright ( c ) 2024 james @ firefly - iii . org .
*
* This file is part of Firefly III ( https :// github . com / firefly - iii ) .
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see https :// www . gnu . org / licenses /.
*/
declare ( strict_types = 1 );
namespace FireflyIII\Support\JsonApi\Enrichments ;
2025-08-03 10:22:12 +02:00
use Carbon\Carbon ;
2025-02-15 16:51:13 +01:00
use FireflyIII\Enums\TransactionTypeEnum ;
2025-02-15 15:44:21 +01:00
use FireflyIII\Exceptions\FireflyException ;
2025-02-15 16:51:13 +01:00
use FireflyIII\Helpers\Collector\GroupCollectorInterface ;
2024-05-10 09:17:09 +02:00
use FireflyIII\Models\Account ;
2025-02-15 15:44:21 +01:00
use FireflyIII\Models\AccountMeta ;
2024-05-10 09:17:09 +02:00
use FireflyIII\Models\AccountType ;
2025-02-15 16:51:13 +01:00
use FireflyIII\Models\Location ;
use FireflyIII\Models\Note ;
2025-08-07 07:46:49 +02:00
use FireflyIII\Models\ObjectGroup ;
2024-07-31 20:19:17 +02:00
use FireflyIII\Models\TransactionCurrency ;
2025-02-15 15:44:21 +01:00
use FireflyIII\Models\UserGroup ;
2025-08-03 08:02:13 +02:00
use FireflyIII\Support\Facades\Amount ;
2025-02-15 16:51:13 +01:00
use FireflyIII\Support\Facades\Steam ;
2025-08-03 10:22:12 +02:00
use FireflyIII\Support\Http\Api\ExchangeRateConverter ;
2025-02-15 15:44:21 +01:00
use FireflyIII\User ;
2024-07-28 12:23:45 +02:00
use Illuminate\Database\Eloquent\Model ;
2024-05-10 06:43:18 +02:00
use Illuminate\Support\Collection ;
2025-08-07 07:46:49 +02:00
use Illuminate\Support\Facades\DB ;
2024-05-10 06:43:18 +02:00
use Illuminate\Support\Facades\Log ;
2025-05-27 16:57:36 +02:00
use Override ;
2024-05-10 06:43:18 +02:00
2024-07-26 18:50:41 +02:00
/**
* Class AccountEnrichment
*
* This class " enriches " accounts and adds data from other tables and models to each account model.
*/
2024-05-10 06:43:18 +02:00
class AccountEnrichment implements EnrichmentInterface
{
2025-08-07 20:04:36 +02:00
private array $ids = [];
private array $accountTypeIds = [];
private array $accountTypes = [];
2025-05-08 20:22:01 +02:00
private Collection $collection ;
2025-08-07 20:04:36 +02:00
private array $currencies = [];
private array $locations = [];
private array $meta = [];
2025-08-01 12:31:01 +02:00
private TransactionCurrency $primaryCurrency ;
2025-08-07 20:04:36 +02:00
private array $notes = [];
2025-08-07 19:09:25 +02:00
private array $openingBalances = [];
2025-05-04 17:41:26 +02:00
private User $user ;
private UserGroup $userGroup ;
2025-08-07 20:04:36 +02:00
private array $lastActivities = [];
private ? Carbon $date = null ;
2025-08-07 19:09:25 +02:00
private bool $convertToPrimary ;
2025-08-07 20:04:36 +02:00
private array $balances = [];
private array $objectGroups = [];
private array $mappedObjects = [];
2024-07-28 07:47:54 +02:00
2025-08-03 07:12:06 +02:00
/**
2025-08-03 08:12:19 +02:00
* TODO The account enricher must do conversion from and to the primary currency .
2025-08-03 07:12:06 +02:00
*/
2024-07-28 07:47:54 +02:00
public function __construct ()
{
2025-08-03 10:22:12 +02:00
$this -> primaryCurrency = Amount :: getPrimaryCurrency ();
$this -> convertToPrimary = Amount :: convertToPrimary ();
2024-07-28 07:47:54 +02:00
}
2025-05-27 16:57:36 +02:00
#[Override]
2025-08-03 16:45:49 +02:00
public function enrichSingle ( array | Model $model ) : Account | array
2024-12-22 08:43:12 +01:00
{
Log :: debug ( __METHOD__ );
$collection = new Collection ([ $model ]);
$collection = $this -> enrich ( $collection );
return $collection -> first ();
}
2025-05-27 16:57:36 +02:00
#[Override]
2024-07-26 18:50:41 +02:00
/**
* Do the actual enrichment .
*/
2024-05-13 05:10:16 +02:00
public function enrich ( Collection $collection ) : Collection
2024-05-10 06:43:18 +02:00
{
2024-07-26 18:50:41 +02:00
Log :: debug ( sprintf ( 'Now doing account enrichment for %d account(s)' , $collection -> count ()));
2025-02-15 15:44:21 +01:00
2024-07-26 18:50:41 +02:00
// prep local fields
2025-02-15 15:44:21 +01:00
$this -> collection = $collection ;
2025-08-06 20:15:02 +02:00
$this -> collectIds ();
2025-02-15 15:44:21 +01:00
$this -> getAccountTypes ();
2024-05-10 09:17:09 +02:00
$this -> collectMetaData ();
2025-02-16 19:45:31 +01:00
$this -> collectNotes ();
2025-05-08 20:22:01 +02:00
$this -> collectLastActivities ();
2025-02-15 16:51:13 +01:00
$this -> collectLocations ();
$this -> collectOpeningBalances ();
2025-08-07 07:46:49 +02:00
$this -> collectObjectGroups ();
2025-08-03 10:22:12 +02:00
$this -> collectBalances ();
2025-02-15 15:44:21 +01:00
$this -> appendCollectedData ();
2024-05-10 06:43:18 +02:00
return $this -> collection ;
}
2025-08-06 20:15:02 +02:00
private function collectIds () : void
2025-05-04 17:41:26 +02:00
{
/** @var Account $account */
foreach ( $this -> collection as $account ) {
2025-08-06 20:23:58 +02:00
$this -> ids [] = ( int ) $account -> id ;
$this -> accountTypeIds [] = ( int ) $account -> account_type_id ;
2025-05-04 17:41:26 +02:00
}
2025-08-06 20:15:02 +02:00
$this -> ids = array_unique ( $this -> ids );
2025-05-04 17:41:26 +02:00
$this -> accountTypeIds = array_unique ( $this -> accountTypeIds );
}
2025-02-15 15:44:21 +01:00
private function getAccountTypes () : void
{
$types = AccountType :: whereIn ( 'id' , $this -> accountTypeIds ) -> get ();
2025-02-17 04:11:50 +01:00
2025-02-15 15:44:21 +01:00
/** @var AccountType $type */
foreach ( $types as $type ) {
2025-08-06 20:23:58 +02:00
$this -> accountTypes [( int ) $type -> id ] = $type -> type ;
2025-02-15 15:44:21 +01:00
}
}
2025-05-04 17:41:26 +02:00
private function collectMetaData () : void
2025-02-15 15:44:21 +01:00
{
2025-08-07 20:04:36 +02:00
$set = AccountMeta :: whereIn ( 'name' , [ 'is_multi_currency' , 'include_net_worth' , 'currency_id' , 'account_role' , 'account_number' , 'BIC' , 'liability_direction' , 'interest' , 'interest_period' , 'current_debt' ])
-> whereIn ( 'account_id' , $this -> ids )
-> get ([ 'account_meta.id' , 'account_meta.account_id' , 'account_meta.name' , 'account_meta.data' ]) -> toArray ()
;
2025-05-04 17:41:26 +02:00
/** @var array $entry */
foreach ( $set as $entry ) {
2025-08-06 20:23:58 +02:00
$this -> meta [( int ) $entry [ 'account_id' ]][ $entry [ 'name' ]] = ( string ) $entry [ 'data' ];
2025-05-04 17:41:26 +02:00
if ( 'currency_id' === $entry [ 'name' ]) {
2025-08-06 20:23:58 +02:00
$this -> currencies [( int ) $entry [ 'data' ]] = true ;
2025-05-04 17:41:26 +02:00
}
2025-02-15 15:44:21 +01:00
}
2025-08-06 20:23:58 +02:00
if ( count ( $this -> currencies ) > 0 ) {
$currencies = TransactionCurrency :: whereIn ( 'id' , array_keys ( $this -> currencies )) -> get ();
foreach ( $currencies as $currency ) {
$this -> currencies [( int ) $currency -> id ] = $currency ;
}
2025-05-04 17:41:26 +02:00
}
2025-08-01 12:31:01 +02:00
$this -> currencies [ 0 ] = $this -> primaryCurrency ;
2025-05-04 17:41:26 +02:00
foreach ( $this -> currencies as $id => $currency ) {
if ( true === $currency ) {
throw new FireflyException ( sprintf ( 'Currency #%d not found.' , $id ));
}
}
}
private function collectNotes () : void
{
2025-08-06 20:15:02 +02:00
$notes = Note :: query () -> whereIn ( 'noteable_id' , $this -> ids )
2025-08-07 20:04:36 +02:00
-> whereNotNull ( 'notes.text' )
-> where ( 'notes.text' , '!=' , '' )
-> where ( 'noteable_type' , Account :: class ) -> get ([ 'notes.noteable_id' , 'notes.text' ]) -> toArray ()
;
2025-05-04 17:41:26 +02:00
foreach ( $notes as $note ) {
2025-08-06 20:23:58 +02:00
$this -> notes [( int ) $note [ 'noteable_id' ]] = ( string ) $note [ 'text' ];
2025-05-04 17:41:26 +02:00
}
Log :: debug ( sprintf ( 'Enrich with %d note(s)' , count ( $this -> notes )));
}
private function collectLocations () : void
{
2025-08-06 20:15:02 +02:00
$locations = Location :: query () -> whereIn ( 'locatable_id' , $this -> ids )
2025-08-07 20:04:36 +02:00
-> where ( 'locatable_type' , Account :: class ) -> get ([ 'locations.locatable_id' , 'locations.latitude' , 'locations.longitude' , 'locations.zoom_level' ]) -> toArray ()
;
2025-05-04 17:41:26 +02:00
foreach ( $locations as $location ) {
2025-08-06 20:23:58 +02:00
$this -> locations [( int ) $location [ 'locatable_id' ]]
2025-05-04 17:41:26 +02:00
= [
2025-08-07 20:04:36 +02:00
'latitude' => ( float ) $location [ 'latitude' ],
'longitude' => ( float ) $location [ 'longitude' ],
'zoom_level' => ( int ) $location [ 'zoom_level' ],
];
2025-05-04 17:41:26 +02:00
}
Log :: debug ( sprintf ( 'Enrich with %d locations(s)' , count ( $this -> locations )));
}
private function collectOpeningBalances () : void
{
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app ( GroupCollectorInterface :: class );
$collector
-> setUser ( $this -> user )
-> setUserGroup ( $this -> userGroup )
-> setAccounts ( $this -> collection )
-> withAccountInformation ()
2025-08-07 20:04:36 +02:00
-> setTypes ([ TransactionTypeEnum :: OPENING_BALANCE -> value ])
;
$journals = $collector -> getExtractedJournals ();
2025-05-04 17:41:26 +02:00
foreach ( $journals as $journal ) {
2025-08-06 20:23:58 +02:00
$this -> openingBalances [( int ) $journal [ 'source_account_id' ]]
2025-05-04 17:41:26 +02:00
= [
2025-08-07 20:04:36 +02:00
'amount' => Steam :: negative ( $journal [ 'amount' ]),
'date' => $journal [ 'date' ],
];
2025-08-06 20:23:58 +02:00
$this -> openingBalances [( int ) $journal [ 'destination_account_id' ]]
2025-05-04 17:41:26 +02:00
= [
2025-08-07 20:04:36 +02:00
'amount' => Steam :: positive ( $journal [ 'amount' ]),
'date' => $journal [ 'date' ],
];
2025-05-04 17:41:26 +02:00
}
}
public function setUserGroup ( UserGroup $userGroup ) : void
{
$this -> userGroup = $userGroup ;
}
public function setUser ( User $user ) : void
{
$this -> user = $user ;
$this -> userGroup = $user -> userGroup ;
2025-02-15 15:44:21 +01:00
}
private function appendCollectedData () : void
{
2025-08-06 20:23:58 +02:00
$this -> collection = $this -> collection -> map ( function ( Account $item ) {
$id = ( int ) $item -> id ;
$item -> full_account_type = $this -> accountTypes [( int ) $item -> account_type_id ] ? ? null ;
$meta = [
'currency' => null ,
'location' => [
2025-02-15 16:51:13 +01:00
'latitude' => null ,
'longitude' => null ,
'zoom_level' => null ,
],
2025-08-07 07:46:49 +02:00
'object_group_id' => null ,
'object_group_order' => null ,
'object_group_title' => null ,
2025-08-06 20:23:58 +02:00
'opening_balance_date' => null ,
2025-08-06 20:15:02 +02:00
'opening_balance_amount' => null ,
2025-08-06 20:23:58 +02:00
'account_number' => null ,
'notes' => $notes [ $id ] ? ? null ,
'last_activity' => $this -> lastActivities [ $id ] ? ? null ,
2025-02-15 16:51:13 +01:00
];
2025-08-06 20:15:02 +02:00
2025-08-07 07:46:49 +02:00
// add object group if available
if ( array_key_exists ( $id , $this -> mappedObjects )) {
$key = $this -> mappedObjects [ $id ];
$meta [ 'object_group_id' ] = $this -> objectGroups [ $key ][ 'id' ];
$meta [ 'object_group_title' ] = $this -> objectGroups [ $key ][ 'title' ];
$meta [ 'object_group_order' ] = $this -> objectGroups [ $key ][ 'order' ];
}
2025-08-06 20:15:02 +02:00
// if location, add location:
if ( array_key_exists ( $id , $this -> locations )) {
$meta [ 'location' ] = $this -> locations [ $id ];
}
if ( array_key_exists ( $id , $this -> meta )) {
foreach ( $this -> meta [ $id ] as $name => $value ) {
$meta [ $name ] = $value ;
2025-02-15 15:44:21 +01:00
}
}
2025-02-15 16:51:13 +01:00
// also add currency, if present.
2025-08-06 20:15:02 +02:00
if ( array_key_exists ( 'currency_id' , $meta )) {
2025-08-06 20:23:58 +02:00
$currencyId = ( int ) $meta [ 'currency_id' ];
2025-08-06 20:15:02 +02:00
$meta [ 'currency' ] = $this -> currencies [ $currencyId ];
2025-02-15 16:51:13 +01:00
}
2025-08-06 20:15:02 +02:00
if ( array_key_exists ( $id , $this -> openingBalances )) {
$meta [ 'opening_balance_date' ] = $this -> openingBalances [ $id ][ 'date' ];
$meta [ 'opening_balance_amount' ] = $this -> openingBalances [ $id ][ 'amount' ];
2025-02-15 16:51:13 +01:00
}
2025-08-03 10:22:12 +02:00
// add balances
// get currencies:
2025-08-07 20:04:36 +02:00
$currency = $this -> primaryCurrency ; // assume primary currency
2025-08-06 20:15:02 +02:00
if ( null !== $meta [ 'currency' ]) {
$currency = $meta [ 'currency' ];
2025-08-03 10:22:12 +02:00
}
// get the current balance:
2025-08-07 20:04:36 +02:00
$date = $this -> getDate ();
2025-08-06 20:39:55 +02:00
// $finalBalance = Steam::finalAccountBalance($item, $date, $this->primaryCurrency, $this->convertToPrimary);
2025-08-07 20:04:36 +02:00
$finalBalance = $this -> balances [ $id ];
2025-08-03 10:22:12 +02:00
Log :: debug ( sprintf ( 'Call finalAccountBalance(%s) with date/time "%s"' , var_export ( $this -> convertToPrimary , true ), $date -> toIso8601String ()), $finalBalance );
// collect current balances:
2025-08-07 20:04:36 +02:00
$currentBalance = Steam :: bcround ( $finalBalance [ $currency -> code ] ? ? '0' , $currency -> decimal_places );
$openingBalance = Steam :: bcround ( $meta [ 'opening_balance_amount' ] ? ? '0' , $currency -> decimal_places );
$virtualBalance = Steam :: bcround ( $account -> virtual_balance ? ? '0' , $currency -> decimal_places );
$debtAmount = $meta [ 'current_debt' ] ? ? null ;
2025-08-03 10:22:12 +02:00
// set some pc_ default values to NULL:
2025-08-07 20:04:36 +02:00
$pcCurrentBalance = null ;
$pcOpeningBalance = null ;
$pcVirtualBalance = null ;
$pcDebtAmount = null ;
2025-08-03 10:22:12 +02:00
// 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
2025-08-06 20:15:02 +02:00
if ( null === $meta [ 'opening_balance_date' ]) {
2025-08-03 10:22:12 +02:00
$openingBalance = null ;
$pcOpeningBalance = null ;
}
2025-08-07 20:04:36 +02:00
$meta [ 'balances' ] = [
2025-08-03 10:22:12 +02:00
'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
2025-08-07 20:04:36 +02:00
$item -> meta = $meta ;
2025-02-15 15:44:21 +01:00
return $item ;
});
}
2025-05-08 20:22:01 +02:00
private function collectLastActivities () : void
{
2025-08-06 20:15:02 +02:00
$this -> lastActivities = Steam :: getLastActivities ( $this -> ids );
2025-05-08 20:22:01 +02:00
}
2025-08-03 10:22:12 +02:00
2025-08-06 20:23:58 +02:00
private function collectBalances () : void
{
2025-08-07 05:52:56 +02:00
$this -> balances = Steam :: accountsBalancesOptimized ( $this -> collection , $this -> getDate (), $this -> primaryCurrency , $this -> convertToPrimary );
2025-08-06 20:15:02 +02:00
}
2025-08-03 10:22:12 +02:00
2025-08-07 07:46:49 +02:00
private function collectObjectGroups () : void
{
2025-08-07 20:04:36 +02:00
$set = DB :: table ( 'object_groupables' )
-> whereIn ( 'object_groupable_id' , $this -> ids )
-> where ( 'object_groupable_type' , Account :: class )
-> get ([ 'object_groupable_id' , 'object_group_id' ])
;
2025-08-07 07:46:49 +02:00
2025-08-07 20:04:36 +02:00
$ids = array_unique ( $set -> pluck ( 'object_group_id' ) -> toArray ());
2025-08-07 07:46:49 +02:00
foreach ( $set as $entry ) {
$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 ;
}
}
2025-08-03 10:22:12 +02:00
public function setDate ( ? Carbon $date ) : void
{
$this -> date = $date ;
}
public function getDate () : Carbon
{
if ( null === $this -> date ) {
return today ();
}
2025-08-03 16:45:49 +02:00
2025-08-03 10:22:12 +02:00
return $this -> date ;
}
2024-05-10 06:43:18 +02:00
}