firefly-iii/app/Repositories/Budget/BudgetRepository.php

661 lines
24 KiB
PHP
Raw Normal View History

2015-02-22 02:46:21 -06:00
<?php
/**
* BudgetRepository.php
* Copyright (C) 2016 thegrumpydictator@gmail.com
*
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
2016-02-05 05:08:25 -06:00
declare(strict_types = 1);
2015-02-22 02:46:21 -06:00
namespace FireflyIII\Repositories\Budget;
use Carbon\Carbon;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
2016-12-22 09:36:56 -06:00
use FireflyIII\Models\AvailableBudget;
2015-02-22 02:46:21 -06:00
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
2016-10-29 09:11:54 -05:00
use FireflyIII\Models\Transaction;
2016-12-22 09:36:56 -06:00
use FireflyIII\Models\TransactionCurrency;
2016-03-02 06:55:48 -06:00
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
2016-05-11 02:08:18 -05:00
use Illuminate\Database\Query\JoinClause;
2015-04-05 03:36:28 -05:00
use Illuminate\Support\Collection;
2016-10-29 09:16:10 -05:00
use Log;
2016-11-26 01:41:15 -06:00
use Navigation;
2016-11-19 06:37:44 -06:00
use stdClass;
2015-02-22 02:46:21 -06:00
/**
* Class BudgetRepository
*
* @package FireflyIII\Repositories\Budget
*/
2016-05-05 23:15:46 -05:00
class BudgetRepository implements BudgetRepositoryInterface
2015-02-22 02:46:21 -06:00
{
/** @var User */
private $user;
/**
2016-04-26 02:21:57 -05:00
* BudgetRepository constructor.
*
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
2015-02-22 02:46:21 -06:00
2016-06-16 13:52:59 -05:00
/**
* @return bool
*/
public function cleanupBudgets(): bool
{
// delete limits with amount 0:
BudgetLimit::where('amount', 0)->delete();
return true;
}
2015-02-22 08:40:13 -06:00
/**
* @param Budget $budget
*
2016-04-05 15:00:03 -05:00
* @return bool
2015-02-22 08:40:13 -06:00
*/
2016-04-05 15:00:03 -05:00
public function destroy(Budget $budget): bool
2015-02-22 08:40:13 -06:00
{
$budget->delete();
return true;
}
2016-11-19 06:37:44 -06:00
/**
* Filters entries from the result set generated by getBudgetPeriodReport
*
* @param Collection $set
* @param int $budgetId
* @param array $periods
*
* @return array
*/
public function filterAmounts(Collection $set, int $budgetId, array $periods): array
{
$arr = [];
$keys = array_keys($periods);
foreach ($keys as $period) {
/** @var stdClass $object */
$result = $set->filter(
function (TransactionJournal $object) use ($budgetId, $period) {
$result = strval($object->period_marker) === strval($period) && $budgetId === intval($object->budget_id);
return $result;
}
);
$amount = '0';
if (!is_null($result->first())) {
$amount = $result->first()->sum_of_period;
}
$arr[$period] = $amount;
}
return $arr;
}
2016-04-01 06:17:07 -05:00
/**
* Find a budget.
*
* @param int $budgetId
*
* @return Budget
*/
public function find(int $budgetId): Budget
{
$budget = $this->user->budgets()->find($budgetId);
if (is_null($budget)) {
$budget = new Budget;
}
return $budget;
}
/**
* Find a budget.
*
* @param string $name
*
* @return Budget
*/
public function findByName(string $name): Budget
{
$budgets = $this->user->budgets()->get(['budgets.*']);
/** @var Budget $budget */
foreach ($budgets as $budget) {
if ($budget->name === $name) {
return $budget;
}
}
return new Budget;
}
/**
* This method returns the oldest journal or transaction date known to this budget.
* Will cache result.
*
* @param Budget $budget
*
* @return Carbon
*/
public function firstUseDate(Budget $budget): Carbon
{
$oldest = Carbon::create()->startOfYear();
$journal = $budget->transactionJournals()->orderBy('date', 'ASC')->first();
2016-05-11 10:17:43 -05:00
if (!is_null($journal)) {
$oldest = $journal->date < $oldest ? $journal->date : $oldest;
}
$transaction = $budget
->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.id')
->orderBy('transaction_journals.date', 'ASC')->first(['transactions.*', 'transaction_journals.date']);
2016-05-11 10:17:43 -05:00
if (!is_null($transaction)) {
$carbon = new Carbon($transaction->date);
$oldest = $carbon < $oldest ? $carbon : $oldest;
}
return $oldest;
}
2015-12-27 14:17:04 -06:00
/**
* @return Collection
*/
2016-04-05 15:00:03 -05:00
public function getActiveBudgets(): Collection
2015-12-27 14:17:04 -06:00
{
/** @var Collection $set */
$set = $this->user->budgets()->where('active', 1)->get();
2015-12-27 14:17:04 -06:00
$set = $set->sortBy(
function (Budget $budget) {
return strtolower($budget->name);
}
);
return $set;
}
/**
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
2016-12-30 01:41:48 -06:00
public function getAllBudgetLimits(Carbon $start, Carbon $end): Collection
{
2016-12-30 01:41:48 -06:00
$set = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->with(['budget'])
->where('budgets.user_id', $this->user->id)
->where(
function (Builder $q1) use ($start, $end) {
$q1->where(
function (Builder $q2) use ($start, $end) {
$q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00'));
$q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00'));
}
)
->orWhere(
function (Builder $q3) use ($start, $end) {
$q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00'));
$q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00'));
}
);
}
)
->orWhere(
function (Builder $q4) use ($start, $end) {
// or start is before start AND end is after end.
$q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00'));
$q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00'));
}
)
->get(['budget_limits.*']);
2015-07-09 04:13:38 -05:00
return $set;
2015-04-05 03:36:28 -05:00
}
2016-12-22 09:36:56 -06:00
/**
* @param TransactionCurrency $currency
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string
{
$amount = '0';
$availableBudget = $this->user->availableBudgets()
->where('transaction_currency_id', $currency->id)
->where('start_date', $start->format('Y-m-d'))
->where('end_date', $end->format('Y-m-d'))->first();
if (!is_null($availableBudget)) {
$amount = strval($availableBudget->amount);
}
return $amount;
}
2016-12-29 13:48:33 -06:00
/**
* @param Budget $budget
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
public function getBudgetLimits(Budget $budget, Carbon $start, Carbon $end): Collection
{
$set = $budget->budgetLimits()
->where(
function (Builder $q1) use ($start, $end) {
2016-12-30 01:41:48 -06:00
// budget limit ends within period
2016-12-29 13:48:33 -06:00
$q1->where(
function (Builder $q2) use ($start, $end) {
$q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00'));
$q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00'));
}
)
2016-12-30 01:41:48 -06:00
// budget limit start within period
2016-12-29 13:48:33 -06:00
->orWhere(
2016-12-30 01:41:48 -06:00
function (Builder $q3) use ($start, $end) {
$q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00'));
$q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00'));
}
);
2016-12-29 13:48:33 -06:00
}
2016-12-30 01:41:48 -06:00
)
->orWhere(
function (Builder $q4) use ($start, $end) {
// or start is before start AND end is after end.
$q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00'));
$q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00'));
}
)
->get();
2016-12-29 13:48:33 -06:00
return $set;
}
2016-11-19 06:37:44 -06:00
/**
2016-12-03 14:03:20 -06:00
* This method is being used to generate the budget overview in the year/multi-year report. Its used
* in both the year/multi-year budget overview AND in the accompanying chart.
2016-11-19 06:37:44 -06:00
*
* @param Collection $budgets
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return array
2016-11-19 06:37:44 -06:00
*/
public function getBudgetPeriodReport(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array
2016-11-19 06:37:44 -06:00
{
2016-11-26 01:41:15 -06:00
$carbonFormat = Navigation::preferredCarbonFormat($start, $end);
2016-12-03 13:38:13 -06:00
$data = [];
// prep data array:
/** @var Budget $budget */
foreach ($budgets as $budget) {
$data[$budget->id] = [
'name' => $budget->name,
'sum' => '0',
'entries' => [],
];
2016-11-19 06:37:44 -06:00
}
2016-12-03 13:38:13 -06:00
// get all transactions:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
2016-12-03 13:38:13 -06:00
$collector->setAccounts($accounts)->setRange($start, $end);
$collector->setBudgets($budgets);
$transactions = $collector->getJournals();
2016-12-03 13:38:13 -06:00
// loop transactions:
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
2016-12-03 13:38:13 -06:00
$budgetId = max(intval($transaction->transaction_journal_budget_id), intval($transaction->transaction_budget_id));
$date = $transaction->date->format($carbonFormat);
$data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $transaction->transaction_amount);
2016-11-19 06:37:44 -06:00
}
2016-12-03 14:03:20 -06:00
return $data;
2016-11-19 06:37:44 -06:00
}
2016-01-20 08:21:27 -06:00
/**
* @return Collection
*/
public function getBudgets(): Collection
2016-01-20 08:21:27 -06:00
{
/** @var Collection $set */
$set = $this->user->budgets()->get();
2016-01-20 08:21:27 -06:00
$set = $set->sortBy(
function (Budget $budget) {
return strtolower($budget->name);
}
);
return $set;
}
2015-04-05 03:36:28 -05:00
/**
* @return Collection
*/
public function getInactiveBudgets(): Collection
{
/** @var Collection $set */
$set = $this->user->budgets()->where('active', 0)->get();
$set = $set->sortBy(
function (Budget $budget) {
return strtolower($budget->name);
}
);
return $set;
}
/**
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array
{
$carbonFormat = Navigation::preferredCarbonFormat($start, $end);
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end);
$collector->setTypes([TransactionType::WITHDRAWAL]);
$collector->withoutBudget();
$transactions = $collector->getJournals();
$result = [
'entries' => [],
'name' => strval(trans('firefly.no_budget')),
'sum' => '0',
];
foreach ($transactions as $transaction) {
$date = $transaction->date->format($carbonFormat);
if (!isset($result['entries'][$date])) {
$result['entries'][$date] = '0';
}
$result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount);
}
return $result;
}
2016-12-22 09:36:56 -06:00
/**
* @param TransactionCurrency $currency
* @param Carbon $start
* @param Carbon $end
* @param string $amount
*
* @return bool
*/
public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): bool
{
$availableBudget = $this->user->availableBudgets()
->where('transaction_currency_id', $currency->id)
->where('start_date', $start->format('Y-m-d'))
->where('end_date', $end->format('Y-m-d'))->first();
if (is_null($availableBudget)) {
$availableBudget = new AvailableBudget;
$availableBudget->user()->associate($this->user);
$availableBudget->transactionCurrency()->associate($currency);
$availableBudget->start_date = $start;
$availableBudget->end_date = $end;
}
$availableBudget->amount = $amount;
$availableBudget->save();
return true;
}
2016-05-05 23:15:46 -05:00
/**
* @param Collection $budgets
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function spentInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): string
2016-05-05 23:15:46 -05:00
{
2016-10-29 09:11:54 -05:00
// collect amount of transaction journals, which is easy:
$budgetIds = $budgets->pluck('id')->toArray();
2016-11-05 05:47:21 -05:00
$accountIds = $accounts->pluck('id')->toArray();
2016-10-29 09:16:10 -05:00
Log::debug(sprintf('spentInPeriod: Now in spentInPeriod for these budgets (%d): ', count($budgetIds)), $budgetIds);
2016-10-29 09:16:10 -05:00
Log::debug('spentInPeriod: and these accounts: ', $accountIds);
Log::debug(sprintf('spentInPeriod: Start date is "%s", end date is "%s"', $start->format('Y-m-d'), $end->format('Y-m-d')));
2016-12-03 13:38:13 -06:00
$fromJournalsQuery = TransactionJournal::leftJoin(
'budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'
)
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin(
'transactions', function (JoinClause $join) {
$join->on('transactions.transaction_journal_id', '=', 'transaction_journals.id')->where(
'transactions.amount', '<', 0
);
}
)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->whereNull('transaction_journals.deleted_at')
->whereNull('transactions.deleted_at')
->where('transaction_journals.user_id', $this->user->id)
->where('transaction_types.type', 'Withdrawal');
2016-10-29 09:11:54 -05:00
// add budgets:
if ($budgets->count() > 0) {
$fromJournalsQuery->whereIn('budget_transaction_journal.budget_id', $budgetIds);
2016-05-11 10:17:43 -05:00
}
2016-10-29 09:11:54 -05:00
// add accounts:
2016-05-11 10:17:43 -05:00
if ($accounts->count() > 0) {
2016-10-29 09:11:54 -05:00
$fromJournalsQuery->whereIn('transactions.account_id', $accountIds);
2016-05-11 10:17:43 -05:00
}
2016-10-29 09:11:54 -05:00
$first = strval($fromJournalsQuery->sum('transactions.amount'));
2016-10-29 09:16:10 -05:00
Log::debug(sprintf('spentInPeriod: Result from first query: %s', $first));
2016-10-29 09:11:54 -05:00
unset($fromJournalsQuery);
// collect amount from transactions:
/**
* select transactions.id, budget_transaction.budget_id , transactions.amount
*
*
* and budget_transaction.budget_id in (1,61)
* and transactions.account_id in (2)
*/
2016-11-28 13:38:03 -06:00
$fromTransactionsQuery = Transaction::leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id')
2016-12-03 13:38:13 -06:00
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at')
->where('transactions.amount', '<', 0)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->where('transaction_journals.user_id', $this->user->id)
->where('transaction_types.type', 'Withdrawal');
2016-10-29 09:11:54 -05:00
// add budgets:
2016-05-11 10:17:43 -05:00
if ($budgets->count() > 0) {
2016-10-29 09:11:54 -05:00
$fromTransactionsQuery->whereIn('budget_transaction.budget_id', $budgetIds);
2016-05-05 23:15:46 -05:00
}
2016-10-29 09:11:54 -05:00
// add accounts:
2016-05-11 10:17:43 -05:00
if ($accounts->count() > 0) {
2016-10-29 09:11:54 -05:00
$fromTransactionsQuery->whereIn('transactions.account_id', $accountIds);
2016-05-11 10:17:43 -05:00
}
2016-10-29 09:11:54 -05:00
$second = strval($fromTransactionsQuery->sum('transactions.amount'));
2016-10-29 09:16:10 -05:00
Log::debug(sprintf('spentInPeriod: Result from second query: %s', $second));
Log::debug(sprintf('spentInPeriod: FINAL: %s', bcadd($first, $second)));
2016-05-11 10:17:43 -05:00
return bcadd($first, $second);
}
/**
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function spentInPeriodWithoutBudget(Collection $accounts, Carbon $start, Carbon $end): string
{
2016-05-16 00:13:54 -05:00
$types = [TransactionType::WITHDRAWAL];
$query = $this->user->transactionJournals()
2016-05-11 10:17:43 -05:00
->distinct()
2016-05-16 00:13:54 -05:00
->transactionTypes($types)
2016-05-11 10:17:43 -05:00
->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin(
2016-05-16 00:13:54 -05:00
'transactions as source', function (JoinClause $join) {
$join->on('source.transaction_journal_id', '=', 'transaction_journals.id')->where('source.amount', '<', 0);
2016-05-11 10:17:43 -05:00
}
)
2016-05-16 00:13:54 -05:00
->leftJoin(
'transactions as destination', function (JoinClause $join) {
$join->on('destination.transaction_journal_id', '=', 'transaction_journals.id')->where('destination.amount', '>', 0);
}
)
->leftJoin('budget_transaction', 'source.id', '=', 'budget_transaction.transaction_id')
2016-05-11 10:17:43 -05:00
->whereNull('budget_transaction_journal.id')
->whereNull('budget_transaction.id')
->before($end)
2016-08-02 12:42:41 -05:00
->after($start)
->whereNull('source.deleted_at')
->whereNull('destination.deleted_at')
->where('transaction_journals.completed', 1);
2016-05-11 10:17:43 -05:00
if ($accounts->count() > 0) {
$accountIds = $accounts->pluck('id')->toArray();
$query->where(
2016-10-06 22:44:21 -05:00
// source.account_id in accountIds XOR destination.account_id in accountIds
function (Builder $sourceXorDestinationQuery) use ($accountIds) {
$sourceXorDestinationQuery->where(
function (Builder $inSourceButNotDestinationQuery) use ($accountIds) {
$inSourceButNotDestinationQuery->whereIn('source.account_id', $accountIds)
->whereNotIn('destination.account_id', $accountIds);
}
)->orWhere(
function (Builder $inDestinationButNotSourceQuery) use ($accountIds) {
$inDestinationButNotSourceQuery->whereIn('destination.account_id', $accountIds)
->whereNotIn('source.account_id', $accountIds);
}
);
}
);
2016-05-11 10:17:43 -05:00
}
2016-08-02 12:42:41 -05:00
$ids = $query->get(['transaction_journals.id'])->pluck('id')->toArray();
$sum = '0';
if (count($ids) > 0) {
$sum = strval(
$this->user->transactions()
->whereIn('transaction_journal_id', $ids)
->where('amount', '<', '0')
->whereNull('transactions.deleted_at')
->sum('amount')
);
}
2016-05-05 23:15:46 -05:00
return $sum;
}
2016-01-20 08:21:27 -06:00
/**
* @param array $data
*
* @return Budget
*/
2016-04-05 15:00:03 -05:00
public function store(array $data): Budget
2016-01-20 08:21:27 -06:00
{
$newBudget = new Budget(
[
2016-10-23 05:19:32 -05:00
'user_id' => $this->user->id,
2016-01-20 08:21:27 -06:00
'name' => $data['name'],
]
);
$newBudget->save();
return $newBudget;
}
/**
* @param Budget $budget
* @param array $data
*
* @return Budget
*/
2016-04-05 15:00:03 -05:00
public function update(Budget $budget, array $data): Budget
2016-01-20 08:21:27 -06:00
{
// update the account:
$budget->name = $data['name'];
$budget->active = $data['active'];
$budget->save();
return $budget;
}
/**
* @param Budget $budget
2016-04-28 03:59:36 -05:00
* @param Carbon $start
* @param Carbon $end
2016-02-05 08:41:40 -06:00
* @param int $amount
2016-01-20 08:21:27 -06:00
*
* @return BudgetLimit
*/
2016-12-29 13:52:02 -06:00
public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, int $amount): BudgetLimit
2016-01-20 08:21:27 -06:00
{
2016-12-29 13:52:02 -06:00
// there might be a budget limit for these dates:
2016-01-20 08:21:27 -06:00
/** @var BudgetLimit $limit */
2016-04-28 03:59:36 -05:00
$limit = $budget->budgetlimits()
2016-12-29 13:52:02 -06:00
->where('budget_limits.start_date', $start->format('Y-m-d'))
->where('budget_limits.end_date', $end->format('Y-m-d'))
->first(['budget_limits.*']);
2016-04-28 03:59:36 -05:00
// delete if amount is zero.
if (!is_null($limit) && $amount <= 0.0) {
$limit->delete();
return new BudgetLimit;
}
// update if exists:
if (!is_null($limit)) {
$limit->amount = $amount;
2016-01-20 08:21:27 -06:00
$limit->save();
2016-04-28 03:59:36 -05:00
return $limit;
2016-01-20 08:21:27 -06:00
}
2016-12-29 13:52:02 -06:00
// or create one and return it.
2016-04-28 03:59:36 -05:00
$limit = new BudgetLimit;
$limit->budget()->associate($budget);
2016-12-29 13:52:02 -06:00
$limit->start_date = $start;
$limit->end_date = $end;
$limit->amount = $amount;
2016-04-28 03:59:36 -05:00
$limit->save();
2016-01-20 08:21:27 -06:00
return $limit;
}
2015-03-29 01:14:32 -05:00
}