firefly-iii/app/Services/Internal/Support/CreditRecalculateService.php

320 lines
10 KiB
PHP
Raw Normal View History

<?php
2021-08-10 12:31:55 -05: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 12:31:55 -05:00
declare(strict_types=1);
namespace FireflyIII\Services\Internal\Support;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\AccountMetaFactory;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
2021-04-10 23:41:21 -05:00
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
2023-04-01 00:04:42 -05:00
use Illuminate\Support\Facades\Log;
2023-07-04 06:29:19 -05:00
/**
* Class CreditRecalculateService
*/
class CreditRecalculateService
{
private ?Account $account;
private ?TransactionGroup $group;
private AccountRepositoryInterface $repository;
private array $work;
/**
* CreditRecalculateService constructor.
*/
public function __construct()
{
$this->group = null;
$this->account = null;
$this->work = [];
}
/**
*
*/
public function recalculate(): void
{
if (true !== config('firefly.feature_flags.handle_debts')) {
return;
}
if (null !== $this->group && null === $this->account) {
$this->processGroup();
}
if (null !== $this->account && null === $this->group) {
// work based on account.
$this->processAccount();
}
2022-11-03 23:11:05 -05:00
if (0 === count($this->work)) {
return;
}
2021-04-26 00:29:39 -05:00
$this->processWork();
}
/**
2023-06-21 05:34:58 -05:00
*
2023-05-29 06:56:55 -05:00
*/
2023-06-21 05:34:58 -05:00
private function processGroup(): void
2023-05-29 06:56:55 -05:00
{
2023-06-21 05:34:58 -05:00
/** @var TransactionJournal $journal */
foreach ($this->group->transactionJournals as $journal) {
try {
$this->findByJournal($journal);
} catch (FireflyException $e) {
Log::error($e->getTraceAsString());
Log::error(sprintf('Could not find work account for transaction group #%d.', $this->group->id));
}
}
}
/**
2023-06-21 05:34:58 -05:00
* @param TransactionJournal $journal
*
* @throws FireflyException
*/
private function findByJournal(TransactionJournal $journal): void
{
$source = $this->getSourceAccount($journal);
$destination = $this->getDestinationAccount($journal);
// destination or source must be liability.
$valid = config('firefly.valid_liabilities');
2022-10-30 08:24:37 -05:00
if (in_array($destination->accountType->type, $valid, true)) {
$this->work[] = $destination;
}
2022-10-30 08:24:37 -05:00
if (in_array($source->accountType->type, $valid, true)) {
$this->work[] = $source;
}
}
/**
2023-06-21 05:34:58 -05:00
* @param TransactionJournal $journal
*
* @return Account
* @throws FireflyException
*/
private function getSourceAccount(TransactionJournal $journal): Account
{
return $this->getAccountByDirection($journal, '<');
}
/**
* @param TransactionJournal $journal
* @param string $direction
*
* @return Account
* @throws FireflyException
*/
private function getAccountByDirection(TransactionJournal $journal, string $direction): Account
{
/** @var Transaction $transaction */
$transaction = $journal->transactions()->where('amount', $direction, '0')->first();
if (null === $transaction) {
throw new FireflyException(sprintf('Cannot find "%s"-transaction of journal #%d', $direction, $journal->id));
}
2021-05-24 01:06:56 -05:00
$foundAccount = $transaction->account;
if (null === $foundAccount) {
throw new FireflyException(sprintf('Cannot find "%s"-account of transaction #%d of journal #%d', $direction, $transaction->id, $journal->id));
}
2021-05-24 01:06:56 -05:00
return $foundAccount;
}
/**
2023-06-21 05:34:58 -05:00
* @param TransactionJournal $journal
*
* @return Account
* @throws FireflyException
*/
private function getDestinationAccount(TransactionJournal $journal): Account
{
return $this->getAccountByDirection($journal, '>');
}
2023-05-29 06:56:55 -05:00
/**
*
*/
2023-06-21 05:34:58 -05:00
private function processAccount(): void
2023-05-29 06:56:55 -05:00
{
2023-06-21 05:34:58 -05:00
$valid = config('firefly.valid_liabilities');
if (in_array($this->account->accountType->type, $valid, true)) {
$this->work[] = $this->account;
}
2023-05-29 06:56:55 -05:00
}
/**
*
*/
2023-06-21 05:34:58 -05:00
private function processWork(): void
{
2023-06-21 05:34:58 -05:00
$this->repository = app(AccountRepositoryInterface::class);
foreach ($this->work as $account) {
$this->processWorkAccount($account);
}
}
/**
2023-06-21 05:34:58 -05:00
* @param Account $account
*/
2023-06-21 05:34:58 -05:00
private function processWorkAccount(Account $account): void
{
2023-06-21 05:34:58 -05:00
// get opening balance (if present)
$this->repository->setUser($account->user);
$startOfDebt = $this->repository->getOpeningBalanceAmount($account) ?? '0';
$leftOfDebt = app('steam')->positive($startOfDebt);
/** @var AccountMetaFactory $factory */
$factory = app(AccountMetaFactory::class);
// amount is positive or negative, doesn't matter.
$factory->crud($account, 'start_of_debt', $startOfDebt);
// get direction of liability:
$direction = (string)$this->repository->getMetaValue($account, 'liability_direction');
// now loop all transactions (except opening balance and credit thing)
$transactions = $account->transactions()->get();
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$leftOfDebt = $this->processTransaction($account, $direction, $transaction, $leftOfDebt);
}
2023-06-21 05:34:58 -05:00
$factory->crud($account, 'current_debt', $leftOfDebt);
}
2021-04-10 23:41:21 -05:00
/**
2023-07-04 06:29:19 -05:00
* @param Account $account
* @param string $direction
2023-06-21 05:34:58 -05:00
* @param Transaction $transaction
2023-07-04 06:29:19 -05:00
* @param string $leftOfDebt
2021-04-10 23:41:21 -05:00
* @return string
*/
private function processTransaction(Account $account, string $direction, Transaction $transaction, string $leftOfDebt): string
2021-04-10 23:41:21 -05:00
{
2022-12-23 22:48:04 -06:00
$journal = $transaction->transactionJournal;
$foreignCurrency = $transaction->foreignCurrency;
$accountCurrency = $this->repository->getAccountCurrency($account);
$groupId = $journal->transaction_group_id;
$type = $journal->transactionType->type;
/** @var Transaction $destTransaction */
$destTransaction = $journal->transactions()->where('amount', '>', '0')->first();
/** @var Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', '0')->first();
2022-12-23 22:48:04 -06:00
2021-05-15 02:16:54 -05:00
if ('' === $direction) {
return $leftOfDebt;
2021-05-15 02:16:54 -05:00
}
if (TransactionType::LIABILITY_CREDIT === $type || TransactionType::OPENING_BALANCE === $type) {
return $leftOfDebt;
}
2022-12-23 22:48:04 -06:00
// amount to use depends on the currency:
$usedAmount = $transaction->amount;
if (null !== $foreignCurrency && $foreignCurrency->id === $accountCurrency->id) {
$usedAmount = $transaction->foreign_amount;
}
// Case 1
2021-05-15 02:16:54 -05:00
// it's a withdrawal into this liability (from asset).
2022-12-27 00:01:13 -06:00
// if it's a credit ("I am owed"), this increases the amount due,
// because we're lending person X more money
2021-05-15 02:16:54 -05:00
if (
2021-09-18 03:21:29 -05:00
$type === TransactionType::WITHDRAWAL
2022-12-23 22:48:04 -06:00
&& (int)$account->id === (int)$transaction->account_id
&& 1 === bccomp($usedAmount, '0')
2021-05-15 02:16:54 -05:00
&& 'credit' === $direction
) {
return bcadd($leftOfDebt, app('steam')->positive($usedAmount));
}
// Case 2
// it's a withdrawal away from this liability (into expense account).
// 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)
if (
$type === TransactionType::WITHDRAWAL
&& (int)$account->id === (int)$sourceTransaction->account_id
&& -1 === bccomp($usedAmount, '0')
&& 'credit' === $direction
) {
return bcsub($leftOfDebt, app('steam')->positive($usedAmount));
2021-04-26 00:29:39 -05:00
}
2021-05-15 02:16:54 -05:00
// case 3
2022-12-27 00:05:56 -06:00
// it's a deposit out of this liability (to asset).
// if it's a credit ("I am owed") this decreases the amount due.
// because the person is paying us back.
2021-05-15 02:16:54 -05:00
if (
2021-09-18 03:21:29 -05:00
$type === TransactionType::DEPOSIT
2022-12-23 22:48:04 -06:00
&& (int)$account->id === (int)$transaction->account_id
&& -1 === bccomp($usedAmount, '0')
2021-05-15 02:16:54 -05:00
&& 'credit' === $direction
) {
return bcsub($leftOfDebt, app('steam')->positive($usedAmount));
2021-04-26 00:29:39 -05:00
}
2023-01-15 00:52:05 -06: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.
if (
$type === TransactionType::DEPOSIT
&& (int)$account->id === (int)$destTransaction->account_id
&& 1 === bccomp($usedAmount, '0')
&& 'credit' === $direction
) {
$newLeftOfDebt = bcadd($leftOfDebt, app('steam')->positive($usedAmount));
return $newLeftOfDebt;
}
// in any other case, remove amount from left of debt.
2021-04-10 23:41:21 -05:00
if (in_array($type, [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER], true)) {
$newLeftOfDebt = bcadd($leftOfDebt, bcmul($usedAmount, '-1'));
return $newLeftOfDebt;
2021-04-10 23:41:21 -05:00
}
2023-01-15 00:52:05 -06:00
Log::warning(sprintf('[6] Catch-all, should not happen. Left of debt = %s', $leftOfDebt));
return $leftOfDebt;
2021-04-10 23:41:21 -05:00
}
/**
2023-06-21 05:34:58 -05:00
* @param Account|null $account
*/
2023-06-21 05:34:58 -05:00
public function setAccount(?Account $account): void
{
2023-06-21 05:34:58 -05:00
$this->account = $account;
}
/**
2023-06-21 05:34:58 -05:00
* @param TransactionGroup $group
*/
2023-06-21 05:34:58 -05:00
public function setGroup(TransactionGroup $group): void
{
2023-06-21 05:34:58 -05:00
$this->group = $group;
}
2021-05-12 23:17:53 -05:00
}