From 108d43f967008b5ed9c3514c35e04ad3ae2d5c7c Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 9 Dec 2018 08:45:53 +0100 Subject: [PATCH] Budgets and budget limits end point. --- app/Api/V1/Controllers/BudgetController.php | 107 +++++++++++++++++- .../V1/Controllers/BudgetLimitController.php | 53 +++++++++ app/Api/V1/Requests/BudgetLimitRequest.php | 26 +++-- app/Models/BudgetLimit.php | 1 + app/Repositories/Budget/BudgetRepository.php | 50 +++++--- app/Transformers/BudgetLimitTransformer.php | 35 ++++-- routes/api.php | 36 +++--- 7 files changed, 261 insertions(+), 47 deletions(-) diff --git a/app/Api/V1/Controllers/BudgetController.php b/app/Api/V1/Controllers/BudgetController.php index 1f6937fcfa..b8ee458896 100644 --- a/app/Api/V1/Controllers/BudgetController.php +++ b/app/Api/V1/Controllers/BudgetController.php @@ -23,11 +23,19 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers; +use FireflyIII\Api\V1\Requests\BudgetLimitRequest; use FireflyIII\Api\V1\Requests\BudgetRequest; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Models\Budget; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Http\Api\TransactionFilter; +use FireflyIII\Transformers\BudgetLimitTransformer; use FireflyIII\Transformers\BudgetTransformer; +use FireflyIII\Transformers\TransactionTransformer; use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -45,6 +53,7 @@ use League\Fractal\Serializer\JsonApiSerializer; */ class BudgetController extends Controller { + use TransactionFilter; /** @var BudgetRepositoryInterface The budget repository */ private $repository; @@ -68,6 +77,33 @@ class BudgetController extends Controller ); } + /** + * Display a listing of the resource. + * + * @param Request $request + * @param Budget $budget + * + * @return JsonResponse + */ + public function budgetLimits(Request $request, Budget $budget): JsonResponse + { + $manager = new Manager; + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + $this->parameters->set('budget_id', $budget->id); + $collection = $this->repository->getBudgetLimits($budget, $this->parameters->get('start'), $this->parameters->get('end')); + $count = $collection->count(); + $budgetLimits = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + $paginator = new LengthAwarePaginator($budgetLimits, $count, $pageSize, $this->parameters->get('page')); + $paginator->setPath(route('api.v1.budgets.budget_limits', [$budget->id]) . $this->buildParams()); + + $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'); + } + /** * Remove the specified resource from storage. * @@ -115,7 +151,6 @@ class BudgetController extends Controller return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); } - /** * Show a budget. * @@ -158,6 +193,76 @@ class BudgetController extends Controller throw new FireflyException('Could not store new budget.'); // @codeCoverageIgnore } + /** + * Store a newly created resource in storage. + * + * @param BudgetLimitRequest $request + * @param Budget $budget + * + * @return JsonResponse + */ + public function storeBudgetLimit(BudgetLimitRequest $request, Budget $budget): JsonResponse + { + $data = $request->getAll(); + $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'); + } + + /** + * Show all transactions. + * + * @param Request $request + * + * @param Budget $budget + * + * @return JsonResponse + */ + public function transactions(Request $request, Budget $budget): JsonResponse + { + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + $type = $request->get('type') ?? 'default'; + $this->parameters->set('type', $type); + + $types = $this->mapTransactionTypes($this->parameters->get('type')); + $manager = new Manager(); + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + /** @var User $admin */ + $admin = auth()->user(); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setUser($admin); + $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); + $collector->setAllAssetAccounts(); + $collector->setBudget($budget); + + if (\in_array(TransactionType::TRANSFER, $types, true)) { + $collector->removeFilter(InternalTransferFilter::class); + } + + if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) { + $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); + } + $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); + $collector->setTypes($types); + $paginator = $collector->getPaginatedTransactions(); + $paginator->setPath(route('api.v1.budgets.transactions', [$budget->id]) . $this->buildParams()); + $transactions = $paginator->getCollection(); + $repository = app(JournalRepositoryInterface::class); + + $resource = new FractalCollection($transactions, new TransactionTransformer($this->parameters, $repository), 'transactions'); + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } /** * Update a budget. diff --git a/app/Api/V1/Controllers/BudgetLimitController.php b/app/Api/V1/Controllers/BudgetLimitController.php index f99adc2933..42383442d6 100644 --- a/app/Api/V1/Controllers/BudgetLimitController.php +++ b/app/Api/V1/Controllers/BudgetLimitController.php @@ -26,9 +26,15 @@ namespace FireflyIII\Api\V1\Controllers; use FireflyIII\Api\V1\Requests\BudgetLimitRequest; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Http\Api\TransactionFilter; use FireflyIII\Transformers\BudgetLimitTransformer; +use FireflyIII\Transformers\TransactionTransformer; use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -48,6 +54,7 @@ use League\Fractal\Serializer\JsonApiSerializer; */ class BudgetLimitController extends Controller { + use TransactionFilter; /** @var BudgetRepositoryInterface The budget repository */ private $repository; @@ -163,6 +170,52 @@ class BudgetLimitController extends Controller return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); } + /** + * Show all transactions. + * + * @param Request $request + * @param BudgetLimit $budgetLimit + * + * @return JsonResponse + */ + public function transactions(Request $request, BudgetLimit $budgetLimit): JsonResponse + { + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + $type = $request->get('type') ?? 'default'; + $this->parameters->set('type', $type); + + $types = $this->mapTransactionTypes($this->parameters->get('type')); + $manager = new Manager(); + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + /** @var User $admin */ + $admin = auth()->user(); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setUser($admin); + $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); + $collector->setAllAssetAccounts(); + $collector->setBudget($budgetLimit->budget); + $collector->setRange($budgetLimit->start_date, $budgetLimit->end_date); + + if (\in_array(TransactionType::TRANSFER, $types, true)) { + $collector->removeFilter(InternalTransferFilter::class); + } + + $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); + $collector->setTypes($types); + $paginator = $collector->getPaginatedTransactions(); + $paginator->setPath(route('api.v1.budget_limits.transactions', [$budgetLimit->id]) . $this->buildParams()); + $transactions = $paginator->getCollection(); + $repository = app(JournalRepositoryInterface::class); + + $resource = new FractalCollection($transactions, new TransactionTransformer($this->parameters, $repository), 'transactions'); + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } + /** * Update the specified resource in storage. * diff --git a/app/Api/V1/Requests/BudgetLimitRequest.php b/app/Api/V1/Requests/BudgetLimitRequest.php index 1080df0d5c..9aff7066af 100644 --- a/app/Api/V1/Requests/BudgetLimitRequest.php +++ b/app/Api/V1/Requests/BudgetLimitRequest.php @@ -48,10 +48,12 @@ class BudgetLimitRequest extends Request 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'), + 'budget_id' => $this->integer('budget_id'), + 'start' => $this->date('start'), + 'end' => $this->date('end'), + 'amount' => $this->string('amount'), + 'currency_id' => $this->integer('currency_id'), + 'currency_code' => $this->string('currency_code'), ]; } @@ -63,10 +65,12 @@ class BudgetLimitRequest extends Request 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', + 'budget_id' => 'required|exists:budgets,id|belongsToUser:budgets,id', + 'start' => 'required|before:end|date', + 'end' => 'required|after:start|date', + 'amount' => 'required|more:0', + 'currency_id' => 'numeric|exists:transaction_currencies,id', + 'currency_code' => 'min:3|max:3|exists:transaction_currencies,code', ]; switch ($this->method()) { default: @@ -76,6 +80,12 @@ class BudgetLimitRequest extends Request $rules['budget_id'] = 'required|exists:budgets,id|belongsToUser:budgets,id'; break; } + // if request has a budget already, drop the rule. + $budget = $this->route()->parameter('budget'); + if (null !== $budget) { + unset($rules['budget_id']); + } + return $rules; } diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index fe220e86f6..508746ac7e 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -40,6 +40,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @property int $budget_id * @property string spent * @property int $transaction_currency_id + * @property TransactionCurrency $transactionCurrency */ class BudgetLimit extends Model { diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index f0c9f09b6c..3307762585 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use Exception; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\AccountType; use FireflyIII\Models\AvailableBudget; @@ -108,7 +109,7 @@ class BudgetRepository implements BudgetRepositoryInterface } catch (Exception|FatalThrowableError $e) { Log::debug(sprintf('Could not delete budget limit: %s', $e->getMessage())); } - Budget::where('order',0)->update(['order' => 100]); + Budget::where('order', 0)->update(['order' => 100]); // do the clean up by hand because Sqlite can be tricky with this. $budgetLimits = BudgetLimit::orderBy('created_at', 'DESC')->get(['id', 'budget_id', 'start_date', 'end_date']); @@ -773,24 +774,33 @@ class BudgetRepository implements BudgetRepositoryInterface /** @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.'); + // if no currency has been provided, use the user's default currency: + /** @var TransactionCurrencyFactory $factory */ + $factory = app(TransactionCurrencyFactory::class); + $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); + if (null === $currency) { + $currency = app('amount')->getDefaultCurrencyByUser($this->user); } + // find limit with same date range. + // if it exists, return that one. + $limit = $budget->budgetlimits() + ->where('budget_limits.start_date', $data['start']->format('Y-m-d 00:00:00')) + ->where('budget_limits.end_date', $data['end']->format('Y-m-d 00:00:00')) + ->where('budget_limits.transaction_currency_id', $currency->id) + ->get(['budget_limits.*'])->first(); + if (null !== $limit) { + return $limit; + } 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->start_date = $data['start']->format('Y-m-d 00:00:00'); + $limit->end_date = $data['end']->format('Y-m-d 00:00:00'); + $limit->amount = $data['amount']; + $limit->transaction_currency_id = $currency->id; $limit->save(); Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount'])); @@ -861,9 +871,19 @@ class BudgetRepository implements BudgetRepositoryInterface $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->start_date = $data['start']->format('Y-m-d 00:00:00'); + $budgetLimit->end_date = $data['end']->format('Y-m-d 00:00:00'); $budgetLimit->amount = $data['amount']; + + // if no currency has been provided, use the user's default currency: + /** @var TransactionCurrencyFactory $factory */ + $factory = app(TransactionCurrencyFactory::class); + $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); + if (null === $currency) { + $currency = app('amount')->getDefaultCurrencyByUser($this->user); + } + $budgetLimit->transaction_currency_id = $currency->id; + $budgetLimit->save(); Log::debug(sprintf('Updated budget limit with ID #%d and amount %s', $budgetLimit->id, $data['amount'])); diff --git a/app/Transformers/BudgetLimitTransformer.php b/app/Transformers/BudgetLimitTransformer.php index 89be58ac35..98072ca2e0 100644 --- a/app/Transformers/BudgetLimitTransformer.php +++ b/app/Transformers/BudgetLimitTransformer.php @@ -24,7 +24,6 @@ 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; @@ -57,17 +56,35 @@ class BudgetLimitTransformer extends TransformerAbstract */ public function transform(BudgetLimit $budgetLimit): array { + $currency = $budgetLimit->transactionCurrency; + $amount = $budgetLimit->amount; + $currencyId = null; + $currencyName = null; + $currencyCode = null; + $currencySymbol = null; + if (null !== $currency) { + $amount = round($budgetLimit->amount, $budgetLimit->transactionCurrency->decimal_places); + $currencyId = $currency->id; + $currencyName = $currency->name; + $currencyCode = $currency->code; + $currencySymbol = $currency->symbol; + } $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' => [ + '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'), + 'budget_id' => $budgetLimit->budget_id, + 'currency_id' => $currencyId, + 'currency_code' => $currencyCode, + 'currency_name' => $currencyName, + 'currency_symbol' => $currencySymbol, + 'amount' => $amount, + 'links' => [ [ 'rel' => 'self', - 'uri' => '/budget_limits/' . $budgetLimit->id, + 'uri' => '/budgets/limits/' . $budgetLimit->id, ], ], ]; diff --git a/routes/api.php b/routes/api.php index 7d1db3df1d..77f7a1d6ad 100644 --- a/routes/api.php +++ b/routes/api.php @@ -72,8 +72,9 @@ Route::group( } ); + Route::group( - ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'budget_limits', 'as' => 'api.v1.budget_limits.'], + ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'budgets/limits', 'as' => 'api.v1.budget_limits.'], function () { // Budget Limit API routes: @@ -82,22 +83,10 @@ Route::group( 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::get('{budgetLimit}/transactions', ['uses' => 'BudgetLimitController@transactions', 'as' => 'transactions']); } ); - -Route::group( - ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'bills', 'as' => 'api.v1.bills.'], function () { - - // Bills API routes: - Route::get('', ['uses' => 'BillController@index', 'as' => 'index']); - Route::post('', ['uses' => 'BillController@store', 'as' => 'store']); - Route::get('{bill}', ['uses' => 'BillController@show', 'as' => 'show']); - Route::put('{bill}', ['uses' => 'BillController@update', 'as' => 'update']); - Route::delete('{bill}', ['uses' => 'BillController@delete', 'as' => 'delete']); -} -); - Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'budgets', 'as' => 'api.v1.budgets.'], function () { @@ -108,9 +97,28 @@ Route::group( Route::get('{budget}', ['uses' => 'BudgetController@show', 'as' => 'show']); Route::put('{budget}', ['uses' => 'BudgetController@update', 'as' => 'update']); Route::delete('{budget}', ['uses' => 'BudgetController@delete', 'as' => 'delete']); + Route::get('{budget}/transactions', ['uses' => 'BudgetController@transactions', 'as' => 'transactions']); + Route::get('{budget}/limits', ['uses' => 'BudgetController@budgetLimits', 'as' => 'budget_limits']); + Route::post('{budget}/limits', ['uses' => 'BudgetController@storeBudgetLimit', 'as' => 'store_budget_limit']); } ); + + + + +Route::group( + ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'bills', 'as' => 'api.v1.bills.'], function () { + + // Bills API routes: + Route::get('', ['uses' => 'BillController@index', 'as' => 'index']); + Route::post('', ['uses' => 'BillController@store', 'as' => 'store']); + Route::get('{bill}', ['uses' => 'BillController@show', 'as' => 'show']); + Route::put('{bill}', ['uses' => 'BillController@update', 'as' => 'update']); + Route::delete('{bill}', ['uses' => 'BillController@delete', 'as' => 'delete']); +} +); + Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'categories', 'as' => 'api.v1.categories.'], function () {