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 ;
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 ;
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-02-15 15:44:21 +01:00
private array $accountIds ;
private array $accountTypeIds ;
private array $accountTypes ;
2025-05-08 20:22:01 +02:00
private Collection $collection ;
2025-02-15 15:44:21 +01:00
private array $currencies ;
2025-05-04 17:41:26 +02:00
private array $locations ;
2025-02-15 15:44:21 +01:00
private array $meta ;
2025-08-01 12:31:01 +02:00
private TransactionCurrency $primaryCurrency ;
2025-02-15 16:51:13 +01:00
private array $notes ;
2025-05-04 17:41:26 +02:00
private array $openingBalances ;
private User $user ;
private UserGroup $userGroup ;
2025-05-08 20:22:01 +02:00
private array $lastActivities ;
2025-08-03 10:22:12 +02:00
private ? Carbon $date = null ;
private bool $convertToPrimary = false ;
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 -> 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 ();
2024-07-28 07:47:54 +02:00
}
2025-05-27 16:57:36 +02:00
#[Override]
2025-08-03 10:22:12 +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 ;
$this -> collectAccountIds ();
$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-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-05-04 17:41:26 +02:00
private function collectAccountIds () : void
{
/** @var Account $account */
foreach ( $this -> collection as $account ) {
$this -> accountIds [] = ( int ) $account -> id ;
$this -> accountTypeIds [] = ( int ) $account -> account_type_id ;
}
$this -> accountIds = array_unique ( $this -> accountIds );
$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 ) {
$this -> accountTypes [( int ) $type -> id ] = $type -> type ;
}
}
2025-05-04 17:41:26 +02:00
private function collectMetaData () : void
2025-02-15 15:44:21 +01:00
{
2025-08-03 10:22:12 +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 -> accountIds )
-> 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 ) {
$this -> meta [( int ) $entry [ 'account_id' ]][ $entry [ 'name' ]] = ( string ) $entry [ 'data' ];
if ( 'currency_id' === $entry [ 'name' ]) {
$this -> currencies [( int ) $entry [ 'data' ]] = true ;
}
2025-02-15 15:44:21 +01:00
}
2025-08-03 10:22:12 +02:00
$currencies = TransactionCurrency :: whereIn ( 'id' , array_keys ( $this -> currencies )) -> get ();
2025-05-04 17:41:26 +02:00
foreach ( $currencies as $currency ) {
$this -> currencies [( int ) $currency -> id ] = $currency ;
}
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
{
$notes = Note :: query () -> whereIn ( 'noteable_id' , $this -> accountIds )
2025-08-03 10:22:12 +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 ) {
$this -> notes [( int ) $note [ 'noteable_id' ]] = ( string ) $note [ 'text' ];
}
Log :: debug ( sprintf ( 'Enrich with %d note(s)' , count ( $this -> notes )));
}
private function collectLocations () : void
{
$locations = Location :: query () -> whereIn ( 'locatable_id' , $this -> accountIds )
2025-08-03 10:22:12 +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 ) {
$this -> locations [( int ) $location [ 'locatable_id' ]]
= [
2025-08-03 10:22:12 +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-03 10:22:12 +02:00
-> setTypes ([ TransactionTypeEnum :: OPENING_BALANCE -> value ]);
$journals = $collector -> getExtractedJournals ();
2025-05-04 17:41:26 +02:00
foreach ( $journals as $journal ) {
$this -> openingBalances [( int ) $journal [ 'source_account_id' ]]
= [
2025-08-03 10:22:12 +02:00
'amount' => Steam :: negative ( $journal [ 'amount' ]),
'date' => $journal [ 'date' ],
];
2025-05-04 17:41:26 +02:00
$this -> openingBalances [( int ) $journal [ 'destination_account_id' ]]
= [
2025-08-03 10:22:12 +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-02-15 16:51:13 +01:00
$notes = $this -> notes ;
$openingBalances = $this -> openingBalances ;
$locations = $this -> locations ;
2025-05-11 12:59:56 +02:00
$lastActivities = $this -> lastActivities ;
2025-08-03 10:22:12 +02:00
$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 ;
2025-02-15 16:51:13 +01:00
$accountMeta = [
2025-08-03 10:22:12 +02:00
'currency' => null ,
'location' => [
2025-02-15 16:51:13 +01:00
'latitude' => null ,
'longitude' => null ,
'zoom_level' => null ,
],
2025-08-03 10:22:12 +02:00
'opening_balance_date' => null ,
2025-02-15 16:51:13 +01:00
];
2025-08-03 10:22:12 +02:00
if ( array_key_exists (( int ) $item -> id , $this -> meta )) {
foreach ( $this -> meta [( int ) $item -> id ] as $name => $value ) {
2025-02-15 16:51:13 +01:00
$accountMeta [ $name ] = $value ;
2025-02-15 15:44:21 +01:00
}
}
2025-02-15 16:51:13 +01:00
// also add currency, if present.
if ( array_key_exists ( 'currency_id' , $accountMeta )) {
$currencyId = ( int ) $accountMeta [ 'currency_id' ];
2025-08-03 10:22:12 +02:00
$accountMeta [ 'currency' ] = $this -> currencies [ $currencyId ];
2025-02-15 16:51:13 +01:00
}
// if notes, add notes.
if ( array_key_exists ( $item -> id , $notes )) {
$accountMeta [ 'notes' ] = $notes [ $item -> id ];
}
// if opening balance, add opening balance
if ( array_key_exists ( $item -> id , $openingBalances )) {
$accountMeta [ 'opening_balance_date' ] = $openingBalances [ $item -> id ][ 'date' ];
$accountMeta [ 'opening_balance_amount' ] = $openingBalances [ $item -> id ][ 'amount' ];
}
2025-08-03 10:22:12 +02:00
// 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
2025-02-15 16:51:13 +01:00
// if location, add location:
if ( array_key_exists ( $item -> id , $locations )) {
$accountMeta [ 'location' ] = $locations [ $item -> id ];
}
2025-05-08 20:22:01 +02:00
if ( array_key_exists ( $item -> id , $lastActivities )) {
$accountMeta [ 'last_activity' ] = $lastActivities [ $item -> id ];
}
2025-08-03 10:22:12 +02:00
$item -> meta = $accountMeta ;
2025-02-15 15:44:21 +01:00
return $item ;
});
}
2025-05-08 20:22:01 +02:00
private function collectLastActivities () : void
{
$this -> lastActivities = Steam :: getLastActivities ( $this -> accountIds );
}
2025-08-03 10:22:12 +02:00
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 ;
}
2024-05-10 06:43:18 +02:00
}