Expand API with budget limits

This commit is contained in:
James Cole
2018-06-24 13:20:29 +02:00
parent 91701473af
commit 0b9be029ac
12 changed files with 587 additions and 26 deletions

View File

@@ -97,7 +97,7 @@ class AvailableBudgetController extends Controller
// types to get, page size:
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
// get list of accounts. Count it and split it.
// get list of available budgets. Count it and split it.
$collection = $this->repository->getAvailableBudgets();
$count = $collection->count();
$availableBudgets = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);

View File

@@ -0,0 +1,230 @@
<?php
/**
* BudgetLimitController.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers;
use Carbon\Carbon;
use FireflyIII\Api\V1\Requests\AvailableBudgetRequest;
use FireflyIII\Api\V1\Requests\BudgetLimitRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Transformers\BudgetLimitTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
use League\Fractal\Resource\Item;
use League\Fractal\Serializer\JsonApiSerializer;
use Log;
/**
* Class BudgetLimitController
*/
class BudgetLimitController extends Controller
{
///** @var CurrencyRepositoryInterface */
//private $currencyRepository;
/** @var BudgetRepositoryInterface */
private $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$this->repository = app(BudgetRepositoryInterface::class);
//$this->currencyRepository = app(CurrencyRepositoryInterface::class);
$this->repository->setUser($user);
return $next($request);
}
);
}
/**
* Remove the specified resource from storage.
*
* @param BudgetLimit $budgetLimit
*
* @return JsonResponse
*/
public function delete(BudgetLimit $budgetLimit): JsonResponse
{
$this->repository->destroyBudgetLimit($budgetLimit);
return response()->json([], 204);
}
/**
* Display a listing of the resource.
*
* @param Request $request
*
* @return JsonResponse
*/
public function index(Request $request): JsonResponse
{
// create some objects:
$manager = new Manager;
$baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
// read budget from request
$budgetId = (int)($request->get('budget_id') ?? 0);
$budget = null;
if ($budgetId > 0) {
$budget = $this->repository->findNull($budgetId);
}
// read start date from request
$start = null;
try {
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
$this->parameters->set('start', $start->format('Y-m-d'));
} catch (InvalidArgumentException $e) {
Log::debug(sprintf('Could not parse start date "%s": %s', $request->get('start'), $e->getMessage()));
}
// read end date from request
$end = null;
try {
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
$this->parameters->set('end', $end->format('Y-m-d'));
} catch (InvalidArgumentException $e) {
Log::debug(sprintf('Could not parse end date "%s": %s', $request->get('end'), $e->getMessage()));
}
$this->parameters->set('budget_id', $budgetId);
// types to get, page size:
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
// get list of budget limits. Count it and split it.
$collection = new Collection;
if (null === $budget) {
$collection = $this->repository->getAllBudgetLimits($start, $end);
}
if (null !== $budget) {
$collection = $this->repository->getBudgetLimits($budget, $start, $end);
}
$count = $collection->count();
$budgetLimits = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// make paginator:
$paginator = new LengthAwarePaginator($budgetLimits, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.budget_limits.index') . $this->buildParams());
// present to user.
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new FractalCollection($budgetLimits, new BudgetLimitTransformer($this->parameters), 'budget_limits');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
/**
* Display the specified resource.
*
* @param Request $request
* @param BudgetLimit $budgetLimit
*
* @return JsonResponse
*/
public function show(Request $request, BudgetLimit $budgetLimit): JsonResponse
{
$manager = new Manager;
// add include parameter:
$include = $request->get('include') ?? '';
$manager->parseIncludes($include);
$baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($budgetLimit, new BudgetLimitTransformer($this->parameters), 'budget_limits');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
/**
* Store a newly created resource in storage.
*
* @param BudgetLimitRequest $request
*
* @return JsonResponse
* @throws FireflyException
*/
public function store(BudgetLimitRequest $request): JsonResponse
{
$data = $request->getAll();
$budget = $this->repository->findNull($data['budget_id']);
if (null === $budget) {
throw new FireflyException('Unknown budget.');
}
$data['budget'] = $budget;
$budgetLimit = $this->repository->storeBudgetLimit($data);
$manager = new Manager;
$baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($budgetLimit, new BudgetLimitTransformer($this->parameters), 'budget_limits');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
/**
* Update the specified resource in storage.
*
* @param AvailableBudgetRequest $request
* @param BudgetLimit $budgetLimit
*
* @return JsonResponse
*/
public function update(BudgetLimitRequest $request, BudgetLimit $budgetLimit): JsonResponse
{
$data = $request->getAll();
$budget = $this->repository->findNull($data['budget_id']);
if (null === $budget) {
$budget = $budgetLimit->budget;
}
$data['budget'] = $budget;
$budgetLimit = $this->repository->updateBudgetLimit($budgetLimit, $data);
$manager = new Manager;
$baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($budgetLimit, new BudgetLimitTransformer($this->parameters), 'budget_limits');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* BudgetLimitRequest.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
/**
* Class BudgetLimitRequest
*/
class BudgetLimitRequest extends Request
{
/**
* @return bool
*/
public function authorize(): bool
{
// Only allow authenticated users
return auth()->check();
}
/**
* @return array
*/
public function getAll(): array
{
return [
'budget_id' => $this->integer('budget_id'),
'start_date' => $this->date('start_date'),
'end_date' => $this->date('end_date'),
'amount' => $this->string('amount'),
];
}
/**
* @return array
*/
public function rules(): array
{
$rules = [
'budget_id' => 'required|exists:budgets,id|belongsToUser:budgets,id',
'start_date' => 'required|before:end_date|date',
'end_date' => 'required|after:start_date|date',
'amount' => 'required|more:0',
];
switch ($this->method()) {
default:
break;
case 'PUT':
case 'PATCH':
$rules['budget_id'] = 'required|exists:budgets,id|belongsToUser:budgets,id';
break;
}
return $rules;
}
}

View File

@@ -58,8 +58,8 @@ class BillFactory
'date' => $data['date'],
'repeat_freq' => $data['repeat_freq'],
'skip' => $data['skip'],
'automatch' => true,
'active' => $data['active'],
'automatch' => $data['automatch'] ?? true,
'active' => $data['active'] ?? true,
]
);

View File

@@ -49,7 +49,6 @@ class BillFormRequest extends Request
'date' => $this->date('date'),
'repeat_freq' => $this->string('repeat_freq'),
'skip' => $this->integer('skip'),
'active' => $this->boolean('active'),
'notes' => $this->string('notes'),
];
}
@@ -73,8 +72,6 @@ class BillFormRequest extends Request
'date' => 'required|date',
'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly',
'skip' => 'required|between:0,31',
'automatch' => 'in:1',
'active' => 'in:1',
];
return $rules;

