From 9b7835c9eda4bf9cc4303517db3062aa46a9aee4 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 4 Jun 2019 20:42:11 +0200 Subject: [PATCH] Various API updates. --- .../Controllers/AvailableBudgetController.php | 14 +- .../Controllers/Chart/AccountController.php | 43 +--- app/Api/V1/Controllers/CurrencyController.php | 23 +-- app/Api/V1/Controllers/RuleController.php | 22 +- .../V1/Requests/TransactionUpdateRequest.php | 15 +- app/Factory/TransactionFactory.php | 192 +++++++++--------- app/Factory/TransactionJournalFactory.php | 130 +++++++++++- app/Repositories/Budget/BudgetRepository.php | 143 ++++++++----- .../Budget/BudgetRepositoryInterface.php | 72 ++++--- app/Support/Http/Api/ApiSupport.php | 66 ++++++ app/Validation/TransactionValidation.php | 72 ++++++- 11 files changed, 527 insertions(+), 265 deletions(-) create mode 100644 app/Support/Http/Api/ApiSupport.php diff --git a/app/Api/V1/Controllers/AvailableBudgetController.php b/app/Api/V1/Controllers/AvailableBudgetController.php index 86e02c1ff9..73cacafb97 100644 --- a/app/Api/V1/Controllers/AvailableBudgetController.php +++ b/app/Api/V1/Controllers/AvailableBudgetController.php @@ -102,21 +102,11 @@ class AvailableBudgetController extends Controller // types to get, page size: $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; - // get list of available budgets. Count it and split it. - $collection = $this->repository->getAvailableBudgets(); - - // filter list on start and end date, if present. - // TODO: put this in the query. $start = $this->parameters->get('start'); $end = $this->parameters->get('end'); - if (null !== $start && null !== $end) { - $collection = $collection->filter( - function (AvailableBudget $availableBudget) use ($start, $end) { - return $availableBudget->start_date->gte($start) && $availableBudget->end_date->lte($end); - } - ); - } + // get list of available budgets. Count it and split it. + $collection = $this->repository->getAvailableBudgetsByDate($start, $end); $count = $collection->count(); $availableBudgets = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php index 3a3d40a84b..0efd9e0c3f 100644 --- a/app/Api/V1/Controllers/Chart/AccountController.php +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -32,16 +32,17 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Http\Api\ApiSupport; use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Collection; /** * Class AccountController */ class AccountController extends Controller { + use ApiSupport; /** @var CurrencyRepositoryInterface */ private $currencyRepository; /** @var AccountRepositoryInterface */ @@ -156,42 +157,6 @@ class AccountController extends Controller return response()->json($chartData); } - /** - * Small helper function for the revenue and expense account charts. - * TODO should include Trait instead of doing this. - * - * @param Collection $accounts - * - * @return array - */ - protected function extractNames(Collection $accounts): array - { - $return = []; - /** @var Account $account */ - foreach ($accounts as $account) { - $return[$account->id] = $account->name; - } - - return $return; - } - - /** - * Small helper function for the revenue and expense account charts. - * TODO should include Trait instead of doing this. - * - * @param array $names - * - * @return array - */ - protected function expandNames(array $names): array - { - $result = []; - foreach ($names as $entry) { - $result[$entry['name']] = 0; - } - - return $result; - } /** * @param Request $request @@ -213,8 +178,8 @@ class AccountController extends Controller // user's preferences $defaultSet = $this->repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray(); - $frontPage = app('preferences')->get('frontPageAccounts', $defaultSet); - $default = app('amount')->getDefaultCurrency(); + $frontPage = app('preferences')->get('frontPageAccounts', $defaultSet); + $default = app('amount')->getDefaultCurrency(); if (0 === count($frontPage->data)) { $frontPage->data = $defaultSet; $frontPage->save(); diff --git a/app/Api/V1/Controllers/CurrencyController.php b/app/Api/V1/Controllers/CurrencyController.php index cb34e54823..bf1a8d8d6c 100644 --- a/app/Api/V1/Controllers/CurrencyController.php +++ b/app/Api/V1/Controllers/CurrencyController.php @@ -28,7 +28,6 @@ use FireflyIII\Api\V1\Requests\CurrencyRequest; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; -use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Bill; use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Recurrence; @@ -181,18 +180,11 @@ class CurrencyController extends Controller $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; // get list of available budgets. Count it and split it. + /** @var BudgetRepositoryInterface $repository */ $repository = app(BudgetRepositoryInterface::class); $repository->setUser($admin); - $unfiltered = $repository->getAvailableBudgets(); - - // filter list. - $collection = $unfiltered->filter( - function (AvailableBudget $availableBudget) use ($currency) { - return $availableBudget->transaction_currency_id === $currency->id; - } - ); - + $collection = $repository->getAvailableBudgetsByCurrency($currency); $count = $collection->count(); $availableBudgets = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); @@ -279,16 +271,7 @@ class CurrencyController extends Controller $manager = new Manager; $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; - $unfiltered = $repository->getAllBudgetLimits($this->parameters->get('start'), $this->parameters->get('end')); - - // TODO replace this - // filter budget limits on currency ID - $collection = $unfiltered->filter( - function (BudgetLimit $budgetLimit) use ($currency) { - return $budgetLimit->transaction_currency_id === $currency->id; - } - ); - + $collection = $repository->getAllBudgetLimitsByCurrency($currency, $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')); diff --git a/app/Api/V1/Controllers/RuleController.php b/app/Api/V1/Controllers/RuleController.php index 8b8326e411..ec2e95f526 100644 --- a/app/Api/V1/Controllers/RuleController.php +++ b/app/Api/V1/Controllers/RuleController.php @@ -34,7 +34,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\TransactionRules\TransactionMatcher; use FireflyIII\Transformers\RuleTransformer; -use FireflyIII\Transformers\TransactionTransformer; +use FireflyIII\Transformers\TransactionGroupTransformer; use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -184,7 +184,6 @@ class RuleController extends Controller } /** - * TODO deprecated return values in transformer. * @param Request $request * @param Rule $rule * @@ -193,10 +192,10 @@ class RuleController extends Controller */ public function testRule(Request $request, Rule $rule): JsonResponse { - $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; - $page = 0 === (int)$request->query('page') ? 1 : (int)$request->query('page'); + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + $page = 0 === (int)$request->query('page') ? 1 : (int)$request->query('page'); /** @var Carbon $startDate */ - $startDate = null === $request->query('start_date') ? null : Carbon::createFromFormat('Y-m-d', $request->query('start_date')); + $startDate = null === $request->query('start_date') ? null : Carbon::createFromFormat('Y-m-d', $request->query('start_date')); /** @var Carbon $endDate */ $endDate = null === $request->query('end_date') ? null : Carbon::createFromFormat('Y-m-d', $request->query('end_date')); $searchLimit = 0 === (int)$request->query('search_limit') ? (int)config('firefly.test-triggers.limit') : (int)$request->query('search_limit'); @@ -229,11 +228,11 @@ class RuleController extends Controller $matcher->setAccounts($accounts); $matchingTransactions = $matcher->findTransactionsByRule(); - $matchingTransactions = $matchingTransactions->unique('id'); // make paginator out of results. - $count = $matchingTransactions->count(); - $transactions = $matchingTransactions->slice(($page - 1) * $pageSize, $pageSize); + $count = count($matchingTransactions); + $transactions = array_slice($matchingTransactions, ($page - 1) * $pageSize, $pageSize); + // make paginator: $paginator = new LengthAwarePaginator($transactions, $count, $pageSize, $this->parameters->get('page')); $paginator->setPath(route('api.v1.rules.test', [$rule->id]) . $this->buildParams()); @@ -243,8 +242,8 @@ class RuleController extends Controller $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; $manager->setSerializer(new JsonApiSerializer($baseUrl)); - /** @var TransactionTransformer $transformer */ - $transformer = app(TransactionTransformer::class); + /** @var TransactionGroupTransformer $transformer */ + $transformer = app(TransactionGroupTransformer::class); $transformer->setParameters($this->parameters); $resource = new FractalCollection($matchingTransactions, $transformer, 'transactions'); @@ -321,6 +320,7 @@ class RuleController extends Controller $resource = new Item($rule, $transformer, 'rules'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); - } + + // TODO move rule up, move rule down. } diff --git a/app/Api/V1/Requests/TransactionUpdateRequest.php b/app/Api/V1/Requests/TransactionUpdateRequest.php index 02f11a4894..542c865ec0 100644 --- a/app/Api/V1/Requests/TransactionUpdateRequest.php +++ b/app/Api/V1/Requests/TransactionUpdateRequest.php @@ -241,9 +241,15 @@ class TransactionUpdateRequest extends Request // validate source/destination is equal, depending on the transaction journal type. $this->validateEqualAccountsForUpdate($validator, $transactionGroup); - // TODO if type is set, source + destination info is mandatory. - // TODO validate that the currency fits the source and/or destination account. - // TODO the currency info must match the accounts involved. + // If type is set, source + destination info is mandatory. + // Not going to do this. Not sure where the demand came from. + + // validate that the currency fits the source and/or destination account. + // validate all account info + $this->validateAccountInformationUpdate($validator); + + // The currency info must match the accounts involved. + // Instead will ignore currency info as much as possible. // TODO if the transaction_journal_id is empty, some fields are mandatory. // TODO like the amount! @@ -255,8 +261,7 @@ class TransactionUpdateRequest extends Request // $this->validateForeignCurrencyInformation($validator); // // - // // validate all account info - // $this->validateAccountInformation($validator); + // // // make sure all splits have valid source + dest info // $this->validateSplitAccounts($validator); diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index 92f0811347..9f44bfc586 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -25,18 +25,15 @@ declare(strict_types=1); namespace FireflyIII\Factory; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Services\Internal\Support\JournalServiceTrait; -use FireflyIII\Support\NullArrayObject; use FireflyIII\User; use FireflyIII\Validation\AccountValidator; use Illuminate\Database\QueryException; -use Illuminate\Support\Collection; use Log; /** @@ -50,8 +47,16 @@ class TransactionFactory private $accountValidator; /** @var TransactionJournal */ private $journal; + /** @var Account */ + private $account; + /** @var TransactionCurrency */ + private $currency; + /** @var TransactionCurrency */ + private $foreignCurrency; /** @var User */ private $user; + /** @var bool */ + private $reconciled; /** * Constructor. @@ -63,93 +68,72 @@ class TransactionFactory } $this->accountRepository = app(AccountRepositoryInterface::class); $this->accountValidator = app(AccountValidator::class); + $this->reconciled = false; + $this->foreignCurrency = null; + } + + /** + * @param bool $reconciled + */ + public function setReconciled(bool $reconciled): void + { + $this->reconciled = $reconciled; + } + + /** + * @param Account $account + */ + public function setAccount(Account $account): void + { + $this->account = $account; } /** - * @param Account $account * @param TransactionCurrency $currency - * @param string $amount + */ + public function setCurrency(TransactionCurrency $currency): void + { + $this->currency = $currency; + } + + /** + * @param TransactionCurrency $foreignCurrency |null + */ + public function setForeignCurrency(?TransactionCurrency $foreignCurrency): void + { + $this->foreignCurrency = $foreignCurrency; + } + + /** + * Create transaction with negative amount (for source accounts). * + * @param string $amount + * @param string|null $foreignAmount * @return Transaction|null */ - public function create(Account $account, TransactionCurrency $currency, string $amount): ?Transaction + public function createNegative(string $amount, ?string $foreignAmount): ?Transaction { - $result = null; - $data = [ - 'reconciled' => false, - 'account_id' => $account->id, - 'transaction_journal_id' => $this->journal->id, - 'description' => null, - 'transaction_currency_id' => $currency->id, - 'amount' => $amount, - 'foreign_amount' => null, - 'foreign_currency_id' => null, - 'identifier' => 0, - ]; - try { - $result = Transaction::create($data); - } catch (QueryException $e) { - Log::error(sprintf('Could not create transaction: %s', $e->getMessage()), $data); - } - if (null !== $result) { - Log::debug( - sprintf( - 'Created transaction #%d (%s %s), part of journal #%d', $result->id, - $currency->code, $amount, $this->journal->id - ) - ); + if (null !== $foreignAmount) { + $foreignAmount = app('steam')->negative($foreignAmount); } - return $result; + return $this->create(app('steam')->negative($amount), $foreignAmount); } /** - * @param NullArrayObject $data - * @param TransactionCurrency $currency - * @param TransactionCurrency|null $foreignCurrency + * Create transaction with positive amount (for destination accounts). * - * @return Collection - * @throws FireflyException + * @param string $amount + * @param string|null $foreignAmount + * @return Transaction|null */ - public function createPair(NullArrayObject $data, TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): Collection + public function createPositive(string $amount, ?string $foreignAmount): ?Transaction { - Log::debug('Going to create a pair of transactions.'); - Log::debug(sprintf('Source info: ID #%d, name "%s"', $data['source_id'], $data['source_name'])); - Log::debug(sprintf('Destination info: ID #%d, name "%s"', $data['destination_id'], $data['destination_name'])); - // validate source and destination using a new Validator. - $this->validateAccounts($data); - - // create or get source and destination accounts: - $type = $this->journal->transactionType->type; - $sourceAccount = $this->getAccount($type, 'source', (int)$data['source_id'], $data['source_name']); - $destinationAccount = $this->getAccount($type, 'destination', (int)$data['destination_id'], $data['destination_name']); - - // at this point we know the - - $amount = $this->getAmount($data['amount']); - $foreignAmount = $this->getForeignAmount($data['foreign_amount']); - $one = $this->create($sourceAccount, $currency, app('steam')->negative($amount)); - $two = $this->create($destinationAccount, $currency, app('steam')->positive($amount)); - - $one->reconciled = $data['reconciled'] ?? false; - $two->reconciled = $data['reconciled'] ?? false; - - // add foreign currency info to $one and $two if necessary. - if (null !== $foreignCurrency && null !== $foreignAmount - && $foreignCurrency->id !== $currency->id - ) { - $one->foreign_currency_id = $foreignCurrency->id; - $two->foreign_currency_id = $foreignCurrency->id; - $one->foreign_amount = app('steam')->negative($foreignAmount); - $two->foreign_amount = app('steam')->positive($foreignAmount); + if (null !== $foreignAmount) { + $foreignAmount = app('steam')->positive($foreignAmount); } - - $one->save(); - $two->save(); - - return new Collection([$one, $two]); - + return $this->create(app('steam')->positive($amount), $foreignAmount); } @@ -171,33 +155,49 @@ class TransactionFactory } /** - * @param NullArrayObject $data - * - * @throws FireflyException + * @param string $amount + * @param string|null $foreignAmount + * @return Transaction|null */ - private function validateAccounts(NullArrayObject $data): void + private function create(string $amount, ?string $foreignAmount): ?Transaction { - $transactionType = $data['type'] ?? 'invalid'; - $this->accountValidator->setUser($this->journal->user); - $this->accountValidator->setTransactionType($transactionType); - - // validate source account. - $sourceId = isset($data['source_id']) ? (int)$data['source_id'] : null; - $sourceName = $data['source_name'] ?? null; - $validSource = $this->accountValidator->validateSource($sourceId, $sourceName); - - // do something with result: - if (false === $validSource) { - throw new FireflyException($this->accountValidator->sourceError); + $result = null; + $data = [ + 'reconciled' => $this->reconciled, + 'account_id' => $this->account->id, + 'transaction_journal_id' => $this->journal->id, + 'description' => null, + 'transaction_currency_id' => $this->currency->id, + 'amount' => $amount, + 'foreign_amount' => null, + 'foreign_currency_id' => null, + 'identifier' => 0, + ]; + try { + $result = Transaction::create($data); + } catch (QueryException $e) { + Log::error(sprintf('Could not create transaction: %s', $e->getMessage()), $data); } - Log::debug('Source seems valid.'); - // validate destination account - $destinationId = isset($data['destination_id']) ? (int)$data['destination_id'] : null; - $destinationName = $data['destination_name'] ?? null; - $validDestination = $this->accountValidator->validateDestination($destinationId, $destinationName); - // do something with result: - if (false === $validDestination) { - throw new FireflyException($this->accountValidator->destError); + if (null !== $result) { + Log::debug( + sprintf( + 'Created transaction #%d (%s %s), part of journal #%d', $result->id, + $this->currency->code, $amount, $this->journal->id + ) + ); + + // do foreign currency thing. + // add foreign currency info to $one and $two if necessary. + if (null !== $this->foreignCurrency && null !== $foreignAmount + && $this->foreignCurrency->id !== $this->currency->id + ) { + $result->foreign_currency_id = $this->foreignCurrency->id; + $result->foreign_amount = app('steam')->negative($foreignAmount); + + } + $result->save(); } + + return $result; } } diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index 988a2e12ef..8d64889551 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -27,8 +27,11 @@ namespace FireflyIII\Factory; use Carbon\Carbon; use Exception; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; @@ -38,6 +41,7 @@ use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface; use FireflyIII\Services\Internal\Support\JournalServiceTrait; use FireflyIII\Support\NullArrayObject; use FireflyIII\User; +use FireflyIII\Validation\AccountValidator; use Illuminate\Support\Collection; use Log; @@ -48,6 +52,10 @@ class TransactionJournalFactory { use JournalServiceTrait; + /** @var AccountValidator */ + private $accountValidator; + /** @var AccountRepositoryInterface */ + private $accountRepository; /** @var BillRepositoryInterface */ private $billRepository; /** @var CurrencyRepositoryInterface */ @@ -100,6 +108,8 @@ class TransactionJournalFactory $this->piggyRepository = app(PiggyBankRepositoryInterface::class); $this->piggyEventFactory = app(PiggyBankEventFactory::class); $this->tagFactory = app(TagFactory::class); + $this->accountValidator = app(AccountValidator::class); + $this->accountRepository = app(AccountRepositoryInterface::class); } /** @@ -153,6 +163,7 @@ class TransactionJournalFactory $this->budgetRepository->setUser($this->user); $this->categoryRepository->setUser($this->user); $this->piggyRepository->setUser($this->user); + $this->accountRepository->setUser($this->user); } /** @@ -211,7 +222,7 @@ class TransactionJournalFactory { $row['import_hash_v2'] = $this->hashArray($row); - /** Get basic fields */ + /** Some basic fields */ $type = $this->typeRepository->findTransactionType(null, $row['type']); $carbon = $row['date'] ?? new Carbon; $order = $row['order'] ?? 0; @@ -224,6 +235,55 @@ class TransactionJournalFactory /** Manipulate basic fields */ $carbon->setTimezone(config('app.timezone')); + /** Get source + destination account */ + Log::debug(sprintf('Source info: ID #%d, name "%s"', $row['source_id'], $row['source_name'])); + Log::debug(sprintf('Destination info: ID #%d, name "%s"', $row['destination_id'], $row['destination_name'])); + // validate source and destination using a new Validator. + $this->validateAccounts($row); + + /** create or get source and destination accounts */ + $sourceAccount = $this->getAccount($type->type, 'source', (int)$row['source_id'], $row['source_name']); + $destinationAccount = $this->getAccount($type->type, 'destination', (int)$row['destination_id'], $row['destination_name']); + + /** double check currencies. */ + + if ($type->type === 'Withdrawal') { + // make sure currency is correct. + $currency = $this->getCurrency($currency, $sourceAccount); + // make sure foreign currency != currency. + if (null !== $foreignCurrency && $foreignCurrency->id === $currency->id) { + $foreignCurrency = null; + } + $sourceCurrency = $currency; + $destCurrency = $currency; + $sourceForeignCurrency = $foreignCurrency; + $destForeignCurrency = $foreignCurrency; + } + if ($type->type === 'Deposit') { + // make sure currency is correct. + $currency = $this->getCurrency($currency, $destinationAccount); + // make sure foreign currency != currency. + if (null !== $foreignCurrency && $foreignCurrency->id === $currency->id) { + $foreignCurrency = null; + } + + $sourceCurrency = $currency; + $destCurrency = $currency; + $sourceForeignCurrency = $foreignCurrency; + $destForeignCurrency = $foreignCurrency; + } + + if ($type->type === 'Transfer') { + // get currencies + $currency = $this->getCurrency($currency, $sourceAccount); + $foreignCurrency = $this->getCurrency($foreignCurrency, $destinationAccount); + + $sourceCurrency = $currency; + $destCurrency = $foreignCurrency; + $sourceForeignCurrency = $foreignCurrency; + $destForeignCurrency = $currency; + } + /** Create a basic journal. */ $journal = TransactionJournal::create( [ @@ -241,8 +301,26 @@ class TransactionJournalFactory Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description)); /** Create two transactions. */ - $this->transactionFactory->setJournal($journal); - $this->transactionFactory->createPair($row, $currency, $foreignCurrency); + /** @var TransactionFactory $transactionFactory */ + $transactionFactory = app(TransactionFactory::class); + $transactionFactory->setUser($this->user); + $transactionFactory->setJournal($journal); + $transactionFactory->setAccount($sourceAccount); + $transactionFactory->setCurrency($sourceCurrency); + $transactionFactory->setForeignCurrency($sourceForeignCurrency); + $transactionFactory->setReconciled($row['reconciled'] ?? false); + $transactionFactory->createNegative($row['amount'], $row['foreign_amount']); + + // and the destination one: + /** @var TransactionFactory $transactionFactory */ + $transactionFactory = app(TransactionFactory::class); + $transactionFactory->setUser($this->user); + $transactionFactory->setJournal($journal); + $transactionFactory->setAccount($destinationAccount); + $transactionFactory->setCurrency($destCurrency); + $transactionFactory->setForeignCurrency($destForeignCurrency); + $transactionFactory->setReconciled($row['reconciled'] ?? false); + $transactionFactory->createPositive($row['amount'], $row['foreign_amount']); // verify that journal has two transactions. Otherwise, delete and cancel. $count = $journal->transactions()->count(); @@ -315,4 +393,50 @@ class TransactionJournalFactory } + /** + * @param NullArrayObject $data + * + * @throws FireflyException + */ + private function validateAccounts(NullArrayObject $data): void + { + $transactionType = $data['type'] ?? 'invalid'; + $this->accountValidator->setUser($this->user); + $this->accountValidator->setTransactionType($transactionType); + + // validate source account. + $sourceId = isset($data['source_id']) ? (int)$data['source_id'] : null; + $sourceName = $data['source_name'] ?? null; + $validSource = $this->accountValidator->validateSource($sourceId, $sourceName); + + // do something with result: + if (false === $validSource) { + throw new FireflyException($this->accountValidator->sourceError); + } + Log::debug('Source seems valid.'); + // validate destination account + $destinationId = isset($data['destination_id']) ? (int)$data['destination_id'] : null; + $destinationName = $data['destination_name'] ?? null; + $validDestination = $this->accountValidator->validateDestination($destinationId, $destinationName); + // do something with result: + if (false === $validDestination) { + throw new FireflyException($this->accountValidator->destError); + } + } + + /** + * @param TransactionCurrency $currency + * @param Account $account + * @return TransactionCurrency + */ + private function getCurrency(TransactionCurrency $currency, Account $account): TransactionCurrency + { + $preference = $this->accountRepository->getAccountCurrency($account); + if (null === $preference && null === $currency) { + // return user's default: + return app('amount')->getDefaultCurrencyByUser($this->user); + } + + return $preference ?? $currency; + } } diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 922635aeed..e4d66e38ac 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -489,16 +489,6 @@ class BudgetRepository implements BudgetRepositoryInterface return $return; } - /** - * Returns all available budget objects. - * - * @return Collection - */ - public function getAvailableBudgets(): Collection - { - return $this->user->availableBudgets()->get(); - } - /** * Calculate the average amount in the budgets available in this period. * Grouped by day. @@ -955,48 +945,6 @@ class BudgetRepository implements BudgetRepositoryInterface return $budget; } - /** - * @param string $oldName - * @param string $newName - */ - private function updateRuleTriggers(string $oldName, string $newName): void - { - $types = ['budget_is',]; - $triggers = RuleTrigger::leftJoin('rules', 'rules.id', '=', 'rule_triggers.rule_id') - ->where('rules.user_id', $this->user->id) - ->whereIn('rule_triggers.trigger_type', $types) - ->where('rule_triggers.trigger_value', $oldName) - ->get(['rule_triggers.*']); - Log::debug(sprintf('Found %d triggers to update.', $triggers->count())); - /** @var RuleTrigger $trigger */ - foreach ($triggers as $trigger) { - $trigger->trigger_value = $newName; - $trigger->save(); - Log::debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value)); - } - } - - /** - * @param string $oldName - * @param string $newName - */ - private function updateRuleActions(string $oldName, string $newName): void - { - $types = ['set_budget',]; - $actions = RuleAction::leftJoin('rules', 'rules.id', '=', 'rule_actions.rule_id') - ->where('rules.user_id', $this->user->id) - ->whereIn('rule_actions.action_type', $types) - ->where('rule_actions.action_value', $oldName) - ->get(['rule_actions.*']); - Log::debug(sprintf('Found %d actions to update.', $actions->count())); - /** @var RuleAction $action */ - foreach ($actions as $action) { - $action->action_value = $newName; - $action->save(); - Log::debug(sprintf('Updated action %d: %s', $action->id, $action->action_value)); - } - } - /** * @param AvailableBudget $availableBudget * @param array $data @@ -1131,4 +1079,95 @@ class BudgetRepository implements BudgetRepositoryInterface return $limit; } + + /** + * Returns all available budget objects. + * + * @param Carbon|null $start + * @param Carbon|null $end + * @return Collection + * + */ + public function getAvailableBudgetsByDate(?Carbon $start, ?Carbon $end): Collection + { + $query = $this->user->availableBudgets(); + + if (null !== $start) { + $query->where('start_date', '>=', $start->format('Y-m-d H:i:s')); + } + if (null !== $end) { + $query->where('emd_date', '<=', $end->format('Y-m-d H:i:s')); + } + + return $query->get(); + } + + /** + * Returns all available budget objects. + * + * @param TransactionCurrency $currency + * @return Collection + */ + public function getAvailableBudgetsByCurrency(TransactionCurrency $currency): Collection + { + return $this->user->availableBudgets()->where('transaction_currency_id', $currency->id)->get(); + } + + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getAllBudgetLimitsByCurrency(TransactionCurrency $currency, Carbon $start = null, Carbon $end = null): Collection + { + return $this->getAllBudgetLimits($start, $end)->filter( + function (BudgetLimit $budgetLimit) use ($currency) { + return $budgetLimit->transaction_currency_id === $currency->id; + } + ); + } + + /** + * @param string $oldName + * @param string $newName + */ + private function updateRuleTriggers(string $oldName, string $newName): void + { + $types = ['budget_is',]; + $triggers = RuleTrigger::leftJoin('rules', 'rules.id', '=', 'rule_triggers.rule_id') + ->where('rules.user_id', $this->user->id) + ->whereIn('rule_triggers.trigger_type', $types) + ->where('rule_triggers.trigger_value', $oldName) + ->get(['rule_triggers.*']); + Log::debug(sprintf('Found %d triggers to update.', $triggers->count())); + /** @var RuleTrigger $trigger */ + foreach ($triggers as $trigger) { + $trigger->trigger_value = $newName; + $trigger->save(); + Log::debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value)); + } + } + + /** + * @param string $oldName + * @param string $newName + */ + private function updateRuleActions(string $oldName, string $newName): void + { + $types = ['set_budget',]; + $actions = RuleAction::leftJoin('rules', 'rules.id', '=', 'rule_actions.rule_id') + ->where('rules.user_id', $this->user->id) + ->whereIn('rule_actions.action_type', $types) + ->where('rule_actions.action_value', $oldName) + ->get(['rule_actions.*']); + Log::debug(sprintf('Found %d actions to update.', $actions->count())); + /** @var RuleAction $action */ + foreach ($actions as $action) { + $action->action_value = $newName; + $action->save(); + Log::debug(sprintf('Updated action %d: %s', $action->id, $action->action_value)); + } + } } diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index e647c2d5c6..b70e853116 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -55,8 +55,8 @@ interface BudgetRepositoryInterface * This method collects various info on budgets, used on the budget page and on the index. * * @param Collection $budgets - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return array */ @@ -82,12 +82,12 @@ interface BudgetRepositoryInterface public function destroyBudgetLimit(BudgetLimit $budgetLimit): void; /** - * @param int|null $budgetId + * @param int|null $budgetId * @param string|null $budgetName * * @return Budget|null */ - public function findBudget( ?int $budgetId, ?string $budgetName): ?Budget; + public function findBudget(?int $budgetId, ?string $budgetName): ?Budget; /** * Find budget by name. @@ -130,8 +130,17 @@ interface BudgetRepositoryInterface /** * @param TransactionCurrency $currency - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getAllBudgetLimitsByCurrency(TransactionCurrency $currency, Carbon $start = null, Carbon $end = null): Collection; + + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end * * @return string */ @@ -148,9 +157,20 @@ interface BudgetRepositoryInterface /** * Returns all available budget objects. * + * @param Carbon|null $start + * @param Carbon|null $end + * @return Collection + * + */ + public function getAvailableBudgetsByDate(?Carbon $start, ?Carbon $end): Collection; + + /** + * Returns all available budget objects. + * + * @param TransactionCurrency $currency * @return Collection */ - public function getAvailableBudgets(): Collection; + public function getAvailableBudgetsByCurrency(TransactionCurrency $currency): Collection; /** * Calculate the average amount in the budgets available in this period. @@ -175,8 +195,8 @@ interface BudgetRepositoryInterface /** * @param Collection $budgets * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return array */ @@ -205,8 +225,8 @@ interface BudgetRepositoryInterface /** * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return array */ @@ -221,9 +241,9 @@ interface BudgetRepositoryInterface /** * @param TransactionCurrency $currency - * @param Carbon $start - * @param Carbon $end - * @param string $amount + * @param Carbon $start + * @param Carbon $end + * @param string $amount * * @return AvailableBudget */ @@ -231,7 +251,7 @@ interface BudgetRepositoryInterface /** * @param Budget $budget - * @param int $order + * @param int $order */ public function setBudgetOrder(Budget $budget, int $order): void; @@ -245,8 +265,8 @@ interface BudgetRepositoryInterface /** * @param Collection $budgets * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return string */ @@ -259,8 +279,8 @@ interface BudgetRepositoryInterface * * @param Collection $budgets * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return array */ @@ -268,8 +288,8 @@ interface BudgetRepositoryInterface /** * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return string */ @@ -277,8 +297,8 @@ interface BudgetRepositoryInterface /** * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param Carbon $start + * @param Carbon $end * * @return array */ @@ -301,7 +321,7 @@ interface BudgetRepositoryInterface /** * @param Budget $budget - * @param array $data + * @param array $data * * @return Budget */ @@ -309,7 +329,7 @@ interface BudgetRepositoryInterface /** * @param AvailableBudget $availableBudget - * @param array $data + * @param array $data * * @return AvailableBudget */ @@ -317,7 +337,7 @@ interface BudgetRepositoryInterface /** * @param BudgetLimit $budgetLimit - * @param array $data + * @param array $data * * @return BudgetLimit */ diff --git a/app/Support/Http/Api/ApiSupport.php b/app/Support/Http/Api/ApiSupport.php new file mode 100644 index 0000000000..fe92f28f2c --- /dev/null +++ b/app/Support/Http/Api/ApiSupport.php @@ -0,0 +1,66 @@ +. + */ + +namespace FireflyIII\Support\Http\Api; + +use FireflyIII\Models\Account; +use Illuminate\Support\Collection; + +/** + * Trait ApiSupport + */ +trait ApiSupport +{ + /** + * Small helper function for the revenue and expense account charts. + * + * @param array $names + * + * @return array + */ + protected function expandNames(array $names): array + { + $result = []; + foreach ($names as $entry) { + $result[$entry['name']] = 0; + } + + return $result; + } + + /** + * Small helper function for the revenue and expense account charts. + * + * @param Collection $accounts + * + * @return array + */ + protected function extractNames(Collection $accounts): array + { + $return = []; + /** @var Account $account */ + foreach ($accounts as $account) { + $return[$account->id] = $account->name; + } + + return $return; + } +} \ No newline at end of file diff --git a/app/Validation/TransactionValidation.php b/app/Validation/TransactionValidation.php index ec7714363f..badae75a01 100644 --- a/app/Validation/TransactionValidation.php +++ b/app/Validation/TransactionValidation.php @@ -25,6 +25,8 @@ namespace FireflyIII\Validation; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use Illuminate\Validation\Validator; /** @@ -178,6 +180,7 @@ trait TransactionValidation $first = $unique[0] ?? 'invalid'; if ('invalid' === $first) { $validator->errors()->add('transactions.0.type', (string)trans('validation.invalid_transaction_type')); + } } @@ -192,7 +195,11 @@ trait TransactionValidation $transactions = $data['transactions'] ?? []; $types = []; foreach ($transactions as $index => $transaction) { - $types[] = $transaction['type'] ?? 'invalid'; + $originalType = $this->getOriginalType((int)($transaction['transaction_journal_id'] ?? 0)); + // if type is not set, fall back to the type of the journal, if one is given. + + + $types[] = $transaction['type'] ?? $originalType; } $unique = array_unique($types); if (count($unique) > 1) { @@ -202,6 +209,51 @@ trait TransactionValidation } } + /** + * Validates the given account information. Switches on given transaction type. + * + * @param Validator $validator + */ + public function validateAccountInformationUpdate(Validator $validator): void + { + $data = $validator->getData(); + $transactions = $data['transactions'] ?? []; + + /** @var AccountValidator $accountValidator */ + $accountValidator = app(AccountValidator::class); + + foreach ($transactions as $index => $transaction) { + $originalType = $this->getOriginalType($transaction['transaction_journal_id'] ?? 0); + $originalData = $this->getOriginalData($transaction['transaction_journal_id'] ?? 0); + $transactionType = $transaction['type'] ?? $originalType; + $accountValidator->setTransactionType($transactionType); + + // validate source account. + $sourceId = isset($transaction['source_id']) ? (int)$transaction['source_id'] : $originalData['source_id']; + $sourceName = $transaction['source_name'] ?? $originalData['source_name']; + $validSource = $accountValidator->validateSource($sourceId, $sourceName); + + // do something with result: + if (false === $validSource) { + $validator->errors()->add(sprintf('transactions.%d.source_id', $index), $accountValidator->sourceError); + $validator->errors()->add(sprintf('transactions.%d.source_name', $index), $accountValidator->sourceError); + + continue; + } + // validate destination account + $destinationId = isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : $originalData['destination_id']; + $destinationName = $transaction['destination_name'] ?? $originalData['destination_name']; + $validDestination = $accountValidator->validateDestination($destinationId, $destinationName); + // do something with result: + if (false === $validDestination) { + $validator->errors()->add(sprintf('transactions.%d.destination_id', $index), $accountValidator->destError); + $validator->errors()->add(sprintf('transactions.%d.destination_name', $index), $accountValidator->destError); + + continue; + } + } + } + /** * @param Validator $validator */ @@ -356,6 +408,24 @@ trait TransactionValidation return $return; } + /** + * @param int $journalId + * @return string + */ + private function getOriginalType(int $journalId): string + { + if (0 === $journalId) { + return 'invalid'; + } + /** @var TransactionJournal $journal */ + $journal = TransactionJournal::with(['transactionType'])->find($journalId); + if (null !== $journal) { + return strtolower($journal->transactionType->type); + } + + return 'invalid'; + } + /** * @param Validator $validator * @param TransactionGroup $transactionGroup