2018-09-15 13:43:57 +02:00
< ? php
2024-11-25 04:18:55 +01:00
2018-09-15 13:43:57 +02:00
/**
* ConvertToTransfer . php
2020-02-16 13:57:05 +01:00
* Copyright ( c ) 2019 james @ firefly - iii . org
2018-09-15 13:43:57 +02:00
*
2019-10-02 06:37:26 +02:00
* This file is part of Firefly III ( https :// github . com / firefly - iii ) .
2018-09-15 13:43:57 +02:00
*
2019-10-02 06:37:26 +02:00
* 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 .
2018-09-15 13:43:57 +02:00
*
2019-10-02 06:37:26 +02:00
* This program is distributed in the hope that it will be useful ,
2018-09-15 13:43:57 +02:00
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2019-10-02 06:37:26 +02:00
* GNU Affero General Public License for more details .
2018-09-15 13:43:57 +02:00
*
2019-10-02 06:37:26 +02:00
* 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 />.
2018-09-15 13:43:57 +02:00
*/
declare ( strict_types = 1 );
namespace FireflyIII\TransactionRules\Actions ;
2021-04-05 22:12:57 +02:00
2025-01-03 09:05:19 +01:00
use FireflyIII\Enums\TransactionTypeEnum ;
2023-08-12 20:57:51 +02:00
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray ;
2023-08-13 15:01:12 +02:00
use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject ;
2022-10-02 06:23:31 +02:00
use FireflyIII\Events\TriggeredAuditLog ;
2023-05-27 06:36:24 +02:00
use FireflyIII\Exceptions\FireflyException ;
2018-09-15 13:43:57 +02:00
use FireflyIII\Models\Account ;
use FireflyIII\Models\RuleAction ;
2023-05-27 06:36:24 +02:00
use FireflyIII\Models\Transaction ;
2025-12-06 07:27:41 +01:00
use FireflyIII\Models\TransactionCurrency ;
2022-06-25 14:36:53 +02:00
use FireflyIII\Models\TransactionJournal ;
2018-09-15 13:43:57 +02:00
use FireflyIII\Models\TransactionType ;
use FireflyIII\Repositories\Account\AccountRepositoryInterface ;
2025-12-06 07:27:41 +01:00
use FireflyIII\Support\Facades\Steam ;
2025-02-23 12:47:04 +01:00
use Illuminate\Support\Facades\DB ;
2025-12-06 07:27:41 +01:00
use Illuminate\Support\Facades\Log ;
2018-09-15 13:43:57 +02:00
/**
* Class ConvertToTransfer
*/
class ConvertToTransfer implements ActionInterface
{
/**
* TriggerInterface constructor .
*/
2025-05-04 13:55:42 +02:00
public function __construct ( private readonly RuleAction $action ) {}
2018-09-15 13:43:57 +02:00
2020-08-23 07:42:14 +02:00
/**
2025-01-03 15:53:10 +01:00
* @ SuppressWarnings ( " PHPMD.ExcessiveMethodLength " )
* @ SuppressWarnings ( " PHPMD.NPathComplexity " )
2020-08-23 07:42:14 +02:00
*/
public function actOnArray ( array $journal ) : bool
{
2025-12-06 07:31:41 +01:00
$accountName = $this -> action -> getValue ( $journal );
2024-03-06 20:54:50 -05:00
2023-05-27 06:36:24 +02:00
// make object from array (so the data is fresh).
2023-12-20 19:35:52 +01:00
/** @var null|TransactionJournal $object */
2025-12-06 07:31:41 +01:00
$object = TransactionJournal :: where ( 'user_id' , $journal [ 'user_id' ]) -> find ( $journal [ 'transaction_journal_id' ]);
2023-05-27 06:36:24 +02:00
if ( null === $object ) {
2025-11-09 09:08:03 +01:00
Log :: error ( sprintf ( 'Cannot find journal #%d, cannot convert to transfer.' , $journal [ 'transaction_journal_id' ]));
2023-08-12 20:57:51 +02:00
event ( new RuleActionFailedOnArray ( $this -> action , $journal , trans ( 'rules.journal_not_found' )));
2023-12-20 19:35:52 +01:00
2023-05-27 06:36:24 +02:00
return false ;
}
2025-12-06 07:31:41 +01:00
$groupCount = TransactionJournal :: where ( 'transaction_group_id' , $journal [ 'transaction_group_id' ]) -> count ();
2022-10-02 06:23:31 +02:00
if ( $groupCount > 1 ) {
2025-11-09 09:08:03 +01:00
Log :: error ( sprintf ( 'Group #%d has more than one transaction in it, cannot convert to transfer.' , $journal [ 'transaction_group_id' ]));
2023-08-12 20:57:51 +02:00
event ( new RuleActionFailedOnArray ( $this -> action , $journal , trans ( 'rules.split_group' )));
2023-12-20 19:35:52 +01:00
2022-06-25 14:36:53 +02:00
return false ;
}
2025-12-06 07:31:41 +01:00
$type = $object -> transactionType -> type ;
$user = $object -> user ;
$journalId = $object -> id ;
2025-01-03 09:05:56 +01:00
if ( TransactionTypeEnum :: TRANSFER -> value === $type ) {
2025-12-06 07:27:41 +01:00
Log :: error ( sprintf ( 'Journal #%d is already a transfer so cannot be converted (rule #%d).' , $object -> id , $this -> action -> rule_id ));
// event(new RuleActionFailedOnArray($this->action, $journal, trans('rules.is_already_transfer')));
2020-08-23 07:42:14 +02:00
return false ;
}
2025-01-03 09:09:15 +01:00
if ( TransactionTypeEnum :: DEPOSIT -> value !== $type && TransactionTypeEnum :: WITHDRAWAL -> value !== $type ) {
2023-08-13 15:01:12 +02:00
event ( new RuleActionFailedOnArray ( $this -> action , $journal , trans ( 'rules.unsupported_transaction_type_transfer' , [ 'type' => $type ])));
2023-12-20 19:35:52 +01:00
2023-08-13 15:01:12 +02:00
return false ;
}
2020-08-23 07:42:14 +02:00
// find the asset account in the action value.
/** @var AccountRepositoryInterface $repository */
2025-12-06 07:31:41 +01:00
$repository = app ( AccountRepositoryInterface :: class );
2020-08-23 07:42:14 +02:00
$repository -> setUser ( $user );
2023-03-25 13:42:26 +01:00
$expectedType = null ;
2025-01-03 09:05:19 +01:00
if ( TransactionTypeEnum :: WITHDRAWAL -> value === $type ) {
2023-05-27 06:36:24 +02:00
$expectedType = $this -> getSourceType ( $journalId );
2023-03-25 13:42:26 +01:00
// Withdrawal? Replace destination with account with same type as source.
}
2025-01-03 09:09:15 +01:00
if ( TransactionTypeEnum :: DEPOSIT -> value === $type ) {
2023-05-27 06:36:24 +02:00
$expectedType = $this -> getDestinationType ( $journalId );
2023-03-25 13:42:26 +01:00
// Deposit? Replace source with account with same type as destination.
}
2025-12-06 07:31:41 +01:00
$opposing = $repository -> findByName ( $accountName , [ $expectedType ]);
2023-03-25 13:42:26 +01:00
if ( null === $opposing ) {
2025-12-06 07:27:41 +01:00
Log :: error ( sprintf ( 'Journal #%d cannot be converted because no valid %s account with name "%s" exists (rule #%d).' , $expectedType , $journalId , $accountName , $this -> action -> rule_id ));
2024-03-07 17:18:46 -05:00
event ( new RuleActionFailedOnArray ( $this -> action , $journal , trans ( 'rules.no_valid_opposing' , [ 'name' => $accountName ])));
2020-08-23 07:42:14 +02:00
return false ;
}
2023-05-27 06:36:24 +02:00
2025-01-03 09:05:19 +01:00
if ( TransactionTypeEnum :: WITHDRAWAL -> value === $type ) {
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Going to transform a withdrawal to a transfer.' );
2023-12-20 19:35:52 +01:00
2023-05-27 06:36:24 +02:00
try {
$res = $this -> convertWithdrawalArray ( $object , $opposing );
} catch ( FireflyException $e ) {
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Could not convert withdrawal to transfer.' );
Log :: error ( $e -> getMessage ());
2023-08-12 20:57:51 +02:00
event ( new RuleActionFailedOnArray ( $this -> action , $journal , trans ( 'rules.complex_error' )));
2023-12-20 19:35:52 +01:00
2023-05-27 06:36:24 +02:00
return false ;
}
2025-11-09 09:08:03 +01:00
if ( $res ) {
2025-01-03 09:05:56 +01:00
event ( new TriggeredAuditLog ( $this -> action -> rule , $object , 'update_transaction_type' , TransactionTypeEnum :: WITHDRAWAL -> value , TransactionTypeEnum :: TRANSFER -> value ));
2023-08-13 15:01:12 +02:00
}
2023-12-20 19:35:52 +01:00
2023-05-27 06:36:24 +02:00
return $res ;
2020-08-23 07:42:14 +02:00
}
2023-11-04 07:18:03 +01:00
// can only be a deposit at this point.
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Going to transform a deposit to a transfer.' );
2023-12-20 19:35:52 +01:00
2023-11-04 07:18:03 +01:00
try {
$res = $this -> convertDepositArray ( $object , $opposing );
} catch ( FireflyException $e ) {
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Could not convert deposit to transfer.' );
Log :: error ( $e -> getMessage ());
2023-11-04 07:18:03 +01:00
event ( new RuleActionFailedOnArray ( $this -> action , $journal , trans ( 'rules.complex_error' )));
2023-12-20 19:35:52 +01:00
2023-11-04 07:18:03 +01:00
return false ;
}
2025-11-09 09:08:03 +01:00
if ( $res ) {
2025-01-03 09:09:15 +01:00
event ( new TriggeredAuditLog ( $this -> action -> rule , $object , 'update_transaction_type' , TransactionTypeEnum :: DEPOSIT -> value , TransactionTypeEnum :: TRANSFER -> value ));
2020-08-23 07:42:14 +02:00
}
2023-12-20 19:35:52 +01:00
2023-11-04 07:18:03 +01:00
return $res ;
2020-08-23 07:42:14 +02:00
}
2023-06-21 12:34:58 +02:00
private function getSourceType ( int $journalId ) : string
{
2023-12-20 19:35:52 +01:00
/** @var null|TransactionJournal $journal */
2023-06-21 12:34:58 +02:00
$journal = TransactionJournal :: find ( $journalId );
if ( null === $journal ) {
2025-11-09 09:08:03 +01:00
Log :: error ( sprintf ( 'Journal #%d does not exist. Cannot convert to transfer.' , $journalId ));
2023-12-20 19:35:52 +01:00
2023-06-21 12:34:58 +02:00
return '' ;
}
2023-12-20 19:35:52 +01:00
2025-12-06 07:27:41 +01:00
return ( string ) $journal -> transactions () -> where ( 'amount' , '<' , 0 ) -> first () ? -> account ? -> accountType ? -> type ;
2023-06-21 12:34:58 +02:00
}
private function getDestinationType ( int $journalId ) : string
{
2023-12-20 19:35:52 +01:00
/** @var null|TransactionJournal $journal */
2023-06-21 12:34:58 +02:00
$journal = TransactionJournal :: find ( $journalId );
if ( null === $journal ) {
2025-11-09 09:08:03 +01:00
Log :: error ( sprintf ( 'Journal #%d does not exist. Cannot convert to transfer.' , $journalId ));
2023-12-20 19:35:52 +01:00
2023-06-21 12:34:58 +02:00
return '' ;
}
2023-12-20 19:35:52 +01:00
2025-12-06 07:27:41 +01:00
return ( string ) $journal -> transactions () -> where ( 'amount' , '>' , 0 ) -> first () ? -> account ? -> accountType ? -> type ;
2023-06-21 12:34:58 +02:00
}
/**
* A withdrawal is from Asset to Expense .
* We replace the Expense with another asset .
* So this replaces the destination
2021-03-23 06:42:26 +01:00
*
2023-05-27 06:36:24 +02:00
* @ throws FireflyException
2020-08-23 07:42:14 +02:00
*/
2023-06-21 12:34:58 +02:00
private function convertWithdrawalArray ( TransactionJournal $journal , Account $opposing ) : bool
2020-08-23 07:42:14 +02:00
{
2025-12-06 07:31:41 +01:00
$repository = app ( AccountRepositoryInterface :: class );
$sourceAccount = $this -> getSourceAccount ( $journal );
2025-12-06 07:27:41 +01:00
$repository -> setUser ( $sourceAccount -> user );
2023-11-05 19:41:37 +01:00
if ( $sourceAccount -> id === $opposing -> id ) {
2025-12-06 07:27:41 +01:00
Log :: error ( vsprintf ( 'Journal #%d has already has "%s" as a source asset. ConvertToTransfer failed. (rule #%d).' , [ $journal -> id , $opposing -> name , $this -> action -> rule_id ]));
2023-08-13 15:01:12 +02:00
event ( new RuleActionFailedOnObject ( $this -> action , $journal , trans ( 'rules.already_has_source_asset' , [ 'name' => $opposing -> name ])));
2021-03-23 06:42:26 +01:00
2020-08-23 07:42:14 +02:00
return false ;
}
2025-12-06 07:31:41 +01:00
2025-12-06 07:27:41 +01:00
/** @var Transaction $sourceTransaction */
2025-12-06 07:31:41 +01:00
$sourceTransaction = Transaction :: where ( 'transaction_journal_id' , '=' , $journal -> id ) -> where ( 'amount' , '<' , 0 ) -> first ();
2025-12-06 07:27:41 +01:00
/** @var Transaction $destTransaction */
2025-12-06 07:31:41 +01:00
$destTransaction = Transaction :: where ( 'transaction_journal_id' , '=' , $journal -> id ) -> where ( 'amount' , '>' , 0 ) -> first ();
2023-06-21 12:34:58 +02:00
// update destination transaction:
2025-12-06 07:27:41 +01:00
$destTransaction -> account_id = $opposing -> id ;
$destTransaction -> save ();
// check if the currencies are a match.
/** @var TransactionCurrency $sourceCurrency */
2025-12-06 07:31:41 +01:00
$sourceCurrency = $repository -> getAccountCurrency ( $sourceAccount );
2025-12-06 07:27:41 +01:00
/** @var TransactionCurrency $destCurrency */
2025-12-06 07:31:41 +01:00
$destCurrency = $repository -> getAccountCurrency ( $opposing );
2025-12-06 07:27:41 +01:00
// if the currencies do not match, need to be smart about the involved amounts:
if ( $sourceCurrency -> id !== $destCurrency -> id ) {
Log :: debug ( sprintf ( 'Accounts have different currencies. Source has %s, dest has %s' , $sourceCurrency -> code , $destCurrency -> code ));
2025-12-06 07:31:41 +01:00
$foreignAmount = '' === ( string ) $sourceTransaction -> foreign_amount ? $sourceTransaction -> amount : $sourceTransaction -> foreign_amount ;
2025-12-06 07:27:41 +01:00
Log :: debug ( sprintf ( 'Foreign amount: %s' , $foreignAmount ));
// source transaction: set the foreign currency ID and leave as is.
2025-12-06 07:31:41 +01:00
$sourceTransaction -> foreign_currency_id = $destCurrency -> id ;
$sourceTransaction -> foreign_amount = Steam :: negative ( $foreignAmount );
2025-12-06 07:27:41 +01:00
$sourceTransaction -> save ();
Log :: debug ( sprintf ( 'Set source transaction #%d foreign currency ID to #%d (amount: %s)' , $sourceTransaction -> id , $destCurrency -> id , $foreignAmount ));
// dest transaction: set reverse amounts and currency IDs from source transaction.
$destTransaction -> foreign_currency_id = $sourceCurrency -> transaction_currency_id ;
$destTransaction -> transaction_currency_id = $sourceTransaction -> foreign_currency_id ;
$destTransaction -> amount = Steam :: positive ( $foreignAmount );
$destTransaction -> foreign_amount = Steam :: positive ( $sourceTransaction -> amount );
$destTransaction -> save ();
Log :: debug ( sprintf ( 'Set dest transaction #%d to #%d %s and foreign #%d %s' , $destTransaction -> id , $destTransaction -> transaction_currency_id , $destTransaction -> amount , $destTransaction -> foreign_currency_id , $destTransaction -> foreign_amount ));
}
2020-08-23 07:42:14 +02:00
// change transaction type of journal:
2025-12-06 07:31:41 +01:00
$newType = TransactionType :: whereType ( TransactionTypeEnum :: TRANSFER -> value ) -> first ();
2020-08-23 07:42:14 +02:00
2025-12-06 07:27:41 +01:00
DB :: table ( 'transaction_journals' ) -> where ( 'id' , '=' , $journal -> id ) -> update ([ 'transaction_type_id' => $newType -> id , 'bill_id' => null ]);
2020-08-23 07:42:14 +02:00
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Converted withdrawal to transfer.' );
2020-08-23 07:42:14 +02:00
return true ;
}
/**
2023-06-21 12:34:58 +02:00
* @ throws FireflyException
*/
private function getSourceAccount ( TransactionJournal $journal ) : Account
{
2023-12-20 19:35:52 +01:00
/** @var null|Transaction $sourceTransaction */
2023-06-21 12:34:58 +02:00
$sourceTransaction = $journal -> transactions () -> where ( 'amount' , '<' , 0 ) -> first ();
if ( null === $sourceTransaction ) {
throw new FireflyException ( sprintf ( 'Cannot find source transaction for journal #%d' , $journal -> id ));
}
2023-12-20 19:35:52 +01:00
2023-06-21 12:34:58 +02:00
return $sourceTransaction -> account ;
}
/**
* A deposit is from Revenue to Asset .
* We replace the Revenue with another asset .
2021-03-23 06:42:26 +01:00
*
2023-05-27 06:36:24 +02:00
* @ throws FireflyException
2020-08-23 07:42:14 +02:00
*/
2023-06-21 12:34:58 +02:00
private function convertDepositArray ( TransactionJournal $journal , Account $opposing ) : bool
2020-08-23 07:42:14 +02:00
{
2023-06-21 12:34:58 +02:00
$destAccount = $this -> getDestinationAccount ( $journal );
2023-11-05 19:41:37 +01:00
if ( $destAccount -> id === $opposing -> id ) {
2025-11-09 09:08:03 +01:00
Log :: error (
2021-03-23 06:42:26 +01:00
vsprintf (
2023-06-21 12:34:58 +02:00
'Journal #%d has already has "%s" as a destination asset. ConvertToTransfer failed. (rule #%d).' ,
2023-05-27 06:36:24 +02:00
[ $journal -> id , $opposing -> name , $this -> action -> rule_id ]
2021-03-23 06:42:26 +01:00
)
);
2023-08-13 15:01:12 +02:00
event ( new RuleActionFailedOnObject ( $this -> action , $journal , trans ( 'rules.already_has_destination_asset' , [ 'name' => $opposing -> name ])));
2021-03-23 06:42:26 +01:00
2020-08-23 07:42:14 +02:00
return false ;
}
2023-06-21 12:34:58 +02:00
// update source transaction:
2025-02-23 12:47:04 +01:00
DB :: table ( 'transactions' )
2025-12-06 07:31:41 +01:00
-> where ( 'transaction_journal_id' , '=' , $journal -> id )
-> where ( 'amount' , '<' , 0 )
-> update ([ 'account_id' => $opposing -> id ])
;
2020-08-23 07:42:14 +02:00
// change transaction type of journal:
2025-12-06 07:31:41 +01:00
$newType = TransactionType :: whereType ( TransactionTypeEnum :: TRANSFER -> value ) -> first ();
2020-08-23 07:42:14 +02:00
2025-02-23 12:47:04 +01:00
DB :: table ( 'transaction_journals' )
2025-12-06 07:31:41 +01:00
-> where ( 'id' , '=' , $journal -> id )
-> update ([ 'transaction_type_id' => $newType -> id , 'bill_id' => null ])
;
2020-08-23 07:42:14 +02:00
2025-11-09 09:08:03 +01:00
Log :: debug ( 'Converted deposit to transfer.' );
2020-08-23 07:42:14 +02:00
return true ;
}
2023-03-25 13:42:26 +01:00
2023-05-27 06:36:24 +02:00
/**
* @ throws FireflyException
*/
private function getDestinationAccount ( TransactionJournal $journal ) : Account
{
2023-12-20 19:35:52 +01:00
/** @var null|Transaction $destAccount */
2023-05-27 06:36:24 +02:00
$destAccount = $journal -> transactions () -> where ( 'amount' , '>' , 0 ) -> first ();
if ( null === $destAccount ) {
throw new FireflyException ( sprintf ( 'Cannot find destination transaction for journal #%d' , $journal -> id ));
}
2023-12-20 19:35:52 +01:00
2023-05-27 06:36:24 +02:00
return $destAccount -> account ;
}
2018-12-31 07:48:23 +01:00
}