diff --git a/app/Api/V1/Controllers/BudgetLimitController.php b/app/Api/V1/Controllers/BudgetLimitController.php index 80dc9f464e..d4cf2dd75c 100644 --- a/app/Api/V1/Controllers/BudgetLimitController.php +++ b/app/Api/V1/Controllers/BudgetLimitController.php @@ -95,8 +95,6 @@ class BudgetLimitController extends Controller { $manager = new Manager; $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; - $start = null; - $end = null; $budgetId = (int)($request->get('budget_id') ?? 0); $budget = $this->repository->findNull($budgetId); $this->parameters->set('budget_id', $budgetId); @@ -119,9 +117,11 @@ class BudgetLimitController extends Controller $collection = new Collection; if (null === $budget) { + /** @noinspection PhpUndefinedVariableInspection */ $collection = $this->repository->getAllBudgetLimits($start, $end); } if (null !== $budget) { + /** @noinspection PhpUndefinedVariableInspection */ $collection = $this->repository->getBudgetLimits($budget, $start, $end); } diff --git a/app/Http/Controllers/Account/ShowController.php b/app/Http/Controllers/Account/ShowController.php index 7ef899cba9..250bf023a3 100644 --- a/app/Http/Controllers/Account/ShowController.php +++ b/app/Http/Controllers/Account/ShowController.php @@ -40,8 +40,9 @@ use Preferences; use View; /** - * * Class ShowController + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShowController extends Controller { diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php index c3258b9fea..ca7d6a181d 100644 --- a/app/Http/Controllers/BillController.php +++ b/app/Http/Controllers/BillController.php @@ -43,6 +43,8 @@ use View; /** * Class BillController. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class BillController extends Controller { @@ -298,6 +300,9 @@ class BillController extends Controller * @param BillFormRequest $request * * @return RedirectResponse + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function store(BillFormRequest $request): RedirectResponse { @@ -316,7 +321,6 @@ class BillController extends Controller $files = $request->hasFile('attachments') ? $request->file('attachments') : null; $this->attachments->saveAttachmentsForModel($bill, $files); - // flash messages if (\count($this->attachments->getMessages()->get('attachments')) > 0) { $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); // @codeCoverageIgnore } diff --git a/app/Http/Controllers/Budget/AmountController.php b/app/Http/Controllers/Budget/AmountController.php new file mode 100644 index 0000000000..9274ce18a0 --- /dev/null +++ b/app/Http/Controllers/Budget/AmountController.php @@ -0,0 +1,276 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Budget; + + +use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Http\Requests\BudgetIncomeRequest; +use FireflyIII\Models\Budget; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Support\CacheProperties; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Support\Collection; +use Log; +use Preferences; +use View; + +/** + * + * Class AmountController + */ +class AmountController extends Controller +{ + /** @var BudgetRepositoryInterface */ + private $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + + View::share('hideBudgets', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', trans('firefly.budgets')); + app('view')->share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); + } + + + /** + * @param Request $request + * @param BudgetRepositoryInterface $repository + * @param Budget $budget + * + * @return JsonResponse + */ + public function amount(Request $request, BudgetRepositoryInterface $repository, Budget $budget): JsonResponse + { + $amount = (string)$request->get('amount'); + $start = Carbon::createFromFormat('Y-m-d', $request->get('start')); + $end = Carbon::createFromFormat('Y-m-d', $request->get('end')); + $budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount); + $largeDiff = false; + $warnText = ''; + $days = 0; + $daysInMonth = 0; + if (0 === bccomp($amount, '0')) { + $budgetLimit = null; + } + + // if today is between start and end, use the diff in days between end and today (days left) + // otherwise, use diff between start and end. + $today = new Carbon; + Log::debug(sprintf('Start is %s, end is %s, today is %s', $start->format('Y-m-d'), $end->format('Y-m-d'), $today->format('Y-m-d'))); + if ($today->gte($start) && $today->lte($end)) { + $days = $end->diffInDays($today); + $daysInMonth = $start->diffInDays($today); + } + if ($today->lte($start) || $today->gte($end)) { + $days = $start->diffInDays($end); + $daysInMonth = $start->diffInDays($end); + } + $days = 0 === $days ? 1 : $days; + $daysInMonth = 0 === $daysInMonth ? 1 : $daysInMonth; + + // calculate left in budget: + $spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); + $currency = app('amount')->getDefaultCurrency(); + $left = app('amount')->formatAnything($currency, bcadd($amount, $spent), true); + $leftPerDay = 'none'; + + // is user has money left, calculate. + if (1 === bccomp(bcadd($amount, $spent), '0')) { + $leftPerDay = app('amount')->formatAnything($currency, bcdiv(bcadd($amount, $spent), (string)$days), true); + } + + + // over or under budgeting, compared to previous budgets? + $average = $this->repository->budgetedPerDay($budget); + // current average per day: + $diff = $start->diffInDays($end); + $current = $amount; + if ($diff > 0) { + $current = bcdiv($amount, (string)$diff); + } + if (bccomp(bcmul('1.1', $average), $current) === -1) { + $largeDiff = true; + $warnText = (string)trans( + 'firefly.over_budget_warn', + [ + 'amount' => app('amount')->formatAnything($currency, $average, false), + 'over_amount' => app('amount')->formatAnything($currency, $current, false), + ] + ); + } + + app('preferences')->mark(); + + return response()->json( + [ + 'left' => $left, + 'name' => $budget->name, + 'limit' => $budgetLimit ? $budgetLimit->id : 0, + 'amount' => $amount, + 'current' => $current, + 'average' => $average, + 'large_diff' => $largeDiff, + 'left_per_day' => $leftPerDay, + 'warn_text' => $warnText, + 'daysInMonth' => $daysInMonth, + + ] + ); + } + + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function infoIncome(Carbon $start, Carbon $end) + { + // properties for cache + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('info-income'); + + Log::debug(sprintf('infoIncome start is %s', $start->format('Y-m-d'))); + Log::debug(sprintf('infoIncome end is %s', $end->format('Y-m-d'))); + + if ($cache->has()) { + // @codeCoverageIgnoreStart + $result = $cache->get(); + + return view('budgets.info', compact('result')); + // @codeCoverageIgnoreEnd + } + $result = [ + 'available' => '0', + 'earned' => '0', + 'suggested' => '0', + ]; + $currency = app('amount')->getDefaultCurrency(); + $range = Preferences::get('viewRange', '1M')->data; + /** @var Carbon $begin */ + $begin = app('navigation')->subtractPeriod($start, $range, 3); + + Log::debug(sprintf('Range is %s', $range)); + Log::debug(sprintf('infoIncome begin is %s', $begin->format('Y-m-d'))); + + // get average amount available. + $total = '0'; + $count = 0; + $currentStart = clone $begin; + while ($currentStart < $start) { + + Log::debug(sprintf('Loop: currentStart is %s', $currentStart->format('Y-m-d'))); + $currentEnd = app('navigation')->endOfPeriod($currentStart, $range); + $total = bcadd($total, $this->repository->getAvailableBudget($currency, $currentStart, $currentEnd)); + $currentStart = app('navigation')->addPeriod($currentStart, $range, 0); + ++$count; + } + Log::debug('Loop end'); + + if (0 === $count) { + $count = 1; + } + $result['available'] = bcdiv($total, (string)$count); + + // amount earned in this period: + $subDay = clone $end; + $subDay->subDay(); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($begin, $subDay)->setTypes([TransactionType::DEPOSIT])->withOpposingAccount(); + $result['earned'] = bcdiv((string)$collector->getJournals()->sum('transaction_amount'), (string)$count); + + // amount spent in period + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($begin, $subDay)->setTypes([TransactionType::WITHDRAWAL])->withOpposingAccount(); + $result['spent'] = bcdiv((string)$collector->getJournals()->sum('transaction_amount'), (string)$count); + // suggestion starts with the amount spent + $result['suggested'] = bcmul($result['spent'], '-1'); + $result['suggested'] = 1 === bccomp($result['suggested'], $result['earned']) ? $result['earned'] : $result['suggested']; + // unless it's more than you earned. So min() of suggested/earned + + $cache->store($result); + + return view('budgets.info', compact('result', 'begin', 'currentEnd')); + } + + + /** + * @param BudgetIncomeRequest $request + * + * @return RedirectResponse + */ + public function postUpdateIncome(BudgetIncomeRequest $request): RedirectResponse + { + $start = Carbon::createFromFormat('Y-m-d', $request->string('start')); + $end = Carbon::createFromFormat('Y-m-d', $request->string('end')); + $defaultCurrency = app('amount')->getDefaultCurrency(); + $amount = $request->get('amount'); + $page = 0 === $request->integer('page') ? 1 : $request->integer('page'); + $this->repository->cleanupBudgets(); + $this->repository->setAvailableBudget($defaultCurrency, $start, $end, $amount); + app('preferences')->mark(); + + return redirect(route('budgets.index', [$start->format('Y-m-d')]) . '?page=' . $page); + } + + /** + * @param Request $request + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function updateIncome(Request $request, Carbon $start, Carbon $end) + { + $defaultCurrency = app('amount')->getDefaultCurrency(); + $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); + $available = round($available, $defaultCurrency->decimal_places); + $page = (int)$request->get('page'); + + return view('budgets.income', compact('available', 'start', 'end', 'page')); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php new file mode 100644 index 0000000000..6fda49b305 --- /dev/null +++ b/app/Http/Controllers/Budget/CreateController.php @@ -0,0 +1,106 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Budget; + + +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Http\Requests\BudgetFormRequest; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use View; + +/** + * Class CreateController + */ +class CreateController extends Controller +{ + /** @var BudgetRepositoryInterface */ + private $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + + View::share('hideBudgets', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', trans('firefly.budgets')); + app('view')->share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); + } + + + /** + * @param Request $request + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function create(Request $request) + { + // put previous url in session if not redirect from store (not "create another"). + if (true !== session('budgets.create.fromStore')) { + $this->rememberPreviousUri('budgets.create.uri'); + } + $request->session()->forget('budgets.create.fromStore'); + $subTitle = (string)trans('firefly.create_new_budget'); + + return view('budgets.create', compact('subTitle')); + } + + + /** + * @param BudgetFormRequest $request + * + * @return \Illuminate\Http\RedirectResponse + */ + public function store(BudgetFormRequest $request): RedirectResponse + { + $data = $request->getBudgetData(); + $budget = $this->repository->store($data); + $this->repository->cleanupBudgets(); + $request->session()->flash('success', (string)trans('firefly.stored_new_budget', ['name' => $budget->name])); + app('preferences')->mark(); + + $redirect = redirect($this->getPreviousUri('budgets.create.uri')); + + if (1 === (int)$request->get('create_another')) { + // @codeCoverageIgnoreStart + $request->session()->put('budgets.create.fromStore', true); + + $redirect = redirect(route('budgets.create'))->withInput(); + // @codeCoverageIgnoreEnd + } + + return $redirect; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Budget/DeleteController.php b/app/Http/Controllers/Budget/DeleteController.php new file mode 100644 index 0000000000..0acdbda200 --- /dev/null +++ b/app/Http/Controllers/Budget/DeleteController.php @@ -0,0 +1,94 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Budget; + + +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Budget; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use Illuminate\Http\Request; +use View; + +/** + * + * Class DeleteController + */ +class DeleteController extends Controller +{ + /** @var BudgetRepositoryInterface */ + private $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + + View::share('hideBudgets', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', trans('firefly.budgets')); + app('view')->share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); + } + + + /** + * @param Budget $budget + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function delete(Budget $budget) + { + $subTitle = trans('firefly.delete_budget', ['name' => $budget->name]); + + // put previous url in session + $this->rememberPreviousUri('budgets.delete.uri'); + + return view('budgets.delete', compact('budget', 'subTitle')); + } + + /** + * @param Request $request + * @param Budget $budget + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function destroy(Request $request, Budget $budget) + { + $name = $budget->name; + $this->repository->destroy($budget); + $request->session()->flash('success', (string)trans('firefly.deleted_budget', ['name' => $name])); + app('preferences')->mark(); + + return redirect($this->getPreviousUri('budgets.delete.uri')); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Budget/EditController.php b/app/Http/Controllers/Budget/EditController.php new file mode 100644 index 0000000000..f8e405f670 --- /dev/null +++ b/app/Http/Controllers/Budget/EditController.php @@ -0,0 +1,117 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Budget; + + +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Http\Requests\BudgetFormRequest; +use FireflyIII\Models\Budget; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use View; + +/** + * + * Class EditController + */ +class EditController extends Controller +{ + /** @var BudgetRepositoryInterface */ + private $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + + View::share('hideBudgets', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', trans('firefly.budgets')); + app('view')->share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param Request $request + * @param Budget $budget + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function edit(Request $request, Budget $budget) + { + $subTitle = trans('firefly.edit_budget', ['name' => $budget->name]); + + // code to handle active-checkboxes + $hasOldInput = null !== $request->old('_token'); + $preFilled = [ + 'active' => $hasOldInput ? (bool)$request->old('active') : $budget->active, + ]; + + // put previous url in session if not redirect from store (not "return_to_edit"). + if (true !== session('budgets.edit.fromUpdate')) { + $this->rememberPreviousUri('budgets.edit.uri'); + } + $request->session()->forget('budgets.edit.fromUpdate'); + $request->session()->flash('preFilled', $preFilled); + + return view('budgets.edit', compact('budget', 'subTitle')); + } + + /** + * @param BudgetFormRequest $request + * @param Budget $budget + * + * @return \Illuminate\Http\RedirectResponse + */ + public function update(BudgetFormRequest $request, Budget $budget): RedirectResponse + { + $data = $request->getBudgetData(); + $this->repository->update($budget, $data); + + $request->session()->flash('success', (string)trans('firefly.updated_budget', ['name' => $budget->name])); + $this->repository->cleanupBudgets(); + app('preferences')->mark(); + + $redirect = redirect($this->getPreviousUri('budgets.edit.uri')); + + if (1 === (int)$request->get('return_to_edit')) { + // @codeCoverageIgnoreStart + $request->session()->put('budgets.edit.fromUpdate', true); + + $redirect = redirect(route('budgets.edit', [$budget->id]))->withInput(['return_to_edit' => 1]); + // @codeCoverageIgnoreEnd + } + + return $redirect; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Budget/IndexController.php b/app/Http/Controllers/Budget/IndexController.php new file mode 100644 index 0000000000..b53187eeaa --- /dev/null +++ b/app/Http/Controllers/Budget/IndexController.php @@ -0,0 +1,174 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Budget; + + +use Carbon\Carbon; +use Exception; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; +use Log; +use Preferences; +use View; + +/** + * + * Class IndexController + */ +class IndexController extends Controller +{ + + /** @var BudgetRepositoryInterface */ + private $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + + View::share('hideBudgets', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', trans('firefly.budgets')); + app('view')->share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); + } + + + /** + * @param Request $request + * @param string|null $moment + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function index(Request $request, string $moment = null) + { + $range = Preferences::get('viewRange', '1M')->data; + $start = session('start', new Carbon); + $end = session('end', new Carbon); + $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); + $pageSize = (int)Preferences::get('listPageSize', 50)->data; + $days = 0; + $daysInMonth = 0; + + // make date if present: + if (null !== $moment || '' !== (string)$moment) { + try { + $start = new Carbon($moment); + $end = app('navigation')->endOfPeriod($start, $range); + } catch (Exception $e) { + // start and end are already defined. + Log::debug(sprintf('start and end are already defined: %s', $e->getMessage())); + } + } + + // if today is between start and end, use the diff in days between end and today (days left) + // otherwise, use diff between start and end. + $today = new Carbon; + if ($today->gte($start) && $today->lte($end)) { + $days = $end->diffInDays($today); + $daysInMonth = $start->diffInDays($today); + } + if ($today->lte($start) || $today->gte($end)) { + $days = $start->diffInDays($end); + $daysInMonth = $start->diffInDays($end); + } + $days = 0 === $days ? 1 : $days; + $daysInMonth = 0 === $daysInMonth ? 1 : $daysInMonth; + + + $next = clone $end; + $next->addDay(); + $prev = clone $start; + $prev->subDay(); + $prev = app('navigation')->startOfPeriod($prev, $range); + $this->repository->cleanupBudgets(); + $allBudgets = $this->repository->getActiveBudgets(); + $total = $allBudgets->count(); + $budgets = $allBudgets->slice(($page - 1) * $pageSize, $pageSize); + $inactive = $this->repository->getInactiveBudgets(); + $periodStart = $start->formatLocalized($this->monthAndDayFormat); + $periodEnd = $end->formatLocalized($this->monthAndDayFormat); + $budgetInformation = $this->repository->collectBudgetInformation($allBudgets, $start, $end); + $defaultCurrency = app('amount')->getDefaultCurrency(); + $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); + $spent = array_sum(array_column($budgetInformation, 'spent')); + $budgeted = array_sum(array_column($budgetInformation, 'budgeted')); + + // paginate budgets + $budgets = new LengthAwarePaginator($budgets, $total, $pageSize, $page); + $budgets->setPath(route('budgets.index')); + + // select thing for last 12 periods: + $previousLoop = []; + /** @var Carbon $previousDate */ + $previousDate = clone $start; + $count = 0; + while ($count < 12) { + $previousDate->subDay(); + $previousDate = app('navigation')->startOfPeriod($previousDate, $range); + $format = $previousDate->format('Y-m-d'); + $previousLoop[$format] = app('navigation')->periodShow($previousDate, $range); + ++$count; + } + + // select thing for next 12 periods: + $nextLoop = []; + /** @var Carbon $nextDate */ + $nextDate = clone $end; + $nextDate->addDay(); + $count = 0; + + while ($count < 12) { + $format = $nextDate->format('Y-m-d'); + $nextLoop[$format] = app('navigation')->periodShow($nextDate, $range); + $nextDate = app('navigation')->endOfPeriod($nextDate, $range); + ++$count; + $nextDate->addDay(); + } + + // display info + $currentMonth = app('navigation')->periodShow($start, $range); + $nextText = app('navigation')->periodShow($next, $range); + $prevText = app('navigation')->periodShow($prev, $range); + + return view( + 'budgets.index', compact( + 'available', 'currentMonth', 'next', 'nextText', 'prev', 'allBudgets', 'prevText', 'periodStart', 'periodEnd', 'days', 'page', + 'budgetInformation', 'daysInMonth', + 'inactive', 'budgets', 'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start', 'end' + ) + ); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Budget/ShowController.php b/app/Http/Controllers/Budget/ShowController.php new file mode 100644 index 0000000000..aa97660c37 --- /dev/null +++ b/app/Http/Controllers/Budget/ShowController.php @@ -0,0 +1,274 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Budget; + + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\CacheProperties; +use Illuminate\Http\Request; +use Illuminate\Support\Collection; +use Preferences; +use View; + +/** + * + * Class ShowController + */ +class ShowController extends Controller +{ + + /** @var BudgetRepositoryInterface */ + private $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + + View::share('hideBudgets', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', trans('firefly.budgets')); + app('view')->share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param Request $request + * @param JournalRepositoryInterface $repository + * @param string|null $moment + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function noBudget(Request $request, JournalRepositoryInterface $repository, string $moment = null) + { + // default values: + $moment = $moment ?? ''; + $range = Preferences::get('viewRange', '1M')->data; + $start = null; + $end = null; + $periods = new Collection; + + // prep for "all" view. + if ('all' === $moment) { + $subTitle = trans('firefly.all_journals_without_budget'); + $first = $repository->firstNull(); + $start = null === $first ? new Carbon : $first->date; + $end = new Carbon; + } + + // prep for "specific date" view. + if ('all' !== $moment && \strlen($moment) > 0) { + $start = new Carbon($moment); + /** @var Carbon $end */ + $end = app('navigation')->endOfPeriod($start, $range); + $subTitle = trans( + 'firefly.without_budget_between', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ); + $periods = $this->getPeriodOverview(); + } + + // prep for current period + if ('' === $moment) { + $start = clone session('start', app('navigation')->startOfPeriod(new Carbon, $range)); + $end = clone session('end', app('navigation')->endOfPeriod(new Carbon, $range)); + $periods = $this->getPeriodOverview(); + $subTitle = trans( + 'firefly.without_budget_between', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ); + } + + $page = (int)$request->get('page'); + $pageSize = (int)Preferences::get('listPageSize', 50)->data; + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page) + ->withoutBudget()->withOpposingAccount(); + $transactions = $collector->getPaginatedJournals(); + $transactions->setPath(route('budgets.no-budget')); + + return view('budgets.no-budget', compact('transactions', 'subTitle', 'moment', 'periods', 'start', 'end')); + } + + /** + * @param Request $request + * @param Budget $budget + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function show(Request $request, Budget $budget) + { + /** @var Carbon $start */ + $start = session('first', Carbon::create()->startOfYear()); + $end = new Carbon; + $page = (int)$request->get('page'); + $pageSize = (int)Preferences::get('listPageSize', 50)->data; + $limits = $this->getLimits($budget, $start, $end); + $repetition = null; + + // collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end)->setBudget($budget)->setLimit($pageSize)->setPage($page)->withBudgetInformation(); + $transactions = $collector->getPaginatedJournals(); + $transactions->setPath(route('budgets.show', [$budget->id])); + + $subTitle = trans('firefly.all_journals_for_budget', ['name' => $budget->name]); + + return view('budgets.show', compact('limits', 'budget', 'repetition', 'transactions', 'subTitle')); + } + + /** + * @param Request $request + * @param Budget $budget + * @param BudgetLimit $budgetLimit + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws FireflyException + */ + public function showByBudgetLimit(Request $request, Budget $budget, BudgetLimit $budgetLimit) + { + if ($budgetLimit->budget->id !== $budget->id) { + throw new FireflyException('This budget limit is not part of this budget.'); + } + + $page = (int)$request->get('page'); + $pageSize = (int)Preferences::get('listPageSize', 50)->data; + $subTitle = trans( + 'firefly.budget_in_period', + [ + 'name' => $budget->name, + 'start' => $budgetLimit->start_date->formatLocalized($this->monthAndDayFormat), + 'end' => $budgetLimit->end_date->formatLocalized($this->monthAndDayFormat), + ] + ); + + // collector: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($budgetLimit->start_date, $budgetLimit->end_date) + ->setBudget($budget)->setLimit($pageSize)->setPage($page)->withBudgetInformation(); + $transactions = $collector->getPaginatedJournals(); + $transactions->setPath(route('budgets.show', [$budget->id, $budgetLimit->id])); + $start = session('first', Carbon::create()->startOfYear()); + $end = new Carbon; + $limits = $this->getLimits($budget, $start, $end); + + return view('budgets.show', compact('limits', 'budget', 'budgetLimit', 'transactions', 'subTitle')); + } + + /** + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function getLimits(Budget $budget, Carbon $start, Carbon $end): Collection + { + // properties for cache + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($budget->id); + $cache->addProperty('get-limits'); + + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + + $set = $this->repository->getBudgetLimits($budget, $start, $end); + $limits = new Collection(); + + /** @var BudgetLimit $entry */ + foreach ($set as $entry) { + $entry->spent = $this->repository->spentInPeriod(new Collection([$budget]), new Collection(), $entry->start_date, $entry->end_date); + $limits->push($entry); + } + $cache->store($limits); + + return $set; + } + + + /** + * @return Collection + */ + private function getPeriodOverview(): Collection + { + /** @var JournalRepositoryInterface $repository */ + $repository = app(JournalRepositoryInterface::class); + $first = $repository->firstNull(); + $start = null === $first ? new Carbon : $first->date; + $range = Preferences::get('viewRange', '1M')->data; + $start = app('navigation')->startOfPeriod($start, $range); + $end = app('navigation')->endOfX(new Carbon, $range, null); + $entries = new Collection; + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('no-budget-period-entries'); + + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + $dates = app('navigation')->blockPeriods($start, $end, $range); + foreach ($dates as $date) { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($date['start'], $date['end'])->withoutBudget()->withOpposingAccount()->setTypes( + [TransactionType::WITHDRAWAL] + ); + $set = $collector->getJournals(); + $sum = (string)($set->sum('transaction_amount') ?? '0'); + $journals = $set->count(); + /** @noinspection PhpUndefinedMethodInspection */ + $dateStr = $date['end']->format('Y-m-d'); + $dateName = app('navigation')->periodShow($date['end'], $date['period']); + $entries->push(['string' => $dateStr, 'name' => $dateName, 'count' => $journals, 'sum' => $sum, 'date' => clone $date['end']]); + } + $cache->store($entries); + + return $entries; + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php deleted file mode 100644 index e3a1ce7b0d..0000000000 --- a/app/Http/Controllers/BudgetController.php +++ /dev/null @@ -1,714 +0,0 @@ -. - */ -declare(strict_types=1); - -namespace FireflyIII\Http\Controllers; - -use Carbon\Carbon; -use Exception; -use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; -use FireflyIII\Http\Requests\BudgetFormRequest; -use FireflyIII\Http\Requests\BudgetIncomeRequest; -use FireflyIII\Models\Budget; -use FireflyIII\Models\BudgetLimit; -use FireflyIII\Models\TransactionType; -use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; -use FireflyIII\Repositories\Journal\JournalRepositoryInterface; -use FireflyIII\Support\CacheProperties; -use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; -use Log; -use Preferences; -use View; - -/** - * Class BudgetController. - * - */ -class BudgetController extends Controller -{ - /** @var BudgetRepositoryInterface */ - private $repository; - - /** - * - */ - public function __construct() - { - parent::__construct(); - - View::share('hideBudgets', true); - - $this->middleware( - function ($request, $next) { - app('view')->share('title', trans('firefly.budgets')); - app('view')->share('mainTitleIcon', 'fa-tasks'); - $this->repository = app(BudgetRepositoryInterface::class); - - return $next($request); - } - ); - } - - /** - * @param Request $request - * @param BudgetRepositoryInterface $repository - * @param Budget $budget - * - * @return JsonResponse - */ - public function amount(Request $request, BudgetRepositoryInterface $repository, Budget $budget): JsonResponse - { - $amount = (string)$request->get('amount'); - $start = Carbon::createFromFormat('Y-m-d', $request->get('start')); - $end = Carbon::createFromFormat('Y-m-d', $request->get('end')); - $budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount); - $largeDiff = false; - $warnText = ''; - $days = 0; - $daysInMonth = 0; - if (0 === bccomp($amount, '0')) { - $budgetLimit = null; - } - - // if today is between start and end, use the diff in days between end and today (days left) - // otherwise, use diff between start and end. - $today = new Carbon; - Log::debug(sprintf('Start is %s, end is %s, today is %s', $start->format('Y-m-d'), $end->format('Y-m-d'), $today->format('Y-m-d'))); - if ($today->gte($start) && $today->lte($end)) { - $days = $end->diffInDays($today); - $daysInMonth = $start->diffInDays($today); - } - if ($today->lte($start) || $today->gte($end)) { - $days = $start->diffInDays($end); - $daysInMonth = $start->diffInDays($end); - } - $days = 0 === $days ? 1 : $days; - $daysInMonth = 0 === $daysInMonth ? 1 : $daysInMonth; - - // calculate left in budget: - $spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); - $currency = app('amount')->getDefaultCurrency(); - $left = app('amount')->formatAnything($currency, bcadd($amount, $spent), true); - $leftPerDay = 'none'; - - // is user has money left, calculate. - if (1 === bccomp(bcadd($amount, $spent), '0')) { - $leftPerDay = app('amount')->formatAnything($currency, bcdiv(bcadd($amount, $spent), (string)$days), true); - } - - - // over or under budgeting, compared to previous budgets? - $average = $this->repository->budgetedPerDay($budget); - // current average per day: - $diff = $start->diffInDays($end); - $current = $amount; - if ($diff > 0) { - $current = bcdiv($amount, (string)$diff); - } - if (bccomp(bcmul('1.1', $average), $current) === -1) { - $largeDiff = true; - $warnText = (string)trans( - 'firefly.over_budget_warn', - [ - 'amount' => app('amount')->formatAnything($currency, $average, false), - 'over_amount' => app('amount')->formatAnything($currency, $current, false), - ] - ); - } - - app('preferences')->mark(); - - return response()->json( - [ - 'left' => $left, - 'name' => $budget->name, - 'limit' => $budgetLimit ? $budgetLimit->id : 0, - 'amount' => $amount, - 'current' => $current, - 'average' => $average, - 'large_diff' => $largeDiff, - 'left_per_day' => $leftPerDay, - 'warn_text' => $warnText, - 'daysInMonth' => $daysInMonth, - - ] - ); - } - - /** - * @param Request $request - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function create(Request $request) - { - // put previous url in session if not redirect from store (not "create another"). - if (true !== session('budgets.create.fromStore')) { - $this->rememberPreviousUri('budgets.create.uri'); - } - $request->session()->forget('budgets.create.fromStore'); - $subTitle = (string)trans('firefly.create_new_budget'); - - return view('budgets.create', compact('subTitle')); - } - - /** - * @param Budget $budget - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function delete(Budget $budget) - { - $subTitle = trans('firefly.delete_budget', ['name' => $budget->name]); - - // put previous url in session - $this->rememberPreviousUri('budgets.delete.uri'); - - return view('budgets.delete', compact('budget', 'subTitle')); - } - - /** - * @param Request $request - * @param Budget $budget - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - */ - public function destroy(Request $request, Budget $budget) - { - $name = $budget->name; - $this->repository->destroy($budget); - $request->session()->flash('success', (string)trans('firefly.deleted_budget', ['name' => $name])); - app('preferences')->mark(); - - return redirect($this->getPreviousUri('budgets.delete.uri')); - } - - /** - * @param Request $request - * @param Budget $budget - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function edit(Request $request, Budget $budget) - { - $subTitle = trans('firefly.edit_budget', ['name' => $budget->name]); - - // code to handle active-checkboxes - $hasOldInput = null !== $request->old('_token'); - $preFilled = [ - 'active' => $hasOldInput ? (bool)$request->old('active') : $budget->active, - ]; - - // put previous url in session if not redirect from store (not "return_to_edit"). - if (true !== session('budgets.edit.fromUpdate')) { - $this->rememberPreviousUri('budgets.edit.uri'); - } - $request->session()->forget('budgets.edit.fromUpdate'); - $request->session()->flash('preFilled', $preFilled); - - return view('budgets.edit', compact('budget', 'subTitle')); - } - - /** - * @param Request $request - * @param string|null $moment - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function index(Request $request, string $moment = null) - { - $range = Preferences::get('viewRange', '1M')->data; - $start = session('start', new Carbon); - $end = session('end', new Carbon); - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $pageSize = (int)Preferences::get('listPageSize', 50)->data; - $days = 0; - $daysInMonth = 0; - - // make date if present: - if (null !== $moment || '' !== (string)$moment) { - try { - $start = new Carbon($moment); - $end = app('navigation')->endOfPeriod($start, $range); - } catch (Exception $e) { - // start and end are already defined. - Log::debug(sprintf('start and end are already defined: %s', $e->getMessage())); - } - } - - // if today is between start and end, use the diff in days between end and today (days left) - // otherwise, use diff between start and end. - $today = new Carbon; - if ($today->gte($start) && $today->lte($end)) { - $days = $end->diffInDays($today); - $daysInMonth = $start->diffInDays($today); - } - if ($today->lte($start) || $today->gte($end)) { - $days = $start->diffInDays($end); - $daysInMonth = $start->diffInDays($end); - } - $days = 0 === $days ? 1 : $days; - $daysInMonth = 0 === $daysInMonth ? 1 : $daysInMonth; - - - $next = clone $end; - $next->addDay(); - $prev = clone $start; - $prev->subDay(); - $prev = app('navigation')->startOfPeriod($prev, $range); - $this->repository->cleanupBudgets(); - $allBudgets = $this->repository->getActiveBudgets(); - $total = $allBudgets->count(); - $budgets = $allBudgets->slice(($page - 1) * $pageSize, $pageSize); - $inactive = $this->repository->getInactiveBudgets(); - $periodStart = $start->formatLocalized($this->monthAndDayFormat); - $periodEnd = $end->formatLocalized($this->monthAndDayFormat); - $budgetInformation = $this->repository->collectBudgetInformation($allBudgets, $start, $end); - $defaultCurrency = app('amount')->getDefaultCurrency(); - $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); - $spent = array_sum(array_column($budgetInformation, 'spent')); - $budgeted = array_sum(array_column($budgetInformation, 'budgeted')); - - // paginate budgets - $budgets = new LengthAwarePaginator($budgets, $total, $pageSize, $page); - $budgets->setPath(route('budgets.index')); - - // select thing for last 12 periods: - $previousLoop = []; - /** @var Carbon $previousDate */ - $previousDate = clone $start; - $count = 0; - while ($count < 12) { - $previousDate->subDay(); - $previousDate = app('navigation')->startOfPeriod($previousDate, $range); - $format = $previousDate->format('Y-m-d'); - $previousLoop[$format] = app('navigation')->periodShow($previousDate, $range); - ++$count; - } - - // select thing for next 12 periods: - $nextLoop = []; - /** @var Carbon $nextDate */ - $nextDate = clone $end; - $nextDate->addDay(); - $count = 0; - - while ($count < 12) { - $format = $nextDate->format('Y-m-d'); - $nextLoop[$format] = app('navigation')->periodShow($nextDate, $range); - $nextDate = app('navigation')->endOfPeriod($nextDate, $range); - ++$count; - $nextDate->addDay(); - } - - // display info - $currentMonth = app('navigation')->periodShow($start, $range); - $nextText = app('navigation')->periodShow($next, $range); - $prevText = app('navigation')->periodShow($prev, $range); - - return view( - 'budgets.index', compact( - 'available', 'currentMonth', 'next', 'nextText', 'prev', 'allBudgets', 'prevText', 'periodStart', 'periodEnd', 'days', 'page', - 'budgetInformation', 'daysInMonth', - 'inactive', 'budgets', 'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start', 'end' - ) - ); - } - - - /** - * @param Carbon $start - * @param Carbon $end - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function infoIncome(Carbon $start, Carbon $end) - { - // properties for cache - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('info-income'); - - Log::debug(sprintf('infoIncome start is %s', $start->format('Y-m-d'))); - Log::debug(sprintf('infoIncome end is %s', $end->format('Y-m-d'))); - - if ($cache->has()) { - // @codeCoverageIgnoreStart - $result = $cache->get(); - - return view('budgets.info', compact('result')); - // @codeCoverageIgnoreEnd - } - $result = [ - 'available' => '0', - 'earned' => '0', - 'suggested' => '0', - ]; - $currency = app('amount')->getDefaultCurrency(); - $range = Preferences::get('viewRange', '1M')->data; - /** @var Carbon $begin */ - $begin = app('navigation')->subtractPeriod($start, $range, 3); - - Log::debug(sprintf('Range is %s', $range)); - Log::debug(sprintf('infoIncome begin is %s', $begin->format('Y-m-d'))); - - // get average amount available. - $total = '0'; - $count = 0; - $currentStart = clone $begin; - while ($currentStart < $start) { - - Log::debug(sprintf('Loop: currentStart is %s', $currentStart->format('Y-m-d'))); - $currentEnd = app('navigation')->endOfPeriod($currentStart, $range); - $total = bcadd($total, $this->repository->getAvailableBudget($currency, $currentStart, $currentEnd)); - $currentStart = app('navigation')->addPeriod($currentStart, $range, 0); - ++$count; - } - Log::debug('Loop end'); - - if (0 === $count) { - $count = 1; - } - $result['available'] = bcdiv($total, (string)$count); - - // amount earned in this period: - $subDay = clone $end; - $subDay->subDay(); - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($begin, $subDay)->setTypes([TransactionType::DEPOSIT])->withOpposingAccount(); - $result['earned'] = bcdiv((string)$collector->getJournals()->sum('transaction_amount'), (string)$count); - - // amount spent in period - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($begin, $subDay)->setTypes([TransactionType::WITHDRAWAL])->withOpposingAccount(); - $result['spent'] = bcdiv((string)$collector->getJournals()->sum('transaction_amount'), (string)$count); - // suggestion starts with the amount spent - $result['suggested'] = bcmul($result['spent'], '-1'); - $result['suggested'] = 1 === bccomp($result['suggested'], $result['earned']) ? $result['earned'] : $result['suggested']; - // unless it's more than you earned. So min() of suggested/earned - - $cache->store($result); - - return view('budgets.info', compact('result', 'begin', 'currentEnd')); - } - - /** - * @param Request $request - * @param JournalRepositoryInterface $repository - * @param string|null $moment - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function noBudget(Request $request, JournalRepositoryInterface $repository, string $moment = null) - { - // default values: - $moment = $moment ?? ''; - $range = Preferences::get('viewRange', '1M')->data; - $start = null; - $end = null; - $periods = new Collection; - - // prep for "all" view. - if ('all' === $moment) { - $subTitle = trans('firefly.all_journals_without_budget'); - $first = $repository->firstNull(); - $start = null === $first ? new Carbon : $first->date; - $end = new Carbon; - } - - // prep for "specific date" view. - if ('all' !== $moment && \strlen($moment) > 0) { - $start = new Carbon($moment); - /** @var Carbon $end */ - $end = app('navigation')->endOfPeriod($start, $range); - $subTitle = trans( - 'firefly.without_budget_between', - ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] - ); - $periods = $this->getPeriodOverview(); - } - - // prep for current period - if ('' === $moment) { - $start = clone session('start', app('navigation')->startOfPeriod(new Carbon, $range)); - $end = clone session('end', app('navigation')->endOfPeriod(new Carbon, $range)); - $periods = $this->getPeriodOverview(); - $subTitle = trans( - 'firefly.without_budget_between', - ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] - ); - } - - $page = (int)$request->get('page'); - $pageSize = (int)Preferences::get('listPageSize', 50)->data; - - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page) - ->withoutBudget()->withOpposingAccount(); - $transactions = $collector->getPaginatedJournals(); - $transactions->setPath(route('budgets.no-budget')); - - return view('budgets.no-budget', compact('transactions', 'subTitle', 'moment', 'periods', 'start', 'end')); - } - - /** - * @param BudgetIncomeRequest $request - * - * @return RedirectResponse - */ - public function postUpdateIncome(BudgetIncomeRequest $request): RedirectResponse - { - $start = Carbon::createFromFormat('Y-m-d', $request->string('start')); - $end = Carbon::createFromFormat('Y-m-d', $request->string('end')); - $defaultCurrency = app('amount')->getDefaultCurrency(); - $amount = $request->get('amount'); - $page = 0 === $request->integer('page') ? 1 : $request->integer('page'); - $this->repository->cleanupBudgets(); - $this->repository->setAvailableBudget($defaultCurrency, $start, $end, $amount); - app('preferences')->mark(); - - return redirect(route('budgets.index', [$start->format('Y-m-d')]) . '?page=' . $page); - } - - /** - * @param Request $request - * @param Budget $budget - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function show(Request $request, Budget $budget) - { - /** @var Carbon $start */ - $start = session('first', Carbon::create()->startOfYear()); - $end = new Carbon; - $page = (int)$request->get('page'); - $pageSize = (int)Preferences::get('listPageSize', 50)->data; - $limits = $this->getLimits($budget, $start, $end); - $repetition = null; - - // collector: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($start, $end)->setBudget($budget)->setLimit($pageSize)->setPage($page)->withBudgetInformation(); - $transactions = $collector->getPaginatedJournals(); - $transactions->setPath(route('budgets.show', [$budget->id])); - - $subTitle = trans('firefly.all_journals_for_budget', ['name' => $budget->name]); - - return view('budgets.show', compact('limits', 'budget', 'repetition', 'transactions', 'subTitle')); - } - - /** - * @param Request $request - * @param Budget $budget - * @param BudgetLimit $budgetLimit - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - * @throws FireflyException - */ - public function showByBudgetLimit(Request $request, Budget $budget, BudgetLimit $budgetLimit) - { - if ($budgetLimit->budget->id !== $budget->id) { - throw new FireflyException('This budget limit is not part of this budget.'); - } - - $page = (int)$request->get('page'); - $pageSize = (int)Preferences::get('listPageSize', 50)->data; - $subTitle = trans( - 'firefly.budget_in_period', - [ - 'name' => $budget->name, - 'start' => $budgetLimit->start_date->formatLocalized($this->monthAndDayFormat), - 'end' => $budgetLimit->end_date->formatLocalized($this->monthAndDayFormat), - ] - ); - - // collector: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($budgetLimit->start_date, $budgetLimit->end_date) - ->setBudget($budget)->setLimit($pageSize)->setPage($page)->withBudgetInformation(); - $transactions = $collector->getPaginatedJournals(); - $transactions->setPath(route('budgets.show', [$budget->id, $budgetLimit->id])); - $start = session('first', Carbon::create()->startOfYear()); - $end = new Carbon; - $limits = $this->getLimits($budget, $start, $end); - - return view('budgets.show', compact('limits', 'budget', 'budgetLimit', 'transactions', 'subTitle')); - } - - /** - * @param BudgetFormRequest $request - * - * @return \Illuminate\Http\RedirectResponse - */ - public function store(BudgetFormRequest $request): RedirectResponse - { - $data = $request->getBudgetData(); - $budget = $this->repository->store($data); - $this->repository->cleanupBudgets(); - $request->session()->flash('success', (string)trans('firefly.stored_new_budget', ['name' => $budget->name])); - app('preferences')->mark(); - - $redirect = redirect($this->getPreviousUri('budgets.create.uri')); - - if (1 === (int)$request->get('create_another')) { - // @codeCoverageIgnoreStart - $request->session()->put('budgets.create.fromStore', true); - - $redirect = redirect(route('budgets.create'))->withInput(); - // @codeCoverageIgnoreEnd - } - - return $redirect; - } - - - /** - * @param BudgetFormRequest $request - * @param Budget $budget - * - * @return \Illuminate\Http\RedirectResponse - */ - public function update(BudgetFormRequest $request, Budget $budget): RedirectResponse - { - $data = $request->getBudgetData(); - $this->repository->update($budget, $data); - - $request->session()->flash('success', (string)trans('firefly.updated_budget', ['name' => $budget->name])); - $this->repository->cleanupBudgets(); - app('preferences')->mark(); - - $redirect = redirect($this->getPreviousUri('budgets.edit.uri')); - - if (1 === (int)$request->get('return_to_edit')) { - // @codeCoverageIgnoreStart - $request->session()->put('budgets.edit.fromUpdate', true); - - $redirect = redirect(route('budgets.edit', [$budget->id]))->withInput(['return_to_edit' => 1]); - // @codeCoverageIgnoreEnd - } - - return $redirect; - } - - /** - * @param Request $request - * @param Carbon $start - * @param Carbon $end - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function updateIncome(Request $request, Carbon $start, Carbon $end) - { - $defaultCurrency = app('amount')->getDefaultCurrency(); - $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); - $available = round($available, $defaultCurrency->decimal_places); - $page = (int)$request->get('page'); - - return view('budgets.income', compact('available', 'start', 'end', 'page')); - } - - - /** - * @param Budget $budget - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - private function getLimits(Budget $budget, Carbon $start, Carbon $end): Collection - { - // properties for cache - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($budget->id); - $cache->addProperty('get-limits'); - - if ($cache->has()) { - return $cache->get(); // @codeCoverageIgnore - } - - $set = $this->repository->getBudgetLimits($budget, $start, $end); - $limits = new Collection(); - - /** @var BudgetLimit $entry */ - foreach ($set as $entry) { - $entry->spent = $this->repository->spentInPeriod(new Collection([$budget]), new Collection(), $entry->start_date, $entry->end_date); - $limits->push($entry); - } - $cache->store($limits); - - return $set; - } - - - /** - * @return Collection - */ - private function getPeriodOverview(): Collection - { - /** @var JournalRepositoryInterface $repository */ - $repository = app(JournalRepositoryInterface::class); - $first = $repository->firstNull(); - $start = null === $first ? new Carbon : $first->date; - $range = Preferences::get('viewRange', '1M')->data; - $start = app('navigation')->startOfPeriod($start, $range); - $end = app('navigation')->endOfX(new Carbon, $range, null); - $entries = new Collection; - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('no-budget-period-entries'); - - if ($cache->has()) { - return $cache->get(); // @codeCoverageIgnore - } - $dates = app('navigation')->blockPeriods($start, $end, $range); - foreach ($dates as $date) { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($date['start'], $date['end'])->withoutBudget()->withOpposingAccount()->setTypes( - [TransactionType::WITHDRAWAL] - ); - $set = $collector->getJournals(); - $sum = (string)($set->sum('transaction_amount') ?? '0'); - $journals = $set->count(); - /** @noinspection PhpUndefinedMethodInspection */ - $dateStr = $date['end']->format('Y-m-d'); - $dateName = app('navigation')->periodShow($date['end'], $date['period']); - $entries->push(['string' => $dateStr, 'name' => $dateName, 'count' => $journals, 'sum' => $sum, 'date' => clone $date['end']]); - } - $cache->store($entries); - - return $entries; - } -} diff --git a/routes/web.php b/routes/web.php index a3deac49cd..d971cf5344 100755 --- a/routes/web.php +++ b/routes/web.php @@ -190,22 +190,31 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'budgets', 'as' => 'budgets.'], function () { - Route::get('income/{start_date}/{end_date}', ['uses' => 'BudgetController@updateIncome', 'as' => 'income']); - Route::get('info/{start_date}/{end_date}', ['uses' => 'BudgetController@infoIncome', 'as' => 'income.info']); - Route::get('create', ['uses' => 'BudgetController@create', 'as' => 'create']); - Route::get('edit/{budget}', ['uses' => 'BudgetController@edit', 'as' => 'edit']); - Route::get('delete/{budget}', ['uses' => 'BudgetController@delete', 'as' => 'delete']); - Route::get('show/{budget}', ['uses' => 'BudgetController@show', 'as' => 'show']); - Route::get('show/{budget}/{budgetLimit}', ['uses' => 'BudgetController@showByBudgetLimit', 'as' => 'show.limit']); - Route::get('list/no-budget/{moment?}', ['uses' => 'BudgetController@noBudget', 'as' => 'no-budget']); - Route::get('{moment?}', ['uses' => 'BudgetController@index', 'as' => 'index']); + // delete + Route::get('delete/{budget}', ['uses' => 'Budget\DeleteController@delete', 'as' => 'delete']); + Route::post('destroy/{budget}', ['uses' => 'Budget\DeleteController@destroy', 'as' => 'destroy']); + // create + Route::get('create', ['uses' => 'Budget\CreateController@create', 'as' => 'create']); + Route::post('store', ['uses' => 'Budget\CreateController@store', 'as' => 'store']); - Route::post('income', ['uses' => 'BudgetController@postUpdateIncome', 'as' => 'income.post']); - Route::post('store', ['uses' => 'BudgetController@store', 'as' => 'store']); - Route::post('update/{budget}', ['uses' => 'BudgetController@update', 'as' => 'update']); - Route::post('destroy/{budget}', ['uses' => 'BudgetController@destroy', 'as' => 'destroy']); - Route::post('amount/{budget}', ['uses' => 'BudgetController@amount', 'as' => 'amount']); + // edit + Route::get('edit/{budget}', ['uses' => 'Budget\EditController@edit', 'as' => 'edit']); + Route::post('update/{budget}', ['uses' => 'Budget\EditController@update', 'as' => 'update']); + + // show + Route::get('show/{budget}', ['uses' => 'Budget\ShowController@show', 'as' => 'show']); + Route::get('show/{budget}/{budgetLimit}', ['uses' => 'Budget\ShowController@showByBudgetLimit', 'as' => 'show.limit']); + Route::get('list/no-budget/{moment?}', ['uses' => 'Budget\ShowController@noBudget', 'as' => 'no-budget']); + + // index + Route::get('{moment?}', ['uses' => 'Budget\IndexController@index', 'as' => 'index']); + + // update budget amount and income amount + Route::get('income/{start_date}/{end_date}', ['uses' => 'Budget\AmountController@updateIncome', 'as' => 'income']); + Route::get('info/{start_date}/{end_date}', ['uses' => 'Budget\AmountController@infoIncome', 'as' => 'income.info']); + Route::post('income', ['uses' => 'Budget\AmountController@postUpdateIncome', 'as' => 'income.post']); + Route::post('amount/{budget}', ['uses' => 'Budget\AmountController@amount', 'as' => 'amount']); } ); diff --git a/tests/Feature/Controllers/Budget/AmountControllerTest.php b/tests/Feature/Controllers/Budget/AmountControllerTest.php new file mode 100644 index 0000000000..e42ea09c1e --- /dev/null +++ b/tests/Feature/Controllers/Budget/AmountControllerTest.php @@ -0,0 +1,220 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Feature\Controllers\Budget; + + +use Carbon\Carbon; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Illuminate\Support\Collection; +use Log; +use Tests\TestCase; + +/** + * + * Class AmountControllerTest + */ +class AmountControllerTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::debug(sprintf('Now in %s.', \get_class($this))); + } + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + */ + public function testAmount(): void + { + Log::debug('Now in testAmount()'); + // mock stuff + $repository = $this->mock(BudgetRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + $repository->shouldReceive('updateLimitAmount')->andReturn(new BudgetLimit); + $repository->shouldReceive('spentInPeriod')->andReturn('0'); + $repository->shouldReceive('budgetedPerDay')->andReturn('10'); + + + $data = ['amount' => 200, 'start' => '2017-01-01', 'end' => '2017-01-31']; + $this->be($this->user()); + $response = $this->post(route('budgets.amount', [1]), $data); + $response->assertStatus(200); + } + + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + */ + public function testAmountLargeDiff(): void + { + Log::debug('Now in testAmount()'); + // mock stuff + $repository = $this->mock(BudgetRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + $repository->shouldReceive('updateLimitAmount')->andReturn(new BudgetLimit); + $repository->shouldReceive('spentInPeriod')->andReturn('0'); + $repository->shouldReceive('budgetedPerDay')->andReturn('10'); + + + $data = ['amount' => 20000, 'start' => '2017-01-01', 'end' => '2017-01-31']; + $this->be($this->user()); + $response = $this->post(route('budgets.amount', [1]), $data); + $response->assertStatus(200); + $response->assertSee('Normally you budget about \u20ac10.00 per day.'); + } + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + */ + public function testAmountOutOfRange(): void + { + Log::debug('Now in testAmountOutOfRange()'); + // mock stuff + $repository = $this->mock(BudgetRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + $repository->shouldReceive('updateLimitAmount')->andReturn(new BudgetLimit); + $repository->shouldReceive('spentInPeriod')->andReturn('0'); + $repository->shouldReceive('budgetedPerDay')->andReturn('10'); + + $today = new Carbon; + $start = $today->startOfMonth()->format('Y-m-d'); + $end = $today->endOfMonth()->format('Y-m-d'); + $data = ['amount' => 200, 'start' => $start, 'end' => $end]; + $this->be($this->user()); + $response = $this->post(route('budgets.amount', [1]), $data); + $response->assertStatus(200); + } + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + */ + public function testAmountZero(): void + { + Log::debug('Now in testAmountZero()'); + // mock stuff + $repository = $this->mock(BudgetRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + $repository->shouldReceive('updateLimitAmount')->andReturn(new BudgetLimit); + $repository->shouldReceive('spentInPeriod')->andReturn('0'); + $repository->shouldReceive('budgetedPerDay')->andReturn('10'); + + $data = ['amount' => 0, 'start' => '2017-01-01', 'end' => '2017-01-31']; + $this->be($this->user()); + $response = $this->post(route('budgets.amount', [1]), $data); + $response->assertStatus(200); + } + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + */ + public function testInfoIncome(): void + { + Log::debug('Now in testInfoIncome()'); + // mock stuff + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $repository = $this->mock(BudgetRepositoryInterface::class); + + $repository->shouldReceive('getAvailableBudget')->andReturn('100.123'); + $accountRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('getAccountsByType')->andReturn(new Collection); + + $this->be($this->user()); + $response = $this->get(route('budgets.income.info', ['20170101', '20170131'])); + $response->assertStatus(200); + } + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + * @dataProvider dateRangeProvider + * + * @param string $range + */ + public function testInfoIncomeExpanded(string $range): void + { + Log::debug(sprintf('Now in testInfoIncomeExpanded(%s)', $range)); + // mock stuff + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $repository = $this->mock(BudgetRepositoryInterface::class); + $repository->shouldReceive('getAvailableBudget')->andReturn('100.123'); + $accountRepos->shouldReceive('setUser'); + $accountRepos->shouldReceive('getAccountsByType')->andReturn(new Collection); + + $this->be($this->user()); + $this->changeDateRange($this->user(), $range); + $response = $this->get(route('budgets.income.info', ['20170301', '20170430'])); + $response->assertStatus(200); + } + + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + */ + public function testPostUpdateIncome(): void + { + Log::debug('Now in testPostUpdateIncome()'); + // mock stuff + $repository = $this->mock(BudgetRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + $repository->shouldReceive('setAvailableBudget'); + $repository->shouldReceive('cleanupBudgets'); + + $data = ['amount' => '200', 'start' => '2017-01-01', 'end' => '2017-01-31']; + $this->be($this->user()); + $response = $this->post(route('budgets.income.post'), $data); + $response->assertStatus(302); + } + + + /** + * @covers \FireflyIII\Http\Controllers\Budget\AmountController + */ + public function testUpdateIncome(): void + { + Log::debug('Now in testUpdateIncome()'); + // must be in list + $this->be($this->user()); + + // mock stuff + $repository = $this->mock(BudgetRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + $repository->shouldReceive('getAvailableBudget')->andReturn('1'); + $repository->shouldReceive('cleanupBudgets'); + + $response = $this->get(route('budgets.income', ['2017-01-01', '2017-01-31'])); + $response->assertStatus(200); + } +} \ No newline at end of file diff --git a/tests/Feature/Controllers/Budget/CreateControllerTest.php b/tests/Feature/Controllers/Budget/CreateControllerTest.php new file mode 100644 index 0000000000..3f6c64cd70 --- /dev/null +++ b/tests/Feature/Controllers/Budget/CreateControllerTest.php @@ -0,0 +1,95 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Feature\Controllers\Budget; + + +use FireflyIII\Models\Budget; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Log; +use Tests\TestCase; + +/** + * + * Class CreateControllerTest + */ +class CreateControllerTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::debug(sprintf('Now in %s.', \get_class($this))); + } + + + /** + * @covers \FireflyIII\Http\Controllers\Budget\CreateController + */ + public function testCreate(): void + { + Log::debug('Now in testCreate()'); + // mock stuff + $repository = $this->mock(BudgetRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $journalRepos->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); + + $this->be($this->user()); + $response = $this->get(route('budgets.create')); + $response->assertStatus(200); + // has bread crumb + $response->assertSee('