View File

@@ -22,11 +22,21 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class BudgetLimit.
*
* @property Budget $budget
* @property int $id
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Carbon $start_date
* @property Carbon $end_date
* @property string $amount
*/
class BudgetLimit extends Model
{
@@ -67,20 +77,10 @@ class BudgetLimit extends Model
/**
* @codeCoverageIgnore
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
* @return BelongsTo
*/
public function budget()
public function budget(): BelongsTo
{
return $this->belongsTo(Budget::class);
}
/**
* @codeCoverageIgnore
*
* @param $value
*/
public function setAmountAttribute($value)
{
$this->attributes['amount'] = (string)round($value, 12);
}
}

View File

@@ -154,6 +154,20 @@ class BudgetRepository implements BudgetRepositoryInterface
return $return;
}
/**
* Deletes a budget limit.
*
* @param BudgetLimit $budgetLimit
*/
public function deleteBudgetLimit(BudgetLimit $budgetLimit): void
{
try {
$budgetLimit->delete();
} catch (Exception $e) {
Log::error(sprintf('Could not delete budget limit: %s', $e->getMessage()));
}
}
/**
* @param Budget $budget
*
@@ -315,8 +329,35 @@ class BudgetRepository implements BudgetRepositoryInterface
*
* @return Collection
*/
public function getAllBudgetLimits(Carbon $start, Carbon $end): Collection
public function getAllBudgetLimits(Carbon $start = null, Carbon $end = null): Collection
{
// both are NULL:
if (null === $start && null === $end) {
$set = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->with(['budget'])
->where('budgets.user_id', $this->user->id)
->get(['budget_limits.*']);
return $set;
}
// one of the two is NULL.
if (null === $start xor null === $end) {
$query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->with(['budget'])
->where('budgets.user_id', $this->user->id);
if (null !== $end) {
// end date must be before $end.
$query->where('end_date', '<=', $end->format('Y-m-d 00:00:00'));
}
if (null !== $start) {
// start date must be after $start.
$query->where('start_date', '>=', $start->format('Y-m-d 00:00:00'));
}
$set = $query->get(['budget_limits.*']);
return $set;
}
// neither are NULL:
$set = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->with(['budget'])
->where('budgets.user_id', $this->user->id)
@@ -389,8 +430,28 @@ class BudgetRepository implements BudgetRepositoryInterface
*
* @return Collection
*/
public function getBudgetLimits(Budget $budget, Carbon $start, Carbon $end): Collection
public function getBudgetLimits(Budget $budget, Carbon $start = null, Carbon $end = null): Collection
{
if (null === $end && null === $start) {
return $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']);
}
if (null === $end xor null === $start) {
$query = $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC');
// one of the two is null
if (null !== $end) {
// end date must be before $end.
$query->where('end_date', '<=', $end->format('Y-m-d 00:00:00'));
}
if (null !== $start) {
// start date must be after $start.
$query->where('start_date', '>=', $start->format('Y-m-d 00:00:00'));
}
$set = $query->get(['budget_limits.*']);
return $set;
}
// when both dates are set:
$set = $budget->budgetlimits()
->where(
function (Builder $q5) use ($start, $end) {
@@ -663,6 +724,42 @@ class BudgetRepository implements BudgetRepositoryInterface
return $newBudget;
}
/**
* @param array $data
*
* @throws FireflyException
* @return BudgetLimit
*/
public function storeBudgetLimit(array $data): BudgetLimit
{
$this->cleanupBudgets();
/** @var Budget $budget */
$budget = $data['budget'];
// find limit with same date range.
// if it exists, throw error.
$limits = $budget->budgetlimits()
->where('budget_limits.start_date', $data['start_date']->format('Y-m-d 00:00:00'))
->where('budget_limits.end_date', $data['end_date']->format('Y-m-d 00:00:00'))
->get(['budget_limits.*'])->count();
Log::debug(sprintf('Found %d budget limits.', $limits));
if ($limits > 0) {
throw new FireflyException('A budget limit for this budget, and this date range already exists. You must update the existing one.');
}
Log::debug('No existing budget limit, create a new one');
// or create one and return it.
$limit = new BudgetLimit;
$limit->budget()->associate($budget);
$limit->start_date = $data['start_date']->format('Y-m-d 00:00:00');
$limit->end_date = $data['end_date']->format('Y-m-d 00:00:00');
$limit->amount = $data['amount'];
$limit->save();
Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount']));
return $limit;
}
/**
* @param Budget $budget
* @param array $data
@@ -708,6 +805,29 @@ class BudgetRepository implements BudgetRepositoryInterface
}
/**
* @param BudgetLimit $budgetLimit
* @param array $data
*
* @return BudgetLimit
* @throws Exception
*/
public function updateBudgetLimit(BudgetLimit $budgetLimit, array $data): BudgetLimit
{
$this->cleanupBudgets();
/** @var Budget $budget */
$budget = $data['budget'];
$budgetLimit->budget()->associate($budget);
$budgetLimit->start_date = $data['start_date']->format('Y-m-d 00:00:00');
$budgetLimit->end_date = $data['end_date']->format('Y-m-d 00:00:00');
$budgetLimit->amount = $data['amount'];
$budgetLimit->save();
Log::debug(sprintf('Updated budget limit with ID #%d and amount %s', $budgetLimit->id, $data['amount']));
return $budgetLimit;
}
/**
* @param Budget $budget
* @param Carbon $start

View File

@@ -63,6 +63,13 @@ interface BudgetRepositoryInterface
*/
public function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array;
/**
* Deletes a budget limit.
*
* @param BudgetLimit $budgetLimit
*/
public function deleteBudgetLimit(BudgetLimit $budgetLimit): void;
/**
* @param Budget $budget
*
@@ -136,7 +143,7 @@ interface BudgetRepositoryInterface
*
* @return Collection
*/
public function getAllBudgetLimits(Carbon $start, Carbon $end): Collection;
public function getAllBudgetLimits(Carbon $start = null, Carbon $end = null): Collection;
/**
* @param TransactionCurrency $currency
@@ -161,7 +168,7 @@ interface BudgetRepositoryInterface
*
* @return Collection
*/
public function getBudgetLimits(Budget $budget, Carbon $start, Carbon $end): Collection;
public function getBudgetLimits(Budget $budget, Carbon $start = null, Carbon $end = null): Collection;
/**
* @param Collection $budgets
@@ -259,6 +266,21 @@ interface BudgetRepositoryInterface
*/
public function updateAvailableBudget(AvailableBudget $availableBudget, array $data): AvailableBudget;
/**
* @param BudgetLimit $budgetLimit
* @param array $data
*
* @return BudgetLimit
*/
public function updateBudgetLimit(BudgetLimit $budgetLimit, array $data): BudgetLimit;
/**
* @param array $data
*
* @return BudgetLimit
*/
public function storeBudgetLimit(array $data): BudgetLimit;
/**
* @param Budget $budget
* @param Carbon $start

View File

@@ -29,6 +29,9 @@ use League\Fractal\Resource\Item;
use League\Fractal\TransformerAbstract;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* Class AvailableBudgetTransformer
*/
class AvailableBudgetTransformer extends TransformerAbstract
{
/**

View File

@@ -0,0 +1,103 @@
<?php
/**
* BudgetLimitTransformer.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Transformers;
use FireflyIII\Models\BudgetLimit;
use League\Fractal\Resource\Item;
use League\Fractal\TransformerAbstract;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* Class BudgetLimitTransformer
*/
class BudgetLimitTransformer extends TransformerAbstract
{
/**
* List of resources possible to include
*
* @var array
*/
protected $availableIncludes = ['budget'];
/**
* List of resources to automatically include
*
* @var array
*/
protected $defaultIncludes = ['budget'];
/** @var ParameterBag */
protected $parameters;
/**
* CurrencyTransformer constructor.
*
* @codeCoverageIgnore
*
* @param ParameterBag $parameters
*/
public function __construct(ParameterBag $parameters)
{
$this->parameters = $parameters;
}
/**
* Attach the budget.
*
* @codeCoverageIgnore
*
* @param BudgetLimit $budgetLimit
*
* @return Item
*/
public function includeBudget(BudgetLimit $budgetLimit): Item
{
return $this->item($budgetLimit->budget, new BudgetTransformer($this->parameters), 'budgets');
}
/**
* Transform the note.
*
* @param BudgetLimit $budgetLimit
*
* @return array
*/
public function transform(BudgetLimit $budgetLimit): array
{
$data = [
'id' => (int)$budgetLimit->id,
'updated_at' => $budgetLimit->updated_at->toAtomString(),
'created_at' => $budgetLimit->created_at->toAtomString(),
'start_date' => $budgetLimit->start_date->format('Y-m-d'),
'end_date' => $budgetLimit->end_date->format('Y-m-d'),
'amount' => $budgetLimit->amount,
'links' => [
[
'rel' => 'self',
'uri' => '/budget_limits/' . $budgetLimit->id,
],
],
];
return $data;
}
}

View File

@@ -35,8 +35,6 @@
{{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
{{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }}
{{ ExpandedForm.integer('skip',0) }}
{# only correct way to do active checkbox #}
{{ ExpandedForm.checkbox('active', 1) }}
</div>
</div>

View File

@@ -66,13 +66,24 @@ Route::group(
Route::get('', ['uses' => 'AvailableBudgetController@index', 'as' => 'index']);
Route::post('', ['uses' => 'AvailableBudgetController@store', 'as' => 'store']);
Route::get('{availableBudget}', ['uses' => 'AvailableBudgetController@show', 'as' => 'show']);
Route::get('{availableBudget}/download', ['uses' => 'AvailableBudgetController@download', 'as' => 'download']);
Route::post('{availableBudget}/upload', ['uses' => 'AvailableBudgetController@upload', 'as' => 'upload']);
Route::put('{availableBudget}', ['uses' => 'AvailableBudgetController@update', 'as' => 'update']);
Route::delete('{availableBudget}', ['uses' => 'AvailableBudgetController@delete', 'as' => 'delete']);
}
);
Route::group(
['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'budget_limits', 'as' => 'api.v1.budget_limits.'],
function () {
// Budget Limit API routes:
Route::get('', ['uses' => 'BudgetLimitController@index', 'as' => 'index']);
Route::post('', ['uses' => 'BudgetLimitController@store', 'as' => 'store']);
Route::get('{budgetLimit}', ['uses' => 'BudgetLimitController@show', 'as' => 'show']);
Route::put('{budgetLimit}', ['uses' => 'BudgetLimitController@update', 'as' => 'update']);
Route::delete('{budgetLimit}', ['uses' => 'BudgetLimitController@delete', 'as' => 'delete']);
}
);
Route::group(
['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'bills', 'as' => 'api.v1.bills.'], function () {