2021-04-10 07:59:54 +02:00
< ? php
2021-08-10 19:31:55 +02:00
2021-04-10 07:59:54 +02:00
/*
* CreditRecalculateService . php
* Copyright ( c ) 2021 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 />.
*/
2021-08-10 19:31:55 +02:00
declare ( strict_types = 1 );
2021-04-10 07:59:54 +02:00
namespace FireflyIII\Services\Internal\Support ;
2024-12-27 07:36:30 +01:00
use FireflyIII\Enums\TransactionTypeEnum ;
2021-04-10 07:59:54 +02:00
use FireflyIII\Exceptions\FireflyException ;
2021-04-10 17:26:36 +02:00
use FireflyIII\Factory\AccountMetaFactory ;
2021-04-10 07:59:54 +02:00
use FireflyIII\Models\Account ;
use FireflyIII\Models\Transaction ;
2023-12-22 09:09:58 +01:00
use FireflyIII\Models\TransactionCurrency ;
2021-04-10 07:59:54 +02:00
use FireflyIII\Models\TransactionGroup ;
use FireflyIII\Models\TransactionJournal ;
use FireflyIII\Repositories\Account\AccountRepositoryInterface ;
2025-12-17 08:47:31 +01:00
use FireflyIII\Support\Facades\Steam ;
2026-01-25 09:02:47 +01:00
use Illuminate\Support\Collection ;
2026-01-23 15:09:50 +01:00
use Illuminate\Support\Facades\Log ;
2021-04-10 07:59:54 +02:00
2023-07-04 13:29:19 +02:00
/**
* Class CreditRecalculateService
*/
2021-04-10 07:59:54 +02:00
class CreditRecalculateService
{
2026-01-25 10:55:27 +01:00
private ? Account $account = null ;
private ? TransactionGroup $group = null ;
private Collection $journals ;
2026-02-04 16:16:27 +01:00
private Collection $accounts ;
2021-04-10 17:26:36 +02:00
private AccountRepositoryInterface $repository ;
2026-01-25 10:55:27 +01:00
private array $work = [];
2026-01-25 09:02:47 +01:00
public function __construct ()
{
$this -> journals = new Collection ();
2026-02-04 16:16:27 +01:00
$this -> accounts = new Collection ();
2026-01-25 09:02:47 +01:00
}
2021-04-10 07:59:54 +02:00
public function recalculate () : void
{
if ( true !== config ( 'firefly.feature_flags.handle_debts' )) {
return ;
}
2025-05-27 17:06:15 +02:00
if ( $this -> group instanceof TransactionGroup && ! $this -> account instanceof Account ) {
2021-04-10 17:26:36 +02:00
$this -> processGroup ();
}
2025-05-27 17:06:15 +02:00
if ( $this -> account instanceof Account && ! $this -> group instanceof TransactionGroup ) {
2021-04-10 17:26:36 +02:00
// work based on account.
$this -> processAccount ();
}
2026-02-04 16:22:26 +01:00
if ( $this -> accounts -> count () > 0 ) {
2026-02-04 16:16:27 +01:00
$this -> processAccounts ();
}
2026-01-25 09:02:47 +01:00
if ( $this -> journals -> count () > 0 ) {
$this -> processJournals ();
}
2022-11-04 05:11:05 +01:00
if ( 0 === count ( $this -> work )) {
2026-01-25 09:02:47 +01:00
Log :: debug ( 'No work found for recalculate() to do.' );
2026-01-25 10:55:27 +01:00
2021-04-10 17:26:36 +02:00
return ;
2021-04-10 07:59:54 +02:00
}
2021-04-26 07:29:39 +02:00
$this -> processWork ();
2021-04-10 07:59:54 +02:00
}
2026-02-06 13:55:17 +01:00
public function setAccount ( ? Account $account ) : void
{
$this -> account = $account ;
}
public function setAccounts ( Collection $accounts ) : void
{
$this -> accounts = $accounts ;
}
public function setGroup ( TransactionGroup $group ) : void
{
$this -> group = $group ;
}
public function setJournals ( Collection $journals ) : void
{
$this -> journals = $journals ;
}
2026-01-25 10:22:02 +01:00
private function collectFromJournals ( Collection $transactionJournals ) : void
2023-05-29 13:56:55 +02:00
{
2026-01-25 10:22:02 +01:00
Log :: debug ( 'Now in collectFromJournals()' );
$valid = config ( 'firefly.valid_liabilities' );
2026-01-25 10:55:27 +01:00
$accounts = Account :: leftJoin ( 'transactions' , 'transactions.account_id' , 'accounts.id' )
2026-01-25 10:22:02 +01:00
-> leftJoin ( 'transaction_journals' , 'transaction_journals.id' , 'transactions.transaction_journal_id' )
-> leftJoin ( 'account_types' , 'account_types.id' , 'accounts.account_type_id' )
-> whereIn ( 'transaction_journals.id' , $transactionJournals -> pluck ( 'id' ) -> toArray ())
-> whereIn ( 'account_types.type' , $valid )
2026-01-25 10:55:27 +01:00
-> get ([ 'accounts.*' ])
;
2026-01-25 10:22:02 +01:00
if ( $accounts -> count () > 0 ) {
Log :: debug ( sprintf ( 'Found %d account(s) to process.' , $accounts -> count ()));
foreach ( $accounts as $account ) {
$this -> work [] = $account ;
2023-06-21 12:34:58 +02:00
}
}
2021-04-10 07:59:54 +02:00
}
/**
* @ throws FireflyException
*/
2021-04-10 17:26:36 +02:00
private function findByJournal ( TransactionJournal $journal ) : void
2021-04-10 07:59:54 +02:00
{
2021-04-10 17:26:36 +02:00
$source = $this -> getSourceAccount ( $journal );
$destination = $this -> getDestinationAccount ( $journal );
// destination or source must be liability.
2026-01-25 10:55:27 +01:00
$valid = config ( 'firefly.valid_liabilities' );
2022-10-30 14:24:37 +01:00
if ( in_array ( $destination -> accountType -> type , $valid , true )) {
2021-04-10 17:26:36 +02:00
$this -> work [] = $destination ;
}
2022-10-30 14:24:37 +01:00
if ( in_array ( $source -> accountType -> type , $valid , true )) {
2021-04-10 17:26:36 +02:00
$this -> work [] = $source ;
}
2021-04-10 07:59:54 +02:00
}
2023-06-21 12:34:58 +02:00
/**
2021-04-10 07:59:54 +02:00
* @ throws FireflyException
*/
2021-04-10 17:26:36 +02:00
private function getAccountByDirection ( TransactionJournal $journal , string $direction ) : Account
2021-04-10 07:59:54 +02:00
{
2023-12-20 19:35:52 +01:00
/** @var null|Transaction $transaction */
2026-01-25 10:55:27 +01:00
$transaction = $journal -> transactions () -> where ( 'amount' , $direction , '0' ) -> first ();
2021-04-10 07:59:54 +02:00
if ( null === $transaction ) {
throw new FireflyException ( sprintf ( 'Cannot find "%s"-transaction of journal #%d' , $direction , $journal -> id ));
}
2023-12-20 19:35:52 +01:00
/** @var null|Account $foundAccount */
2021-05-24 08:06:56 +02:00
$foundAccount = $transaction -> account ;
if ( null === $foundAccount ) {
2021-04-10 07:59:54 +02:00
throw new FireflyException ( sprintf ( 'Cannot find "%s"-account of transaction #%d of journal #%d' , $direction , $transaction -> id , $journal -> id ));
}
2021-05-24 08:06:56 +02:00
return $foundAccount ;
2021-04-10 07:59:54 +02:00
}
2026-02-06 13:55:17 +01:00
private function getAmountToUse ( Transaction $transaction , TransactionCurrency $accountCurrency , ? TransactionCurrency $foreignCurrency ) : string
{
$usedAmount = $transaction -> amount ;
// Log::debug(sprintf('Amount of transaction is %s', \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2)));
if ( $foreignCurrency instanceof TransactionCurrency && $foreignCurrency -> id === $accountCurrency -> id ) {
$usedAmount = $transaction -> foreign_amount ;
// Log::debug(sprintf('Overruled by foreign amount. Amount of transaction is now %s', \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2)));
}
return $usedAmount ;
}
2021-04-10 17:26:36 +02:00
/**
* @ throws FireflyException
*/
private function getDestinationAccount ( TransactionJournal $journal ) : Account
{
return $this -> getAccountByDirection ( $journal , '>' );
}
2026-02-06 13:55:17 +01:00
/**
* @ throws FireflyException
*/
private function getSourceAccount ( TransactionJournal $journal ) : Account
2023-05-29 13:56:55 +02:00
{
2026-02-06 13:55:17 +01:00
return $this -> getAccountByDirection ( $journal , '<' );
2023-05-29 13:56:55 +02:00
}
2026-02-06 13:55:17 +01:00
/**
* case 4
* it ' s a deposit into this liability ( from revenue account ) .
* if it ' s a credit ( " I am owed " ) this increases the amount due .
* because the person is having to pay more money .
*/
private function isDepositIn ( string $amount , string $transactionType ) : bool
2021-04-10 17:26:36 +02:00
{
2026-02-06 13:55:17 +01:00
return TransactionTypeEnum :: DEPOSIT -> value === $transactionType && 1 === bccomp ( $amount , '0' );
2021-04-10 17:26:36 +02:00
}
2026-02-06 13:55:17 +01:00
/**
* it ' s a deposit out of this liability ( to asset ) .
*
* case 3
* if it ' s a credit ( " I am owed " ) this decreases the amount due .
* because the person is paying us back .
*
* case 7
* if it ' s a debit ( " I owe " ) this increases the amount due .
* because we are borrowing more money .
*/
private function isDepositOut ( string $amount , string $transactionType ) : bool
2021-04-10 17:26:36 +02:00
{
2026-02-06 13:55:17 +01:00
return TransactionTypeEnum :: DEPOSIT -> value === $transactionType && - 1 === bccomp ( $amount , '0' );
}
2023-08-06 07:03:39 +02:00
2026-02-06 13:55:17 +01:00
/**
* case 5 : transfer into loan ( from other loan ) .
* if it ' s a credit ( " I am owed " ) this increases the amount due ,
* because the person has to pay more back .
*
* case 8 : transfer into loan ( from other loan ) .
* if it ' s a debit ( " I owe " ) this decreases the amount due .
* because the person has to pay more back .
*/
private function isTransferIn ( string $amount , string $transactionType ) : bool
{
return TransactionTypeEnum :: TRANSFER -> value === $transactionType && 1 === bccomp ( $amount , '0' );
}
2023-06-21 12:34:58 +02:00
2026-02-06 13:55:17 +01:00
/**
* it ' s a transfer out of loan ( from other loan )
*
* case 9
* if it ' s a debit ( " I owe " ) this decreases the amount due .
* because we remove money from the amount left to owe
*/
private function isTransferOut ( string $amount , string $transactionType ) : bool
{
return TransactionTypeEnum :: TRANSFER -> value === $transactionType && - 1 === bccomp ( $amount , '0' );
}
2023-06-21 12:34:58 +02:00
2026-02-06 13:55:17 +01:00
/**
* It ' s a withdrawal into this liability ( from asset ) .
*
* Case 1 = credit
* if it ' s a credit ( " I am owed " ), this increases the amount due ,
* because we ' re lending person X more money
*
* Case 6 = debit
* if it ' s a debit ( " I owe this amount " ), this decreases the amount due ,
* because we ' re paying off the debt
*/
private function isWithdrawalIn ( string $amount , string $transactionType ) : bool
{
return TransactionTypeEnum :: WITHDRAWAL -> value === $transactionType && 1 === bccomp ( $amount , '0' );
}
2023-08-06 07:03:39 +02:00
2026-02-06 13:55:17 +01:00
/**
* it ' s a withdrawal away from this liability ( into expense account ) .
*
* Case 2
* if it ' s a credit ( " I am owed " ), this decreases the amount due ,
* because we ' re sending money away from the loan ( like loan forgiveness )
*
* Case 8
* if it ' s a debit ( " I owe this amount " ) this increase the amount due .
* because we are paying interest .
*/
private function isWithdrawalOut ( string $amount , string $transactionType ) : bool
{
return TransactionTypeEnum :: WITHDRAWAL -> value === $transactionType && - 1 === bccomp ( $amount , '0' );
}
2023-12-20 19:35:52 +01:00
2026-02-06 13:55:17 +01:00
private function processAccount () : void
{
$valid = config ( 'firefly.valid_liabilities' );
if ( in_array ( $this -> account -> accountType -> type , $valid , true )) {
$this -> work [] = $this -> account ;
2021-09-18 10:26:12 +02:00
}
2021-04-10 17:26:36 +02:00
}
2026-02-06 13:55:17 +01:00
private function processAccounts () : void
2023-10-04 19:14:47 +02:00
{
2026-02-06 13:55:17 +01:00
$valid = config ( 'firefly.valid_liabilities' );
2023-12-20 19:35:52 +01:00
2026-02-06 13:55:17 +01:00
/** @var Account $account */
foreach ( $this -> accounts as $account ) {
if ( in_array ( $account -> accountType -> type , $valid , true )) {
$this -> work [] = $account ;
2023-10-04 19:14:47 +02:00
}
}
2026-02-06 13:55:17 +01:00
}
2026-01-23 15:09:50 +01:00
2026-02-06 13:55:17 +01:00
private function processGroup () : void
{
$this -> collectFromJournals ( $this -> group -> transactionJournals );
}
private function processJournals () : void
{
$this -> collectFromJournals ( $this -> journals );
2023-10-04 19:14:47 +02:00
}
2023-12-22 17:28:42 +01:00
/**
2023-12-22 20:12:38 +01:00
* A complex and long method , but rarely used luckily .
*
2025-01-03 15:53:10 +01:00
* @ SuppressWarnings ( " PHPMD.ExcessiveMethodLength " )
* @ SuppressWarnings ( " PHPMD.NPathComplexity " )
* @ SuppressWarnings ( " PHPMD.CyclomaticComplexity " )
2023-12-22 17:28:42 +01:00
*/
2023-01-15 06:58:09 +01:00
private function processTransaction ( Account $account , string $direction , Transaction $transaction , string $leftOfDebt ) : string
2021-04-11 06:41:21 +02:00
{
2026-01-25 10:55:27 +01:00
$journal = $transaction -> transactionJournal ;
2024-10-27 10:49:55 +01:00
// here be null pointers.
if ( null === $journal ) {
2024-12-27 07:36:30 +01:00
Log :: warning ( sprintf ( 'Transaction #%d has no journal.' , $transaction -> id ));
2024-10-28 04:15:04 +01:00
2024-10-27 10:49:55 +01:00
return $leftOfDebt ;
}
2022-12-24 05:48:04 +01:00
$foreignCurrency = $transaction -> foreignCurrency ;
$accountCurrency = $this -> repository -> getAccountCurrency ( $account );
$type = $journal -> transactionType -> type ;
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Left of debt is: %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2)));
2023-10-14 14:52:21 +02:00
2021-05-15 09:16:54 +02:00
if ( '' === $direction ) {
2024-12-27 12:43:57 +01:00
// Log::warning('Direction is empty, so do nothing.');
2023-12-20 19:35:52 +01:00
2023-01-15 06:58:09 +01:00
return $leftOfDebt ;
2021-05-15 09:16:54 +02:00
}
2024-12-27 07:36:30 +01:00
if ( TransactionTypeEnum :: LIABILITY_CREDIT -> value === $type || TransactionTypeEnum :: OPENING_BALANCE -> value === $type ) {
2024-12-31 08:17:35 +01:00
// Log::warning(sprintf('Transaction type is "%s", so do nothing.', $type));
2023-12-20 19:35:52 +01:00
2023-01-15 06:58:09 +01:00
return $leftOfDebt ;
}
2024-12-27 12:43:57 +01:00
// Log::debug(sprintf('Liability direction is "%s"', $direction));
2023-01-15 06:58:09 +01:00
2022-12-24 05:48:04 +01:00
// amount to use depends on the currency:
2026-01-25 10:55:27 +01:00
$usedAmount = $this -> getAmountToUse ( $transaction , $accountCurrency , $foreignCurrency );
$isSameAccount = $account -> id === $transaction -> account_id ;
$isDebit = 'debit' === $direction ;
$isCredit = 'credit' === $direction ;
2022-12-24 05:48:04 +01:00
2023-12-22 09:09:58 +01:00
if ( $isSameAccount && $isCredit && $this -> isWithdrawalIn ( $usedAmount , $type )) { // case 1
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcadd ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2023-01-15 06:58:09 +01:00
}
2023-12-22 09:09:58 +01:00
if ( $isSameAccount && $isCredit && $this -> isWithdrawalOut ( $usedAmount , $type )) { // case 2
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcsub ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2021-04-26 07:29:39 +02:00
}
2021-05-15 09:16:54 +02:00
2023-12-22 09:09:58 +01:00
if ( $isSameAccount && $isCredit && $this -> isDepositOut ( $usedAmount , $type )) { // case 3
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcsub ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2021-04-26 07:29:39 +02:00
}
2023-12-22 09:09:58 +01:00
if ( $isSameAccount && $isCredit && $this -> isDepositIn ( $usedAmount , $type )) { // case 4
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcadd ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2023-08-06 07:03:39 +02:00
}
2023-12-22 09:09:58 +01:00
if ( $isSameAccount && $isCredit && $this -> isTransferIn ( $usedAmount , $type )) { // case 5
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcadd ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2023-01-15 07:52:05 +01:00
}
2023-12-22 09:09:58 +01:00
if ( $isSameAccount && $isDebit && $this -> isWithdrawalIn ( $usedAmount , $type )) { // case 6
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcsub ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2023-09-18 17:43:20 +02:00
}
2023-12-22 17:28:42 +01:00
if ( $isSameAccount && $isDebit && $this -> isDepositOut ( $usedAmount , $type )) { // case 7
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcadd ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2023-09-18 17:43:20 +02:00
}
2023-12-22 17:28:42 +01:00
if ( $isSameAccount && $isDebit && $this -> isWithdrawalOut ( $usedAmount , $type )) { // case 8
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcadd ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2023-10-05 12:36:37 -04:00
}
2024-01-06 14:40:06 +01:00
if ( $isSameAccount && $isDebit && $this -> isTransferIn ( $usedAmount , $type )) { // case 9
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2024-01-06 14:44:50 +01:00
2026-01-25 10:55:27 +01:00
return bcsub ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2024-12-27 12:43:57 +01:00
// 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS.
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2024-01-06 14:40:06 +01:00
}
if ( $isSameAccount && $isDebit && $this -> isTransferOut ( $usedAmount , $type )) { // case 10
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: positive ( $usedAmount );
2024-01-06 14:40:06 +01:00
2026-01-25 10:55:27 +01:00
return bcadd ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2024-12-27 12:43:57 +01:00
// 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE.
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2024-01-06 14:40:06 +01:00
}
2023-01-15 06:58:09 +01:00
// in any other case, remove amount from left of debt.
2025-01-03 09:09:15 +01:00
if ( in_array ( $type , [ TransactionTypeEnum :: WITHDRAWAL -> value , TransactionTypeEnum :: DEPOSIT -> value , TransactionTypeEnum :: TRANSFER -> value ], true )) {
2025-12-17 08:47:31 +01:00
$usedAmount = Steam :: negative ( $usedAmount );
2023-12-20 19:35:52 +01:00
2026-01-25 10:55:27 +01:00
return bcadd ( $leftOfDebt , ( string ) $usedAmount );
2026-01-23 15:09:50 +01:00
2025-12-17 08:43:39 +01:00
// Log::debug(sprintf('Case X (all other cases): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2)));
2021-04-11 06:41:21 +02:00
}
2025-12-17 08:47:31 +01:00
Log :: warning ( sprintf ( '[-1] Catch-all, should not happen. Left of debt = %s' , Steam :: bcround ( $leftOfDebt , 2 )));
2023-01-15 06:58:09 +01:00
return $leftOfDebt ;
2021-04-11 06:41:21 +02:00
}
2023-12-22 09:09:58 +01:00
2026-02-06 13:55:17 +01:00
private function processWork () : void
2023-12-22 09:09:58 +01:00
{
2026-02-06 13:55:17 +01:00
$this -> repository = app ( AccountRepositoryInterface :: class );
foreach ( $this -> work as $account ) {
$this -> processWorkAccount ( $account );
2023-12-22 09:09:58 +01:00
}
}
2026-02-06 13:55:17 +01:00
private function processWorkAccount ( Account $account ) : void
2023-12-22 09:09:58 +01:00
{
2026-02-06 13:55:17 +01:00
Log :: debug ( sprintf ( 'Now processing account #%d ("%s").' , $account -> id , $account -> name ));
// get opening balance (if present)
$this -> repository -> setUser ( $account -> user );
$direction = ( string ) $this -> repository -> getMetaValue ( $account , 'liability_direction' );
$openingBalance = $this -> repository -> getOpeningBalance ( $account );
// Log::debug(sprintf('Found opening balance transaction journal #%d', $openingBalance->id));
// if account direction is "debit" ("I owe this amount") the opening balance must always be AWAY from the account:
if ( $openingBalance instanceof TransactionJournal && 'debit' === $direction ) {
$this -> validateOpeningBalance ( $account , $openingBalance );
}
$startOfDebt = $this -> repository -> getOpeningBalanceAmount ( $account , false ) ? ? '0' ;
$leftOfDebt = Steam :: positive ( $startOfDebt );
// Log::debug(sprintf('Start of debt is "%s", so initial left of debt is "%s"', \FireflyIII\Support\Facades\Steam::bcround($startOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2)));
2023-12-22 09:09:58 +01:00
2026-02-06 13:55:17 +01:00
/** @var AccountMetaFactory $factory */
$factory = app ( AccountMetaFactory :: class );
2023-12-22 09:09:58 +01:00
2026-02-06 13:55:17 +01:00
// amount is positive or negative, doesn't matter.
$factory -> crud ( $account , 'start_of_debt' , $startOfDebt );
2023-12-22 09:09:58 +01:00
2026-02-06 13:55:17 +01:00
// Log::debug(sprintf('Debt direction is "%s"', $direction));
2023-12-22 09:09:58 +01:00
2026-02-06 13:55:17 +01:00
// now loop all transactions (except opening balance and credit thing)
$transactions = $account
-> transactions ()
-> leftJoin ( 'transaction_journals' , 'transaction_journals.id' , '=' , 'transactions.transaction_journal_id' )
-> orderBy ( 'transaction_journals.date' , 'ASC' )
-> get ([ 'transactions.*' ])
;
// $transactions->count();
// Log::debug(sprintf('Found %d transaction(s) to process.', $total));
/** @var Transaction $transaction */
foreach ( $transactions as $transaction ) {
// Log::debug(sprintf('[%d/%d] Processing transaction.', $index + 1, $total));
$leftOfDebt = $this -> processTransaction ( $account , $direction , $transaction , $leftOfDebt );
}
$factory -> crud ( $account , 'current_debt' , $leftOfDebt );
Log :: debug ( sprintf ( 'Done processing account #%d ("%s")' , $account -> id , $account -> name ));
2023-12-22 09:09:58 +01:00
}
2024-01-06 14:40:06 +01:00
/**
2026-02-06 13:55:17 +01:00
* If account direction is " debit " ( " I owe this amount " ) the opening balance must always be AWAY from the account :
2024-01-06 14:40:06 +01:00
*/
2026-02-06 13:55:17 +01:00
private function validateOpeningBalance ( Account $account , TransactionJournal $openingBalance ) : void
2026-02-04 16:16:27 +01:00
{
2026-02-06 13:55:17 +01:00
/** @var Transaction $source */
$source = $openingBalance -> transactions () -> where ( 'amount' , '<' , 0 ) -> first ();
2026-02-04 16:22:26 +01:00
2026-02-06 13:55:17 +01:00
/** @var Transaction $dest */
$dest = $openingBalance -> transactions () -> where ( 'amount' , '>' , 0 ) -> first ();
if ( $source -> account_id !== $account -> id ) {
Log :: info ( sprintf ( 'Liability #%d has a reversed opening balance. Will fix this now.' , $account -> id ));
Log :: debug ( sprintf ( 'Source amount "%s" is now "%s"' , $source -> amount , Steam :: positive ( $source -> amount )));
Log :: debug ( sprintf ( 'Destination amount "%s" is now "%s"' , $dest -> amount , Steam :: negative ( $dest -> amount )));
$source -> amount = Steam :: positive ( $source -> amount );
$dest -> amount = Steam :: negative ( $source -> amount );
if ( null !== $source -> foreign_amount && '' !== $source -> foreign_amount ) {
$source -> foreign_amount = Steam :: positive ( $source -> foreign_amount );
Log :: debug ( sprintf ( 'Source foreign amount "%s" is now "%s"' , $source -> foreign_amount , Steam :: positive ( $source -> foreign_amount )));
}
if ( null !== $dest -> foreign_amount && '' !== $dest -> foreign_amount ) {
$dest -> foreign_amount = Steam :: negative ( $dest -> foreign_amount );
Log :: debug ( sprintf ( 'Destination amount "%s" is now "%s"' , $dest -> foreign_amount , Steam :: negative ( $dest -> foreign_amount )));
2026-02-04 16:16:27 +01:00
}
2026-02-06 13:55:17 +01:00
$source -> save ();
$dest -> save ();
2026-02-04 16:16:27 +01:00
}
2026-02-06 13:55:17 +01:00
// Log::debug('Opening balance is valid');
2026-02-04 16:16:27 +01:00
}
2021-05-13 06:17:53 +02:00
}