A new endpoint that allows you to search for transfers.

This commit is contained in:
James Cole 2019-10-26 07:49:36 +02:00
parent 6823adc976
commit 8b11afb83f
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
7 changed files with 405 additions and 0 deletions

View File

@ -0,0 +1,116 @@
<?php
/**
* TransferController.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Controllers\Search;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Search\TransferRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Support\Search\TransferSearch;
use FireflyIII\Transformers\TransactionGroupTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
/**
* Class TransferController
*/
class TransferController extends Controller
{
/**
* @param Request $request
*
* @return JsonResponse|Response
* @throws FireflyException
*/
public function search(TransferRequest $request)
{
// configure transfer search to search for a > b
$search = app(TransferSearch::class);
$search->setSource($request->get('source'));
$search->setDestination($request->get('destination'));
$search->setAmount($request->get('amount'));
$search->setDescription($request->get('description'));
$search->setDate($request->get('date'));
$left = $search->search();
// configure transfer search to search for b > a
$search->setSource($request->get('destination'));
$search->setDestination($request->get('source'));
$search->setAmount($request->get('amount'));
$search->setDescription($request->get('description'));
$search->setDate($request->get('date'));
$right = $search->search();
// add parameters to URL:
$this->parameters->set('source', $request->get('source'));
$this->parameters->set('destination', $request->get('destination'));
$this->parameters->set('amount', $request->get('amount'));
$this->parameters->set('description', $request->get('description'));
$this->parameters->set('date', $request->get('date'));
// get all journal ID's.
$total = $left->merge($right)->unique('id')->pluck('id')->toArray();
if (0 === count($total)) {
// forces search to be empty.
$total = [-1];
}
// collector to return results.
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
$manager = $this->getManager();
/** @var User $admin */
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
// all info needed for the API:
->withAPIInformation()
// set page size:
->setLimit($pageSize)
// set page to retrieve
->setPage(1)
->setJournalIds($total);
$paginator = $collector->getPaginatedGroups();
$paginator->setPath(route('api.v1.search.transfers') . $this->buildParams());
$transactions = $paginator->getCollection();
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* TransferRequest.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Requests\Search;
use FireflyIII\Api\V1\Requests\Request;
use FireflyIII\Rules\IsTransferAccount;
/**
* Class TransferRequest
*/
class TransferRequest extends Request
{
/**
* Authorize logged in users.
*
* @return bool
*/
public function authorize(): bool
{
// Only allow authenticated users
return auth()->check();
}
/**
* @return array
*/
public function rules(): array
{
return [
'source' => ['required', new IsTransferAccount],
'destination' => ['required', new IsTransferAccount],
'amount' => 'required|numeric|more:0',
'description' => 'required|min:1',
'date' => 'required|date',
];
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* IsTransferAccount.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Rules;
use FireflyIII\Models\TransactionType;
use FireflyIII\Validation\AccountValidator;
use Illuminate\Contracts\Validation\Rule;
use Log;
/**
* Class IsTransferAccount
*/
class IsTransferAccount implements Rule
{
/**
* Get the validation error message.
*
* @return string|array
*/
public function message(): string
{
return (string)trans('validation.not_transfer_account');
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
*
* @return bool
*/
public function passes($attribute, $value): bool
{
Log::debug(sprintf('Now in %s(%s)', __METHOD__, $value));
/** @var AccountValidator $validator */
$validator = app(AccountValidator::class);
$validator->setTransactionType(TransactionType::TRANSFER);
$validator->setUser(auth()->user());
$validAccount = $validator->validateSource(null, (string)$value);
if (true === $validAccount) {
Log::debug('Found account based on name. Return true.');
// found by name, use repos to return.
return true;
}
$validAccount = $validator->validateSource((int)$value, null);
Log::debug(sprintf('Search by id (%d), result is %s.', (int)$value, var_export($validAccount, true)));
return !(false === $validAccount);
}
}

View File

@ -22,7 +22,16 @@
namespace FireflyIII\Support\Search; namespace FireflyIII\Support\Search;
use Illuminate\Support\Collection;
/**
* Interface GenericSearchInterface
*/
interface GenericSearchInterface interface GenericSearchInterface
{ {
/**
* @return Collection
*/
public function search(): Collection;
} }

View File

@ -0,0 +1,147 @@
<?php
/**
* TransferSearch.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Support\Search;
use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\User;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Log;
/**
* Class TransferSearch
*/
class TransferSearch implements GenericSearchInterface
{
/** @var AccountRepositoryInterface */
private $accountRepository;
/** @var string */
private $amount;
/** @var Carbon */
private $date;
/** @var string */
private $description;
/** @var Account */
private $destination;
/** @var Account */
private $source;
/** @var array */
private $types;
public function __construct()
{
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->types = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE];
}
/**
* @return Collection
*/
public function search(): Collection
{
/** @var User $user */
$user = auth()->user();
$query = $user->transactionJournals()
->leftJoin(
'transactions as source', static function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 'source.transaction_journal_id');
$join->where('source.amount', '<', '0');
}
)
->leftJoin(
'transactions as destination', static function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 'destination.transaction_journal_id');
$join->where('destination.amount', '>', '0');
}
)
->where('source.account_id', $this->source->id)
->where('destination.account_id', $this->destination->id)
->where('transaction_journals.description', $this->description)
->where('destination.amount', $this->amount)
->where('transaction_journals.date', $this->date->format('Y-m-d 00:00:00'))
;
return $query->get(['transaction_journals.id', 'transaction_journals.transaction_group_id']);
}
/**
* @param string $amount
*/
public function setAmount(string $amount): void
{
$this->amount = $amount;
}
/**
* @param string $date
*/
public function setDate(string $date): void
{
try {
$carbon = Carbon::createFromFormat('Y-m-d', $date);
} catch (InvalidArgumentException $e) {
Log::error($e->getMessage());
$carbon = Carbon::now();
}
$this->date = $carbon;
}
/**
* @param string $description
*/
public function setDescription(string $description): void
{
$this->description = $description;
}
/**
* @param string $destination
*/
public function setDestination(string $destination): void
{
if (is_numeric($destination)) {
$this->destination = $this->accountRepository->findNull((int)$destination);
}
if (null === $this->destination) {
$this->destination = $this->accountRepository->findByName($destination, $this->types);
}
}
/**
* @param string $source
*/
public function setSource(string $source): void
{
if (is_numeric($source)) {
$this->source = $this->accountRepository->findNull((int)$source);
}
if (null === $this->source) {
$this->source = $this->accountRepository->findByName($source, $this->types);
}
}
}

View File

@ -45,6 +45,7 @@ return [
'at_least_one_repetition' => 'Need at least one repetition.', 'at_least_one_repetition' => 'Need at least one repetition.',
'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.',
'require_currency_info' => 'The content of this field is invalid without currency information.', 'require_currency_info' => 'The content of this field is invalid without currency information.',
'not_transfer_account' => 'This account is not an account that can be used for transfers.',
'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.',
'equal_description' => 'Transaction description should not equal global description.', 'equal_description' => 'Transaction description should not equal global description.',
'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',

View File

@ -342,6 +342,7 @@ Route::group(
// Attachment API routes: // Attachment API routes:
Route::get('transactions', ['uses' => 'TransactionController@search', 'as' => 'transactions']); Route::get('transactions', ['uses' => 'TransactionController@search', 'as' => 'transactions']);
Route::get('accounts', ['uses' => 'AccountController@search', 'as' => 'accounts']); Route::get('accounts', ['uses' => 'AccountController@search', 'as' => 'accounts']);
Route::get('transfers', ['uses' => 'TransferController@search', 'as' => 'transfers']);
} }
); );