diff --git a/app/Api/V1/Controllers/Search/TransferController.php b/app/Api/V1/Controllers/Search/TransferController.php new file mode 100644 index 0000000000..921c49bdf8 --- /dev/null +++ b/app/Api/V1/Controllers/Search/TransferController.php @@ -0,0 +1,116 @@ +. + */ + +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'); + } +} \ No newline at end of file diff --git a/app/Api/V1/Requests/Search/TransferRequest.php b/app/Api/V1/Requests/Search/TransferRequest.php new file mode 100644 index 0000000000..66d970bfd4 --- /dev/null +++ b/app/Api/V1/Requests/Search/TransferRequest.php @@ -0,0 +1,58 @@ +. + */ + +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', + ]; + } + +} \ No newline at end of file diff --git a/app/Rules/IsTransferAccount.php b/app/Rules/IsTransferAccount.php new file mode 100644 index 0000000000..68cb9d3667 --- /dev/null +++ b/app/Rules/IsTransferAccount.php @@ -0,0 +1,73 @@ +. + */ + +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); + } +} \ No newline at end of file diff --git a/app/Support/Search/GenericSearchInterface.php b/app/Support/Search/GenericSearchInterface.php index 47dacfbbdd..c14e59d57e 100644 --- a/app/Support/Search/GenericSearchInterface.php +++ b/app/Support/Search/GenericSearchInterface.php @@ -22,7 +22,16 @@ namespace FireflyIII\Support\Search; +use Illuminate\Support\Collection; + +/** + * Interface GenericSearchInterface + */ interface GenericSearchInterface { + /** + * @return Collection + */ + public function search(): Collection; } \ No newline at end of file diff --git a/app/Support/Search/TransferSearch.php b/app/Support/Search/TransferSearch.php new file mode 100644 index 0000000000..dd9912b830 --- /dev/null +++ b/app/Support/Search/TransferSearch.php @@ -0,0 +1,147 @@ +. + */ + +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); + } + } +} \ No newline at end of file diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 3337ef46f6..338e005c2d 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -45,6 +45,7 @@ return [ '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_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.', '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.', diff --git a/routes/api.php b/routes/api.php index 6629f37eab..0bfe33e91c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -342,6 +342,7 @@ Route::group( // Attachment API routes: Route::get('transactions', ['uses' => 'TransactionController@search', 'as' => 'transactions']); Route::get('accounts', ['uses' => 'AccountController@search', 'as' => 'accounts']); + Route::get('transfers', ['uses' => 'TransferController@search', 'as' => 'transfers']); } );