. */ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Json; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Log; use Throwable; /** * * Class ReconcileController * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ReconcileController extends Controller { /** @var AccountRepositoryInterface The account repository */ private $accountRepos; /** @var CurrencyRepositoryInterface The currency repository */ private $currencyRepos; /** @var JournalRepositoryInterface Journals and transactions overview */ private $repository; /** * ReconcileController constructor. */ public function __construct() { parent::__construct(); // translations: $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); app('view')->share('title', (string)trans('firefly.accounts')); $this->repository = app(JournalRepositoryInterface::class); $this->accountRepos = app(AccountRepositoryInterface::class); $this->currencyRepos = app(CurrencyRepositoryInterface::class); return $next($request); } ); } /** @noinspection MoreThanThreeArgumentsInspection */ /** * Overview of reconciliation. * * @param Request $request * @param Account $account * @param Carbon $start * @param Carbon $end * * @return JsonResponse * * @throws FireflyException * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function overview(Request $request, Account $account, Carbon $start, Carbon $end): JsonResponse { if (AccountType::ASSET !== $account->accountType->type) { throw new FireflyException(sprintf('Account %s is not an asset account.', $account->name)); } $startBalance = $request->get('startBalance'); $endBalance = $request->get('endBalance'); $transactionIds = $request->get('transactions') ?? []; $clearedIds = $request->get('cleared') ?? []; $amount = '0'; $clearedAmount = '0'; $route = route('accounts.reconcile.submit', [$account->id, $start->format('Ymd'), $end->format('Ymd')]); // get sum of transaction amounts: $transactions = $this->repository->getTransactionsById($transactionIds); $cleared = $this->repository->getTransactionsById($clearedIds); $countCleared = 0; /** @var Transaction $transaction */ foreach ($transactions as $transaction) { $amount = bcadd($amount, $transaction->amount); } /** @var Transaction $transaction */ foreach ($cleared as $transaction) { if ($transaction->transactionJournal->date <= $end) { $clearedAmount = bcadd($clearedAmount, $transaction->amount); ++$countCleared; } } $difference = bcadd(bcadd(bcsub($startBalance, $endBalance), $clearedAmount), $amount); $diffCompare = bccomp($difference, '0'); try { $view = view( 'accounts.reconcile.overview', compact( 'account', 'start', 'diffCompare', 'difference', 'end', 'clearedIds', 'transactionIds', 'clearedAmount', 'startBalance', 'endBalance', 'amount', 'route', 'countCleared' ) )->render(); } catch (Throwable $e) { Log::debug(sprintf('View error: %s', $e->getMessage())); $view = 'Could not render accounts.reconcile.overview'; } $return = [ 'post_uri' => $route, 'html' => $view, ]; return response()->json($return); } /** * Returns a list of transactions in a modal. * * @param Account $account * @param Carbon $start * @param Carbon $end * * @return mixed * * @throws FireflyException */ public function transactions(Account $account, Carbon $start, Carbon $end) { if (AccountType::INITIAL_BALANCE === $account->accountType->type) { return $this->redirectToOriginalAccount($account); } $startDate = clone $start; $startDate->subDays(1); $currencyId = (int)$this->accountRepos->getMetaValue($account, 'currency_id'); $currency = $this->currencyRepos->findNull($currencyId); if (0 === $currencyId) { $currency = app('amount')->getDefaultCurrency(); // @codeCoverageIgnore } $startBalance = round(app('steam')->balance($account, $startDate), $currency->decimal_places); $endBalance = round(app('steam')->balance($account, $end), $currency->decimal_places); // get the transactions $selectionStart = clone $start; $selectionStart->subDays(3); $selectionEnd = clone $end; $selectionEnd->addDays(3); // grab transactions: /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setAccounts(new Collection([$account])) ->setRange($selectionStart, $selectionEnd)->withBudgetInformation()->withOpposingAccount()->withCategoryInformation(); $transactions = $collector->getJournals(); try { $html = view( 'accounts.reconcile.transactions', compact('account', 'transactions', 'currency', 'start', 'end', 'selectionStart', 'selectionEnd') )->render(); } catch (Throwable $e) { Log::debug(sprintf('Could not render: %s', $e->getMessage())); $html = 'Could not render accounts.reconcile.transactions'; } return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]); } /** * Redirect to actual account. * * @param Account $account * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * * @throws FireflyException */ protected function redirectToOriginalAccount(Account $account) // user redirection + nav { /** @var Transaction $transaction */ $transaction = $account->transactions()->first(); if (null === $transaction) { throw new FireflyException(sprintf('Expected a transaction. Account #%d has none. BEEP, error.', $account->id)); // @codeCoverageIgnore } $journal = $transaction->transactionJournal; /** @var Transaction $opposingTransaction */ $opposingTransaction = $journal->transactions()->where('transactions.id', '!=', $transaction->id)->first(); if (null === $opposingTransaction) { throw new FireflyException('Expected an opposing transaction. This account has none. BEEP, error.'); // @codeCoverageIgnore } return redirect(route('accounts.show', [$opposingTransaction->account_id])); } }