From c519b4d0df4c41d11e1ec720be90ea4f3ffaccae Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 6 Apr 2019 08:10:50 +0200 Subject: [PATCH] Is now capable of updating transactions over the API. --- .../V1/Controllers/TransactionController.php | 33 +- ...equest.php => TransactionStoreRequest.php} | 27 +- .../V1/Requests/TransactionUpdateRequest.php | 316 ++++++++ .../Commands/Correction/CorrectDatabase.php | 1 + .../Commands/Upgrade/MigrateToGroups.php | 32 +- app/Factory/TransactionFactory.php | 146 +--- app/Factory/TransactionJournalFactory.php | 114 +-- app/Factory/TransactionJournalMetaFactory.php | 7 + .../Events/UpdatedGroupEventHandler.php | 2 +- app/Helpers/Collector/GroupCollector.php | 1 + .../Controllers/System/InstallController.php | 4 +- app/Http/Requests/Request.php | 3 + app/Models/TransactionJournal.php | 5 +- app/Providers/JournalServiceProvider.php | 14 +- app/Repositories/Budget/BudgetRepository.php | 15 +- .../Budget/BudgetRepositoryInterface.php | 3 +- .../Category/CategoryRepository.php | 19 +- .../Category/CategoryRepositoryInterface.php | 3 +- .../Currency/CurrencyRepository.php | 33 +- .../Currency/CurrencyRepositoryInterface.php | 14 +- .../Journal/JournalRepository.php | 34 - .../Journal/JournalRepositoryInterface.php | 14 - .../TransactionGroupRepository.php | 49 ++ .../TransactionGroupRepositoryInterface.php | 31 + .../TransactionTypeRepository.php | 4 +- .../Internal/Support/JournalServiceTrait.php | 490 ++++++++---- .../Internal/Update/GroupUpdateService.php | 84 ++ .../Internal/Update/JournalUpdateService.php | 718 +++++++++++++++--- app/Support/Binder/TransactionGroup.php | 53 ++ .../Import/Placeholder/ImportTransaction.php | 4 +- .../TransactionGroupTransformer.php | 3 +- app/Validation/AccountValidator.php | 44 +- app/Validation/TransactionValidation.php | 58 +- config/firefly.php | 116 ++- resources/lang/en_US/validation.php | 1 + routes/api.php | 54 +- 36 files changed, 1840 insertions(+), 709 deletions(-) rename app/Api/V1/Requests/{TransactionRequest.php => TransactionStoreRequest.php} (94%) create mode 100644 app/Api/V1/Requests/TransactionUpdateRequest.php create mode 100644 app/Services/Internal/Update/GroupUpdateService.php create mode 100644 app/Support/Binder/TransactionGroup.php diff --git a/app/Api/V1/Controllers/TransactionController.php b/app/Api/V1/Controllers/TransactionController.php index 7af174717c..e017d038da 100644 --- a/app/Api/V1/Controllers/TransactionController.php +++ b/app/Api/V1/Controllers/TransactionController.php @@ -24,7 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers; -use FireflyIII\Api\V1\Requests\TransactionRequest; +use FireflyIII\Api\V1\Requests\TransactionStoreRequest; +use FireflyIII\Api\V1\Requests\TransactionUpdateRequest; use FireflyIII\Events\StoredTransactionGroup; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Exceptions\FireflyException; @@ -32,6 +33,7 @@ use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface; use FireflyIII\Support\Http\Api\TransactionFilter; use FireflyIII\Transformers\AttachmentTransformer; use FireflyIII\Transformers\PiggyBankEventTransformer; @@ -44,16 +46,18 @@ use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Resource\Collection as FractalCollection; use League\Fractal\Resource\Item; use League\Fractal\Serializer\JsonApiSerializer; +use Log; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class TransactionController - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TransactionController extends Controller { use TransactionFilter; + /** @var TransactionGroupRepositoryInterface Group repository. */ + private $groupRepository; /** @var JournalRepositoryInterface The journal repository */ private $repository; @@ -68,9 +72,10 @@ class TransactionController extends Controller /** @var User $admin */ $admin = auth()->user(); - /** @var JournalRepositoryInterface repository */ - $this->repository = app(JournalRepositoryInterface::class); + $this->repository = app(JournalRepositoryInterface::class); + $this->groupRepository = app(TransactionGroupRepositoryInterface::class); $this->repository->setUser($admin); + $this->groupRepository->setUser($admin); return $next($request); } @@ -247,18 +252,16 @@ class TransactionController extends Controller /** * Store a new transaction. * - * @param TransactionRequest $request + * @param TransactionStoreRequest $request * - * @param JournalRepositoryInterface $repository - * - * @throws FireflyException * @return JsonResponse + * @throws FireflyException */ - public function store(TransactionRequest $request, JournalRepositoryInterface $repository): JsonResponse + public function store(TransactionStoreRequest $request): JsonResponse { $data = $request->getAll(); $data['user'] = auth()->user()->id; - $transactionGroup = $repository->store($data); + $transactionGroup = $this->groupRepository->store($data); event(new StoredTransactionGroup($transactionGroup)); @@ -294,16 +297,16 @@ class TransactionController extends Controller /** * Update a transaction. * - * @param TransactionRequest $request - * @param TransactionGroup $transactionGroup + * @param TransactionUpdateRequest $request + * @param TransactionGroup $transactionGroup * * @return JsonResponse */ - public function update(TransactionRequest $request, TransactionGroup $transactionGroup): JsonResponse + public function update(TransactionUpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse { + Log::debug('Now in update routine.'); $data = $request->getAll(); - $data['user'] = auth()->user()->id; - $transactionGroup = $this->repository->update($transactionGroup, $data); + $transactionGroup = $this->groupRepository->update($transactionGroup, $data); $manager = new Manager(); $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; $manager->setSerializer(new JsonApiSerializer($baseUrl)); diff --git a/app/Api/V1/Requests/TransactionRequest.php b/app/Api/V1/Requests/TransactionStoreRequest.php similarity index 94% rename from app/Api/V1/Requests/TransactionRequest.php rename to app/Api/V1/Requests/TransactionStoreRequest.php index ddd2654b65..e8a99c8c85 100644 --- a/app/Api/V1/Requests/TransactionRequest.php +++ b/app/Api/V1/Requests/TransactionStoreRequest.php @@ -1,7 +1,7 @@ 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation', 'transactions.*.date' => ['required', new IsDateOrTime], + 'transactions.*.order' => 'numeric|min:0', // currency info 'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id', @@ -144,10 +145,6 @@ class TransactionRequest extends Request 'transactions.*.invoice_date' => 'date|nullable', ]; - if ('PUT' === $this->method()) { - unset($rules['transactions.*.type'], $rules['transactions.*.piggy_bank_id'], $rules['transactions.*.piggy_bank_name']); - } - return $rules; @@ -156,7 +153,7 @@ class TransactionRequest extends Request /** * Configure the validator instance. * - * @param Validator $validator + * @param Validator $validator * * @return void */ @@ -170,14 +167,13 @@ class TransactionRequest extends Request // all journals must have a description $this->validateDescriptions($validator); - // all transaction types must be equal: - $this->validateTransactionTypes($validator); + // all transaction types must be equal: + $this->validateTransactionTypes($validator); // validate foreign currency info $this->validateForeignCurrencyInformation($validator); - // validate all account info $this->validateAccountInformation($validator); @@ -186,6 +182,8 @@ class TransactionRequest extends Request // the group must have a description if > 1 journal. $this->validateGroupDescription($validator); + + // TODO validate that the currency fits the source and/or destination account. } ); } @@ -207,9 +205,10 @@ class TransactionRequest extends Request foreach ($this->get('transactions') as $index => $transaction) { $object = new NullArrayObject($transaction); $return[] = [ - // $this->dateFromValue($object['']) - 'type' => $this->stringFromValue($object['type']), - 'date' => $this->dateFromValue($object['date']), + 'type' => $this->stringFromValue($object['type']), + 'date' => $this->dateFromValue($object['date']), + 'order' => $this->integerFromValue((string)$object['order']), + 'currency_id' => $this->integerFromValue($object['currency_id']), 'currency_code' => $this->stringFromValue($object['currency_code']), diff --git a/app/Api/V1/Requests/TransactionUpdateRequest.php b/app/Api/V1/Requests/TransactionUpdateRequest.php new file mode 100644 index 0000000000..72ec3e6811 --- /dev/null +++ b/app/Api/V1/Requests/TransactionUpdateRequest.php @@ -0,0 +1,316 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Requests; + +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Rules\BelongsUser; +use FireflyIII\Rules\IsBoolean; +use FireflyIII\Rules\IsDateOrTime; +use FireflyIII\Validation\TransactionValidation; +use Illuminate\Validation\Validator; + + +/** + * Class TransactionUpdateRequest + */ +class TransactionUpdateRequest extends Request +{ + use TransactionValidation; + + /** @var array Array values. */ + private $arrayFields; + /** @var array Boolean values. */ + private $booleanFields; + /** @var array Fields that contain date values. */ + private $dateFields; + /** @var array Fields that contain integer values. */ + private $integerFields; + /** @var array Fields that contain string values. */ + private $stringFields; + + /** + * Authorize logged in users. + * + * @return bool + */ + public function authorize(): bool + { + // Only allow authenticated users + return auth()->check(); + } + + /** + * Get all data. Is pretty complex because of all the ??-statements. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @return array + */ + public function getAll(): array + { + $this->integerFields = [ + 'order', + 'currency_id', + 'foreign_currency_id', + 'source_id', + 'destination_id', + 'budget_id', + 'category_id', + 'bill_id', + 'recurrence_id', + ]; + + $this->dateFields = [ + 'date', + 'interest_date', + 'book_date', + 'process_date', + 'due_date', + 'payment_date', + 'invoice_date', + ]; + + $this->stringFields = [ + 'type', + 'currency_code', + 'foreign_currency_code', + 'amount', + 'foreign_amount', + 'description', + 'source_name', + 'destination_name', + 'budget_name', + 'category_name', + 'bill_name', + 'notes', + 'internal_reference', + 'external_id', + 'bunq_payment_id', + 'sepa_cc', + 'sepa_ct_op', + 'sepa_ct_id', + 'sepa_db', + 'sepa_country', + 'sepa_ep', + 'sepa_ci', + 'sepa_batch_id', + ]; + $this->booleanFields = [ + 'reconciled', + ]; + + $this->arrayFields = [ + 'tags', + ]; + + + $data = [ + 'transactions' => $this->getTransactionData(), + ]; + if ($this->has('group_title')) { + $data['group_title'] = $this->string('group_title'); + } + + return $data; + } + + /** + * The rules that the incoming request must be matched against. + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function rules(): array + { + $rules = [ + // basic fields for group: + 'group_title' => 'between:1,255', + + // transaction rules (in array for splits): + 'transactions.*.type' => 'in:withdrawal,deposit,transfer,opening-balance,reconciliation', + 'transactions.*.date' => [new IsDateOrTime], + 'transactions.*.order' => 'numeric|min:0', + + // currency info + 'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id', + 'transactions.*.currency_code' => 'min:3|max:3|exists:transaction_currencies,code', + 'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id', + 'transactions.*.foreign_currency_code' => 'min:3|max:3|exists:transaction_currencies,code', + + // amount + 'transactions.*.amount' => 'numeric|more:0', + 'transactions.*.foreign_amount' => 'numeric|gte:0', + + // description + 'transactions.*.description' => 'nullable|between:1,255', + + // source of transaction + 'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser], + 'transactions.*.source_name' => 'between:1,255|nullable', + + // destination of transaction + 'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser], + 'transactions.*.destination_name' => 'between:1,255|nullable', + + // budget, category, bill and piggy + 'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser], + 'transactions.*.budget_name' => ['between:1,255', 'nullable', new BelongsUser], + 'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser], + 'transactions.*.category_name' => 'between:1,255|nullable', + 'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser], + 'transactions.*.bill_name' => ['between:1,255', 'nullable', new BelongsUser], + + // other interesting fields + 'transactions.*.reconciled' => [new IsBoolean], + 'transactions.*.notes' => 'min:1,max:50000|nullable', + 'transactions.*.tags' => 'between:0,255', + + // meta info fields + 'transactions.*.internal_reference' => 'min:1,max:255|nullable', + 'transactions.*.external_id' => 'min:1,max:255|nullable', + 'transactions.*.recurrence_id' => 'min:1,max:255|nullable', + 'transactions.*.bunq_payment_id' => 'min:1,max:255|nullable', + + // SEPA fields: + 'transactions.*.sepa_cc' => 'min:1,max:255|nullable', + 'transactions.*.sepa_ct_op' => 'min:1,max:255|nullable', + 'transactions.*.sepa_ct_id' => 'min:1,max:255|nullable', + 'transactions.*.sepa_db' => 'min:1,max:255|nullable', + 'transactions.*.sepa_country' => 'min:1,max:255|nullable', + 'transactions.*.sepa_ep' => 'min:1,max:255|nullable', + 'transactions.*.sepa_ci' => 'min:1,max:255|nullable', + 'transactions.*.sepa_batch_id' => 'min:1,max:255|nullable', + + // dates + 'transactions.*.interest_date' => 'date|nullable', + 'transactions.*.book_date' => 'date|nullable', + 'transactions.*.process_date' => 'date|nullable', + 'transactions.*.due_date' => 'date|nullable', + 'transactions.*.payment_date' => 'date|nullable', + 'transactions.*.invoice_date' => 'date|nullable', + ]; + + return $rules; + } + + /** + * Configure the validator instance. + * + * @param Validator $validator + * + * @return void + */ + public function withValidator(Validator $validator): void + { + /** @var TransactionGroup $transactionGroup */ + $transactionGroup = $this->route()->parameter('transactionGroup'); + $validator->after( + function (Validator $validator) use ($transactionGroup) { + // must submit at least one transaction. + $this->validateOneTransaction($validator); + + // if more than one, verify that there are journal ID's present. + $this->validateJournalIds($validator, $transactionGroup); + + // all transaction types must be equal: + $this->validateTransactionTypesForUpdate($validator); + + // if type is set, source + destination info is mandatory. + $this->validateAccountPresence($validator); + + // TODO validate that the currency fits the source and/or destination account. + + // all journals must have a description + //$this->validateDescriptions($validator); + + // // validate foreign currency info + // $this->validateForeignCurrencyInformation($validator); + // + // + // // validate all account info + // $this->validateAccountInformation($validator); + // + // // make sure all splits have valid source + dest info + // $this->validateSplitAccounts($validator); + // the group must have a description if > 1 journal. + // $this->validateGroupDescription($validator); + } + ); + } + + /** + * Get transaction data. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @return array + */ + private function getTransactionData(): array + { + $return = []; + /** + * @var int $index + * @var array $transaction + */ + foreach ($this->get('transactions') as $index => $transaction) { + // default response is to update nothing in the transaction: + $current = []; + + // for each field, add it to the array if a reference is present in the request: + foreach ($this->integerFields as $fieldName) { + if (array_key_exists($fieldName, $transaction)) { + $current[$fieldName] = $this->integerFromValue((string)$transaction[$fieldName]); + } + } + + foreach ($this->stringFields as $fieldName) { + if (array_key_exists($fieldName, $transaction)) { + $current[$fieldName] = $this->stringFromValue((string)$transaction[$fieldName]); + } + } + + foreach ($this->dateFields as $fieldName) { + if (array_key_exists($fieldName, $transaction)) { + $current[$fieldName] = $this->dateFromValue((string)$transaction[$fieldName]); + } + } + + foreach ($this->booleanFields as $fieldName) { + if (array_key_exists($fieldName, $transaction)) { + $current[$fieldName] = $this->convertBoolean((string)$transaction[$fieldName]); + } + } + + foreach ($this->arrayFields as $fieldName) { + if (array_key_exists($fieldName, $transaction)) { + $current[$fieldName] = $this->arrayFromValue((string)$transaction[$fieldName]); + } + } + $return[] = $current; + } + + return $return; + } +} diff --git a/app/Console/Commands/Correction/CorrectDatabase.php b/app/Console/Commands/Correction/CorrectDatabase.php index 189f4a7801..212fa9bb6e 100644 --- a/app/Console/Commands/Correction/CorrectDatabase.php +++ b/app/Console/Commands/Correction/CorrectDatabase.php @@ -68,6 +68,7 @@ class CorrectDatabase extends Command 'firefly-iii:delete-empty-journals', 'firefly-iii:delete-empty-groups', 'firefly-iii:fix-account-types', + 'firefly-iii:rename-meta-fields' ]; foreach ($commands as $command) { $this->line(sprintf('Now executing %s', $command)); diff --git a/app/Console/Commands/Upgrade/MigrateToGroups.php b/app/Console/Commands/Upgrade/MigrateToGroups.php index 0dd6f17d5f..2f61c24b98 100644 --- a/app/Console/Commands/Upgrade/MigrateToGroups.php +++ b/app/Console/Commands/Upgrade/MigrateToGroups.php @@ -23,7 +23,7 @@ namespace FireflyIII\Console\Commands\Upgrade; use DB; use Exception; -use FireflyIII\Factory\TransactionJournalFactory; +use FireflyIII\Factory\TransactionGroupFactory; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; @@ -54,13 +54,10 @@ class MigrateToGroups extends Command * @var string */ protected $signature = 'firefly-iii:migrate-to-groups {--F|force : Force the migration, even if it fired before.}'; - - /** @var TransactionJournalFactory */ - private $journalFactory; - + /** @var TransactionGroupFactory */ + private $groupFactory; /** @var JournalRepositoryInterface */ private $journalRepository; - /** @var JournalDestroyService */ private $service; @@ -72,9 +69,9 @@ class MigrateToGroups extends Command public function __construct() { parent::__construct(); - $this->journalFactory = app(TransactionJournalFactory::class); $this->journalRepository = app(JournalRepositoryInterface::class); $this->service = app(JournalDestroyService::class); + $this->groupFactory = app(TransactionGroupFactory::class); } /** @@ -119,7 +116,7 @@ class MigrateToGroups extends Command private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction { $set = $journal->transactions->filter( - function (Transaction $subject) use ($transaction) { + static function (Transaction $subject) use ($transaction) { return $transaction->amount * -1 === (float)$subject->amount && $transaction->identifier === $subject->identifier; } ); @@ -135,7 +132,7 @@ class MigrateToGroups extends Command private function getDestinationTransactions(TransactionJournal $journal): Collection { return $journal->transactions->filter( - function (Transaction $transaction) { + static function (Transaction $transaction) { return $transaction->amount > 0; } ); @@ -224,13 +221,10 @@ class MigrateToGroups extends Command Log::debug(sprintf('Will now try to convert journal #%d', $journal->id)); $this->journalRepository->setUser($journal->user); - $this->journalFactory->setUser($journal->user); + $this->groupFactory->setUser($journal->user); $data = [ // mandatory fields. - 'type' => strtolower($journal->transactionType->type), - 'date' => $journal->date, - 'user' => $journal->user_id, 'group_title' => $journal->description, 'transactions' => [], ]; @@ -280,6 +274,9 @@ class MigrateToGroups extends Command } $tArray = [ + 'type' => strtolower($journal->transactionType->type), + 'date' => $journal->date, + 'user' => $journal->user_id, 'currency_id' => $transaction->transaction_currency_id, 'foreign_currency_id' => $transaction->foreign_currency_id, 'amount' => $transaction->amount, @@ -318,21 +315,18 @@ class MigrateToGroups extends Command $data['transactions'][] = $tArray; } Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions']))); - $result = $this->journalFactory->create($data); + $group = $this->groupFactory->create($data); Log::debug('Done calling transaction journal factory'); // delete the old transaction journal. $this->service->destroy($journal); - // first group ID - $first = $result->first() ? $result->first()->transaction_group_id : 0; - // report on result: Log::debug( - sprintf('Migrated journal #%d into group #%d with these journals: #%s', $journal->id, $first, implode(', #', $result->pluck('id')->toArray())) + sprintf('Migrated journal #%d into group #%d with these journals: #%s', $journal->id, $group->id, implode(', #', $group->transactionJournals->pluck('id')->toArray())) ); $this->line( - sprintf('Migrated journal #%d into group #%d with these journals: #%s', $journal->id, $first, implode(', #', $result->pluck('id')->toArray())) + sprintf('Migrated journal #%d into group #%d with these journals: #%s', $journal->id, $group->id, implode(', #', $group->transactionJournals->pluck('id')->toArray())) ); } diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index 329397fa08..7890918d06 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -27,11 +27,11 @@ namespace FireflyIII\Factory; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; 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; @@ -44,8 +44,8 @@ use Log; */ class TransactionFactory { - /** @var AccountRepositoryInterface */ - private $accountRepository; + use JournalServiceTrait; + /** @var AccountValidator */ private $accountValidator; /** @var TransactionJournal */ @@ -113,12 +113,16 @@ class TransactionFactory */ public function createPair(NullArrayObject $data, TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): Collection { + 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: - $sourceAccount = $this->getAccount('source', (int)$data['source_id'], $data['source_name']); - $destinationAccount = $this->getAccount('destination', (int)$data['destination_id'], $data['destination_name']); + $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']); $amount = $this->getAmount($data['amount']); $foreignAmount = $this->getForeignAmount($data['foreign_amount']); @@ -129,7 +133,7 @@ class TransactionFactory $two->reconciled = $data['reconciled'] ?? false; // add foreign currency info to $one and $two if necessary. - if (null !== $foreignCurrency) { + if (null !== $foreignCurrency && null !== $foreignAmount) { $one->foreign_currency_id = $foreignCurrency->id; $two->foreign_currency_id = $foreignCurrency->id; $one->foreign_amount = app('steam')->negative($foreignAmount); @@ -144,134 +148,6 @@ class TransactionFactory } - /** - * @param string $direction - * @param int|null $accountId - * @param string|null $accountName - * - * @return Account - * @throws FireflyException - */ - public function getAccount(string $direction, ?int $accountId, ?string $accountName): Account - { - // some debug logging: - Log::debug(sprintf('Now in getAccount(%s, %d, %s)', $direction, $accountId, $accountName)); - - // final result: - $result = null; - - // expected type of source account, in order of preference - /** @var array $array */ - $array = config('firefly.expected_source_types'); - $expectedTypes = $array[$direction]; - unset($array); - - // and now try to find it, based on the type of transaction. - $transactionType = $this->journal->transactionType->type; - $message = 'Based on the fact that the transaction is a %s, the %s account should be in: %s'; - Log::debug(sprintf($message, $transactionType, $direction, implode(', ', $expectedTypes[$transactionType]))); - - // first attempt, find by ID. - if (null !== $accountId) { - $search = $this->accountRepository->findNull($accountId); - if (null !== $search && in_array($search->accountType->type, $expectedTypes[$transactionType], true)) { - Log::debug( - sprintf('Found "account_id" object for %s: #%d, "%s" of type %s', $direction, $search->id, $search->name, $search->accountType->type) - ); - $result = $search; - } - } - - // second attempt, find by name. - if (null === $result && null !== $accountName) { - Log::debug('Found nothing by account ID.'); - // find by preferred type. - $source = $this->accountRepository->findByName($accountName, [$expectedTypes[$transactionType][0]]); - // or any expected type. - $source = $source ?? $this->accountRepository->findByName($accountName, $expectedTypes[$transactionType]); - - if (null !== $source) { - Log::debug(sprintf('Found "account_name" object for %s: #%d, %s', $direction, $source->id, $source->name)); - - $result = $source; - } - } - - // return cash account. - if (null === $result && null === $accountName - && in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) { - $result = $this->accountRepository->getCashAccount(); - } - - // return new account. - if (null === $result) { - $accountName = $accountName ?? '(no name)'; - // final attempt, create it. - $preferredType = $expectedTypes[$transactionType][0]; - if (AccountType::ASSET === $preferredType) { - throw new FireflyException(sprintf('TransactionFactory: Cannot create asset account with ID #%d or name "%s".', $accountId, $accountName)); - } - - $result = $this->accountRepository->store( - [ - 'account_type_id' => null, - 'accountType' => $preferredType, - 'name' => $accountName, - 'active' => true, - 'iban' => null, - ] - ); - } - - return $result; - } - - /** - * @param string $amount - * - * @return string - * @throws FireflyException - */ - public function getAmount(string $amount): string - { - if ('' === $amount) { - throw new FireflyException(sprintf('The amount cannot be an empty string: "%s"', $amount)); - } - if (0 === bccomp('0', $amount)) { - throw new FireflyException(sprintf('The amount seems to be zero: "%s"', $amount)); - } - - return $amount; - } - - /** - * @param string|null $amount - * - * @return string - */ - public function getForeignAmount(?string $amount): ?string - { - $result = null; - if (null === $amount) { - Log::debug('No foreign amount info in array. Return NULL'); - - return null; - } - if ('' === $amount) { - Log::debug('Foreign amount is empty string, return NULL.'); - - return null; - } - if (0 === bccomp('0', $amount)) { - Log::debug('Foreign amount is 0.0, return NULL.'); - - return null; - } - Log::debug(sprintf('Foreign amount is %s', $amount)); - - return $amount; - } - /** * @param TransactionJournal $journal @@ -298,6 +174,7 @@ class TransactionFactory private function validateAccounts(NullArrayObject $data): void { $transactionType = $data['type'] ?? 'invalid'; + $this->accountValidator->setUser($this->journal->user); $this->accountValidator->setTransactionType($transactionType); // validate source account. @@ -309,6 +186,7 @@ class TransactionFactory 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; diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index 4f2ab89126..797451aa69 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -27,7 +27,6 @@ namespace FireflyIII\Factory; use Carbon\Carbon; use Exception; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Models\Note; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; @@ -37,6 +36,7 @@ use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface; +use FireflyIII\Services\Internal\Support\JournalServiceTrait; use FireflyIII\Support\NullArrayObject; use FireflyIII\User; use Illuminate\Support\Collection; @@ -47,12 +47,10 @@ use Log; */ class TransactionJournalFactory { + use JournalServiceTrait; + /** @var BillRepositoryInterface */ private $billRepository; - /** @var BudgetRepositoryInterface */ - private $budgetRepository; - /** @var CategoryRepositoryInterface */ - private $categoryRepository; /** @var CurrencyRepositoryInterface */ private $currencyRepository; /** @var array */ @@ -61,8 +59,6 @@ class TransactionJournalFactory private $piggyEventFactory; /** @var PiggyBankRepositoryInterface */ private $piggyRepository; - /** @var TagFactory */ - private $tagFactory; /** @var TransactionFactory */ private $transactionFactory; /** @var TransactionTypeRepositoryInterface */ @@ -88,7 +84,7 @@ class TransactionJournalFactory 'due_date', 'payment_date', 'invoice_date', // others - 'recurrence_id', 'internal_reference', 'bunq_payment_id', + 'recurrence_id', 'internal_reference', 'bunq_payment_id', 'import_hash', 'import_hash_v2', 'external_id', 'original_source']; @@ -151,6 +147,7 @@ class TransactionJournalFactory { $this->user = $user; $this->currencyRepository->setUser($this->user); + $this->tagFactory->setUser($user); $this->transactionFactory->setUser($this->user); $this->billRepository->setUser($this->user); $this->budgetRepository->setUser($this->user); @@ -184,31 +181,6 @@ class TransactionJournalFactory Log::debug('Create no piggy event'); } - /** - * Link tags to journal. - * - * @param TransactionJournal $journal - * @param array $tags - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function storeTags(TransactionJournal $journal, ?array $tags): void - { - $this->tagFactory->setUser($journal->user); - $set = []; - if (!\is_array($tags)) { - return; - } - foreach ($tags as $string) { - if ('' !== $string) { - $tag = $this->tagFactory->findOrCreate($string); - if (null !== $tag) { - $set[] = $tag->id; - } - } - } - $journal->tags()->sync($set); - } - /** * @param TransactionJournal $journal * @param NullArrayObject $data @@ -229,27 +201,6 @@ class TransactionJournalFactory $factory->updateOrCreate($set); } - /** - * @param TransactionJournal $journal - * @param string $notes - */ - protected function storeNote(TransactionJournal $journal, ?string $notes): void - { - $notes = (string)$notes; - if ('' !== $notes) { - $note = $journal->notes()->first(); - if (null === $note) { - $note = new Note; - $note->noteable()->associate($journal); - } - $note->text = $notes; - $note->save(); - Log::debug(sprintf('Stored notes for journal #%d', $journal->id)); - - return; - } - } - /** * @param NullArrayObject $row * @@ -263,8 +214,9 @@ class TransactionJournalFactory /** Get basic fields */ $type = $this->typeRepository->findTransactionType(null, $row['type']); $carbon = $row['date'] ?? new Carbon; + $order = $row['order'] ?? 0; $currency = $this->currencyRepository->findCurrency((int)$row['currency_id'], $row['currency_code']); - $foreignCurrency = $this->findForeignCurrency($row); + $foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'], $row['foreign_currency_code']); $bill = $this->billRepository->findBill((int)$row['bill_id'], $row['bill_name']); $billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null; $description = app('steam')->cleanString((string)$row['description']); @@ -281,7 +233,7 @@ class TransactionJournalFactory 'transaction_currency_id' => $currency->id, 'description' => '' === $description ? '(empty description)' : $description, 'date' => $carbon->format('Y-m-d H:i:s'), - 'order' => 0, + 'order' => $order, 'tag_count' => 0, 'completed' => 0, ] @@ -318,7 +270,7 @@ class TransactionJournalFactory $this->storeCategory($journal, $row); /** Set notes */ - $this->storeNote($journal, $row['notes']); + $this->storeNotes($journal, $row['notes']); /** Set piggy bank */ $this->storePiggyEvent($journal, $row); @@ -332,22 +284,6 @@ class TransactionJournalFactory return $journal; } - /** - * This is a separate function because "findCurrency" will default to EUR and that may not be what we want. - * - * @param NullArrayObject $transaction - * - * @return TransactionCurrency|null - */ - private function findForeignCurrency(NullArrayObject $transaction): ?TransactionCurrency - { - if (null === $transaction['foreign_currency'] && null === $transaction['foreign_currency_id'] && null === $transaction['foreign_currency_code']) { - return null; - } - - return $this->currencyRepository->findCurrency((int)$transaction['foreign_currency_id'], $transaction['foreign_currency_code']); - } - /** * @param NullArrayObject $row * @@ -355,7 +291,7 @@ class TransactionJournalFactory */ private function hashArray(NullArrayObject $row): string { - $row['import_hash_v2'] = null; + $row['import_hash_v2'] = null; $row['original_source'] = null; $json = json_encode($row); if (false === $json) { @@ -367,36 +303,6 @@ class TransactionJournalFactory return $hash; } - - /** - * @param TransactionJournal $journal - * @param NullArrayObject $data - */ - private function storeBudget(TransactionJournal $journal, NullArrayObject $data): void - { - if (TransactionType::WITHDRAWAL !== $journal->transactionType->type) { - return; - } - $budget = $this->budgetRepository->findBudget($data['budget'], $data['budget_id'], $data['budget_name']); - if (null !== $budget) { - Log::debug(sprintf('Link budget #%d to journal #%d', $budget->id, $journal->id)); - $journal->budgets()->sync([$budget->id]); - } - } - - /** - * @param TransactionJournal $journal - * @param NullArrayObject $data - */ - private function storeCategory(TransactionJournal $journal, NullArrayObject $data): void - { - $category = $this->categoryRepository->findCategory($data['category'], $data['category_id'], $data['category_name']); - if (null !== $category) { - Log::debug(sprintf('Link category #%d to journal #%d', $category->id, $journal->id)); - $journal->categories()->sync([$category->id]); - } - } - /** * @param TransactionJournal $journal * @param NullArrayObject $transaction diff --git a/app/Factory/TransactionJournalMetaFactory.php b/app/Factory/TransactionJournalMetaFactory.php index 30948ef5fa..96d3030a61 100644 --- a/app/Factory/TransactionJournalMetaFactory.php +++ b/app/Factory/TransactionJournalMetaFactory.php @@ -53,10 +53,12 @@ class TransactionJournalMetaFactory */ public function updateOrCreate(array $data): ?TransactionJournalMeta { + Log::debug('In updateOrCreate()'); $value = $data['data']; /** @var TransactionJournalMeta $entry */ $entry = $data['journal']->transactionJournalMeta()->where('name', $data['name'])->first(); if (null === $value && null !== $entry) { + Log::debug('Value is empty, delete meta value.'); try { $entry->delete(); } catch (Exception $e) { // @codeCoverageIgnore @@ -67,11 +69,14 @@ class TransactionJournalMetaFactory } if ($data['data'] instanceof Carbon) { + Log::debug('Is a carbon object.'); $value = $data['data']->toW3cString(); } if ('' === (string)$value) { + Log::debug('Is an empty string.'); // don't store blank strings. if (null !== $entry) { + Log::debug('Will not store empty strings, delete meta value'); try { $entry->delete(); } catch (Exception $e) { // @codeCoverageIgnore @@ -83,11 +88,13 @@ class TransactionJournalMetaFactory } if (null === $entry) { + Log::debug('Will create new object.'); Log::debug(sprintf('Going to create new meta-data entry to store "%s".', $data['name'])); $entry = new TransactionJournalMeta(); $entry->transactionJournal()->associate($data['journal']); $entry->name = $data['name']; } + Log::debug('Will update value and return.'); $entry->data = $value; $entry->save(); diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index b215ef13e2..69e2cddcd0 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -44,7 +44,7 @@ class UpdatedGroupEventHandler public function processRules(UpdatedTransactionGroup $updatedJournalEvent): bool { // get all the user's rule groups, with the rules, order by 'order'. - $journals = $updatedJournalEvent->transactionGroup; + $journals = $updatedJournalEvent->transactionGroup->transactionJournals; /** @var RuleGroupRepositoryInterface $ruleGroupRepos */ $ruleGroupRepos = app(RuleGroupRepositoryInterface::class); diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index bb464dfba0..2486e2ab87 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -99,6 +99,7 @@ class GroupCollector implements GroupCollectorInterface 'transaction_types.type as transaction_type_type', 'transaction_journals.description', 'transaction_journals.date', + 'transaction_journals.order', # source info (always present) 'source.id as source_transaction_id', diff --git a/app/Http/Controllers/System/InstallController.php b/app/Http/Controllers/System/InstallController.php index ad5c0ad5d2..cc4ec6efd5 100644 --- a/app/Http/Controllers/System/InstallController.php +++ b/app/Http/Controllers/System/InstallController.php @@ -82,7 +82,7 @@ class InstallController extends Controller 'firefly-iii:migrate-to-groups' => [], 'firefly-iii:back-to-journals' => [], - // there are 12 verify commands. + // there are 13 verify commands. 'firefly-iii:fix-piggies' => [], 'firefly-iii:create-link-types' => [], 'firefly-iii:create-access-tokens' => [], @@ -95,8 +95,10 @@ class InstallController extends Controller 'firefly-iii:delete-empty-journals' => [], 'firefly-iii:delete-empty-groups' => [], 'firefly-iii:fix-account-types' => [], + 'firefly-iii:rename-meta-fields' => [], ]; } + /** * Show index. * diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index 82eb6e1fd7..bfa180d150 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -170,6 +170,9 @@ class Request extends FormRequest if (null === $string) { return null; } + if ('' === $string) { + return null; + } return (int)$string; } diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index bde4a81de8..93acd8b507 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -134,9 +134,8 @@ class TransactionJournal extends Model /** @var array Fields that can be filled */ protected $fillable - = ['user_id', 'transaction_type_id', 'bill_id', 'interest_date', 'book_date', 'process_date', - 'transaction_currency_id', 'description', 'completed', - 'date', 'rent_date', 'encrypted', 'tag_count',]; + = ['user_id', 'transaction_type_id', 'bill_id', 'tag_count','transaction_currency_id', 'description', 'completed', 'order', + 'date']; /** @var array Hidden from view */ protected $hidden = ['encrypted']; diff --git a/app/Providers/JournalServiceProvider.php b/app/Providers/JournalServiceProvider.php index 053d960c7a..9340ce2a96 100644 --- a/app/Providers/JournalServiceProvider.php +++ b/app/Providers/JournalServiceProvider.php @@ -97,8 +97,18 @@ class JournalServiceProvider extends ServiceProvider private function registerGroupRepository() { - // password verifier thing - $this->app->bind(TransactionGroupRepositoryInterface::class, TransactionGroupRepository::class); + $this->app->bind( + TransactionGroupRepositoryInterface::class, + function (Application $app) { + /** @var TransactionGroupRepositoryInterface $repository */ + $repository = app(TransactionGroupRepository::class); + if ($app->auth->check()) { + $repository->setUser(auth()->user()); + } + + return $repository; + } + ); } /** diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 1dcea670d2..7a32f925ce 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -220,25 +220,16 @@ class BudgetRepository implements BudgetRepositoryInterface } /** - * @param Budget|null $budget * @param int|null $budgetId * @param string|null $budgetName * * @return Budget|null */ - public function findBudget(?Budget $budget, ?int $budgetId, ?string $budgetName): ?Budget + public function findBudget(?int $budgetId, ?string $budgetName): ?Budget { Log::debug('Now in findBudget()'); - $result = null; - if (null !== $budget) { - Log::debug(sprintf('Parameters contain budget #%d, will return this.', $budget->id)); - $result = $budget; - } - - if (null === $result) { - Log::debug(sprintf('Searching for budget with ID #%d...', $budgetId)); - $result = $this->findNull((int)$budgetId); - } + Log::debug(sprintf('Searching for budget with ID #%d...', $budgetId)); + $result = $this->findNull((int)$budgetId); if (null === $result) { Log::debug(sprintf('Searching for budget with name %s...', $budgetName)); $result = $this->findByName((string)$budgetName); diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index 88c729cd19..e647c2d5c6 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -82,13 +82,12 @@ interface BudgetRepositoryInterface public function destroyBudgetLimit(BudgetLimit $budgetLimit): void; /** - * @param Budget|null $budget * @param int|null $budgetId * @param string|null $budgetName * * @return Budget|null */ - public function findBudget(?Budget $budget, ?int $budgetId, ?string $budgetName): ?Budget; + public function findBudget( ?int $budgetId, ?string $budgetName): ?Budget; /** * Find budget by name. diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index 0768781dd6..e06c8cd4ce 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -253,25 +253,16 @@ class CategoryRepository implements CategoryRepositoryInterface } /** - * @param Category|null $category - * @param int|null $categoryId - * @param string|null $categoryName + * @param int|null $categoryId + * @param string|null $categoryName * * @return Category|null */ - public function findCategory(?Category $category, ?int $categoryId, ?string $categoryName): ?Category + public function findCategory(?int $categoryId, ?string $categoryName): ?Category { Log::debug('Now in findCategory()'); - $result = null; - if (null !== $category) { - Log::debug(sprintf('Parameters contain category #%d, will return this.', $category->id)); - $result = $category; - } - - if (null === $result) { - Log::debug(sprintf('Searching for category with ID #%d...', $categoryId)); - $result = $this->findNull((int)$categoryId); - } + Log::debug(sprintf('Searching for category with ID #%d...', $categoryId)); + $result = $this->findNull((int)$categoryId); if (null === $result) { Log::debug(sprintf('Searching for category with name %s...', $categoryName)); $result = $this->findByName((string)$categoryName); diff --git a/app/Repositories/Category/CategoryRepositoryInterface.php b/app/Repositories/Category/CategoryRepositoryInterface.php index e6d4730d89..d7ce278258 100644 --- a/app/Repositories/Category/CategoryRepositoryInterface.php +++ b/app/Repositories/Category/CategoryRepositoryInterface.php @@ -34,13 +34,12 @@ interface CategoryRepositoryInterface { /** - * @param Category|null $category * @param int|null $categoryId * @param string|null $categoryName * * @return Category|null */ - public function findCategory(?Category $category, ?int $categoryId, ?string $categoryName): ?Category; + public function findCategory( ?int $categoryId, ?string $categoryName): ?Category; /** * @param Category $category diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index 296092e7ca..7358838328 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -265,16 +265,13 @@ class CurrencyRepository implements CurrencyRepositoryInterface */ public function findCurrency(?int $currencyId, ?string $currencyCode): TransactionCurrency { - Log::debug('Now in findCurrency()'); - $result = $this->find((int)$currencyId); - if (null === $result) { - Log::debug(sprintf('Searching for currency with code %s...', $currencyCode)); - $result = $this->findByCode((string)$currencyCode); - } + $result = $this->findCurrencyNull($currencyId, $currencyCode); + if (null === $result) { Log::debug('Grabbing default currency for this user...'); $result = app('amount')->getDefaultCurrencyByUser($this->user); } + if (null === $result) { Log::debug('Grabbing EUR as fallback.'); $result = $this->findByCode('EUR'); @@ -288,6 +285,30 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $result; } + /** + * Find by object, ID or code. Returns NULL if nothing found. + * + * @param int|null $currencyId + * @param string|null $currencyCode + * + * @return TransactionCurrency|null + */ + public function findCurrencyNull(?int $currencyId, ?string $currencyCode): ?TransactionCurrency + { + Log::debug('Now in findCurrencyNull()'); + $result = $this->find((int)$currencyId); + if (null === $result) { + Log::debug(sprintf('Searching for currency with code %s...', $currencyCode)); + $result = $this->findByCode((string)$currencyCode); + } + if (null !== $result && false === $result->enabled) { + Log::debug(sprintf('Also enabled currency %s', $result->code)); + $this->enable($result); + } + + return $result; + } + /** * Find by ID, return NULL if not found. * Used in Import Currency! diff --git a/app/Repositories/Currency/CurrencyRepositoryInterface.php b/app/Repositories/Currency/CurrencyRepositoryInterface.php index 1ceddc70c6..5122588827 100644 --- a/app/Repositories/Currency/CurrencyRepositoryInterface.php +++ b/app/Repositories/Currency/CurrencyRepositoryInterface.php @@ -135,13 +135,23 @@ interface CurrencyRepositoryInterface /** * Find by object, ID or code. Returns user default or system default. * - * @param int|null $currencyId - * @param string|null $currencyCode + * @param int|null $currencyId + * @param string|null $currencyCode * * @return TransactionCurrency|null */ public function findCurrency(?int $currencyId, ?string $currencyCode): TransactionCurrency; + /** + * Find by object, ID or code. Returns NULL if nothing found. + * + * @param int|null $currencyId + * @param string|null $currencyCode + * + * @return TransactionCurrency|null + */ + public function findCurrencyNull(?int $currencyId, ?string $currencyCode): ?TransactionCurrency; + /** * Find by ID, return NULL if not found. * diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 9f0b5de822..2be8a0ab26 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -774,40 +774,6 @@ class JournalRepository implements JournalRepositoryInterface $this->user = $user; } - /** - * @param array $data - * - * @return TransactionGroup - * - * @throws FireflyException - */ - public function store(array $data): TransactionGroup - { - /** @var TransactionGroupFactory $factory */ - $factory = app(TransactionGroupFactory::class); - $factory->setUser($this->user); - - return $factory->create($data); - } - - /** - * @param TransactionGroup $journal - * @param array $data - * - * @return TransactionGroup - * - * @throws FireflyException - * @throws FireflyException - */ - public function update(TransactionGroup $journal, array $data): TransactionGroup - { - /** @var JournalUpdateService $service */ - $service = app(JournalUpdateService::class); - $journal = $service->update($journal, $data); - - return $journal; - } - /** * Update budget for a journal. * diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index 05eeabfd2f..d643783927 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -319,21 +319,7 @@ interface JournalRepositoryInterface */ public function setUser(User $user); - /** - * @param array $data - * - * @throws FireflyException - * @return TransactionJournal - */ - public function store(array $data): TransactionGroup; - /** - * @param TransactionGroup $transactionGroup - * @param array $data - * - * @return TransactionGroup - */ - public function update(TransactionGroup $transactionGroup, array $data): TransactionGroup; /** * Update budget for a journal. diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepository.php b/app/Repositories/TransactionGroup/TransactionGroupRepository.php index 276dee800a..f046a7b130 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepository.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepository.php @@ -27,8 +27,12 @@ namespace FireflyIII\Repositories\TransactionGroup; use Carbon\Carbon; use DB; use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\TransactionGroupFactory; use FireflyIII\Models\Note; +use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Services\Internal\Update\GroupUpdateService; use FireflyIII\Support\NullArrayObject; /** @@ -36,6 +40,8 @@ use FireflyIII\Support\NullArrayObject; */ class TransactionGroupRepository implements TransactionGroupRepositoryInterface { + private $user; + /** * Constructor. */ @@ -61,6 +67,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface ::table('journal_meta') ->where('transaction_journal_id', $journalId) ->whereIn('name', $fields) + ->whereNull('deleted_at') ->get(['name', 'data']); $return = []; @@ -85,6 +92,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface ::table('journal_meta') ->where('transaction_journal_id', $journalId) ->whereIn('name', $fields) + ->whereNull('deleted_at') ->get(['name', 'data']); $return = []; @@ -133,4 +141,45 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface return $result->pluck('tag')->toArray(); } + + /** + * @param mixed $user + */ + public function setUser($user): void + { + $this->user = $user; + } + + /** + * @param array $data + * + * @return TransactionGroup + * + * @throws FireflyException + */ + public function store(array $data): TransactionGroup + { + /** @var TransactionGroupFactory $factory */ + $factory = app(TransactionGroupFactory::class); + $factory->setUser($this->user); + + return $factory->create($data); + } + + /** + * @param TransactionGroup $transactionGroup + * @param array $data + * + * @return TransactionGroup + * + * @throws FireflyException + */ + public function update(TransactionGroup $transactionGroup, array $data): TransactionGroup + { + /** @var GroupUpdateService $service */ + $service = app(GroupUpdateService::class); + $updatedGroup = $service->update($transactionGroup, $data); + + return $updatedGroup; + } } \ No newline at end of file diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php b/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php index 7b7489f19e..326fda4bb6 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php @@ -23,7 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Repositories\TransactionGroup; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\TransactionGroup; use FireflyIII\Support\NullArrayObject; +use FireflyIII\User; /** * Interface TransactionGroupRepositoryInterface @@ -67,4 +70,32 @@ interface TransactionGroupRepositoryInterface * @return array */ public function getTags(int $journalId): array; + + /** + * Set the user. + * + * @param User $user + */ + public function setUser(User $user): void; + + /** + * Create a new transaction group. + * + * @param array $data + * + * @return TransactionGroup + * @throws FireflyException + */ + public function store(array $data): TransactionGroup; + + /** + * Update an existing transaction group. + * + * @param TransactionGroup $transactionGroup + * @param array $data + * + * @return TransactionGroup + */ + public function update(TransactionGroup $transactionGroup, array $data): TransactionGroup; + } \ No newline at end of file diff --git a/app/Repositories/TransactionType/TransactionTypeRepository.php b/app/Repositories/TransactionType/TransactionTypeRepository.php index 41f64d6854..bb7cab684a 100644 --- a/app/Repositories/TransactionType/TransactionTypeRepository.php +++ b/app/Repositories/TransactionType/TransactionTypeRepository.php @@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\TransactionType; use FireflyIII\Models\TransactionType; use Log; + /** * Class TransactionTypeRepository */ @@ -57,7 +58,8 @@ class TransactionTypeRepository implements TransactionTypeRepositoryInterface return $type; } - $search = $this->findByType($typeString); + $typeString = $typeString ?? TransactionType::WITHDRAWAL; + $search = $this->findByType($typeString); if (null === $search) { $search = $this->findByType(TransactionType::WITHDRAWAL); } diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php index 0373ea19ce..d4a1b2ce2c 100644 --- a/app/Services/Internal/Support/JournalServiceTrait.php +++ b/app/Services/Internal/Support/JournalServiceTrait.php @@ -24,16 +24,17 @@ declare(strict_types=1); namespace FireflyIII\Services\Internal\Support; use Exception; -use FireflyIII\Factory\BillFactory; -use FireflyIII\Factory\BudgetFactory; -use FireflyIII\Factory\CategoryFactory; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TagFactory; -use FireflyIII\Factory\TransactionJournalMetaFactory; -use FireflyIII\Models\Bill; -use FireflyIII\Models\Budget; -use FireflyIII\Models\Category; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; use FireflyIII\Models\Note; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Support\NullArrayObject; use Log; /** @@ -42,26 +43,229 @@ use Log; */ trait JournalServiceTrait { + /** @var AccountRepositoryInterface */ + private $accountRepository; + /** @var BudgetRepositoryInterface */ + private $budgetRepository; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + /** @var TagFactory */ + private $tagFactory; + + + /** + * @param string|null $amount + * + * @return string + */ + protected function getForeignAmount(?string $amount): ?string + { + $result = null; + if (null === $amount) { + Log::debug('No foreign amount info in array. Return NULL'); + + return null; + } + if ('' === $amount) { + Log::debug('Foreign amount is empty string, return NULL.'); + + return null; + } + if (0 === bccomp('0', $amount)) { + Log::debug('Foreign amount is 0.0, return NULL.'); + + return null; + } + Log::debug(sprintf('Foreign amount is %s', $amount)); + + return $amount; + } + + /** + * @param string $transactionType + * @param string $direction + * @param int|null $accountId + * @param string|null $accountName + * + * @return Account + * @throws FireflyException + */ + protected function getAccount(string $transactionType, string $direction, ?int $accountId, ?string $accountName): Account + { + // some debug logging: + Log::debug(sprintf('Now in getAccount(%s, %d, %s)', $direction, $accountId, $accountName)); + + // final result: + $result = null; + + // expected type of source account, in order of preference + /** @var array $array */ + $array = config('firefly.expected_source_types'); + $expectedTypes = $array[$direction]; + unset($array); + + // and now try to find it, based on the type of transaction. + $message = 'Based on the fact that the transaction is a %s, the %s account should be in: %s'; + Log::debug(sprintf($message, $transactionType, $direction, implode(', ', $expectedTypes[$transactionType]))); + + // first attempt, find by ID. + if (null !== $accountId) { + $search = $this->accountRepository->findNull($accountId); + if (null !== $search && in_array($search->accountType->type, $expectedTypes[$transactionType], true)) { + Log::debug( + sprintf('Found "account_id" object for %s: #%d, "%s" of type %s', $direction, $search->id, $search->name, $search->accountType->type) + ); + $result = $search; + } + } + + // second attempt, find by name. + if (null === $result && null !== $accountName) { + Log::debug('Found nothing by account ID.'); + // find by preferred type. + $source = $this->accountRepository->findByName($accountName, [$expectedTypes[$transactionType][0]]); + // or any expected type. + $source = $source ?? $this->accountRepository->findByName($accountName, $expectedTypes[$transactionType]); + + if (null !== $source) { + Log::debug(sprintf('Found "account_name" object for %s: #%d, %s', $direction, $source->id, $source->name)); + + $result = $source; + } + } + + // return cash account. + if (null === $result && null === $accountName + && in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) { + $result = $this->accountRepository->getCashAccount(); + } + + // return new account. + if (null === $result) { + $accountName = $accountName ?? '(no name)'; + // final attempt, create it. + $preferredType = $expectedTypes[$transactionType][0]; + if (AccountType::ASSET === $preferredType) { + throw new FireflyException(sprintf('TransactionFactory: Cannot create asset account with ID #%d or name "%s".', $accountId, $accountName)); + } + + $result = $this->accountRepository->store( + [ + 'account_type_id' => null, + 'accountType' => $preferredType, + 'name' => $accountName, + 'active' => true, + 'iban' => null, + ] + ); + } + + return $result; + } + + /** + * @param string $amount + * + * @return string + * @throws FireflyException + */ + protected function getAmount(string $amount): string + { + if ('' === $amount) { + throw new FireflyException(sprintf('The amount cannot be an empty string: "%s"', $amount)); + } + if (0 === bccomp('0', $amount)) { + throw new FireflyException(sprintf('The amount seems to be zero: "%s"', $amount)); + } + + return $amount; + } + + /** + * @param TransactionJournal $journal + * @param NullArrayObject $data + */ + protected function storeBudget(TransactionJournal $journal, NullArrayObject $data): void + { + if (TransactionType::WITHDRAWAL !== $journal->transactionType->type) { + $journal->budgets()->sync([]); + + return; + } + $budget = $this->budgetRepository->findBudget($data['budget_id'], $data['budget_name']); + if (null !== $budget) { + Log::debug(sprintf('Link budget #%d to journal #%d', $budget->id, $journal->id)); + $journal->budgets()->sync([$budget->id]); + + return; + } + // if the budget is NULL, sync empty. + $journal->budgets()->sync([]); + } + + /** + * @param TransactionJournal $journal + * @param NullArrayObject $data + */ + protected function storeCategory(TransactionJournal $journal, NullArrayObject $data): void + { + $category = $this->categoryRepository->findCategory($data['category_id'], $data['category_name']); + if (null !== $category) { + Log::debug(sprintf('Link category #%d to journal #%d', $category->id, $journal->id)); + $journal->categories()->sync([$category->id]); + + return; + } + // if the category is NULL, sync empty. + $journal->categories()->sync([]); + } + + /** + * @param TransactionJournal $journal + * @param string $notes + */ + protected function storeNotes(TransactionJournal $journal, ?string $notes): void + { + $notes = (string)$notes; + $note = $journal->notes()->first(); + if ('' !== $notes) { + if (null === $note) { + $note = new Note; + $note->noteable()->associate($journal); + } + $note->text = $notes; + $note->save(); + Log::debug(sprintf('Stored notes for journal #%d', $journal->id)); + + return; + } + if ('' === $notes && null !== $note) { + // try to delete existing notes. + try { + $note->delete(); + } catch (Exception $e) { + Log::debug(sprintf('Could not delete journal notes: %s', $e->getMessage())); + } + } + } /** * Link tags to journal. * * @param TransactionJournal $journal - * @param array $data + * @param array $tags * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function connectTags(TransactionJournal $journal, array $data): void + protected function storeTags(TransactionJournal $journal, ?array $tags): void { - /** @var TagFactory $factory */ - $factory = app(TagFactory::class); - $factory->setUser($journal->user); + $this->tagFactory->setUser($journal->user); $set = []; - if (!\is_array($data['tags'])) { - return; // @codeCoverageIgnore + if (!is_array($tags)) { + return; } - foreach ($data['tags'] as $string) { + foreach ($tags as $string) { if ('' !== $string) { - $tag = $factory->findOrCreate($string); + $tag = $this->tagFactory->findOrCreate($string); if (null !== $tag) { $set[] = $tag->id; } @@ -71,117 +275,147 @@ trait JournalServiceTrait } - /** - * @param int|null $budgetId - * @param null|string $budgetName - * - * @return Budget|null - */ - protected function findBudget(?int $budgetId, ?string $budgetName): ?Budget - { - /** @var BudgetFactory $factory */ - $factory = app(BudgetFactory::class); - $factory->setUser($this->user); - return $factory->find($budgetId, $budgetName); - } - - /** - * @param int|null $categoryId - * @param null|string $categoryName - * - * @return Category|null - */ - protected function findCategory(?int $categoryId, ?string $categoryName): ?Category - { - Log::debug(sprintf('Going to find or create category #%d, with name "%s"', $categoryId, $categoryName)); - /** @var CategoryFactory $factory */ - $factory = app(CategoryFactory::class); - $factory->setUser($this->user); - - return $factory->findOrCreate($categoryId, $categoryName); - } - - - /** - * @param TransactionJournal $journal - * @param Budget|null $budget - */ - protected function setBudget(TransactionJournal $journal, ?Budget $budget): void - { - if (null === $budget) { - $journal->budgets()->sync([]); - - return; - } - $journal->budgets()->sync([$budget->id]); - - } - - - /** - * @param TransactionJournal $journal - * @param Category|null $category - */ - protected function setCategory(TransactionJournal $journal, ?Category $category): void - { - if (null === $category) { - $journal->categories()->sync([]); - - return; - } - $journal->categories()->sync([$category->id]); - - } - - - /** - * @param TransactionJournal $journal - * @param array $data - * @param string $field - */ - protected function storeMeta(TransactionJournal $journal, array $data, string $field): void - { - $set = [ - 'journal' => $journal, - 'name' => $field, - 'data' => (string)($data[$field] ?? ''), - ]; - - Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data'])); - - /** @var TransactionJournalMetaFactory $factory */ - $factory = app(TransactionJournalMetaFactory::class); - $factory->updateOrCreate($set); - } - - /** - * @param TransactionJournal $journal - * @param string $notes - */ - protected function storeNote(TransactionJournal $journal, ?string $notes): void - { - $notes = (string)$notes; - if ('' !== $notes) { - $note = $journal->notes()->first(); - if (null === $note) { - $note = new Note; - $note->noteable()->associate($journal); - } - $note->text = $notes; - $note->save(); - - return; - } - $note = $journal->notes()->first(); - if (null !== $note) { - try { - $note->delete(); - } catch (Exception $e) { - Log::debug(sprintf('Journal service trait could not delete note: %s', $e->getMessage())); - } - } - - - } + // + // /** + // * Link tags to journal. + // * + // * @param TransactionJournal $journal + // * @param array $data + // * @SuppressWarnings(PHPMD.CyclomaticComplexity) + // */ + // public function connectTags(TransactionJournal $journal, array $data): void + // { + // /** @var TagFactory $factory */ + // $factory = app(TagFactory::class); + // $factory->setUser($journal->user); + // $set = []; + // if (!\is_array($data['tags'])) { + // return; // @codeCoverageIgnore + // } + // foreach ($data['tags'] as $string) { + // if ('' !== $string) { + // $tag = $factory->findOrCreate($string); + // if (null !== $tag) { + // $set[] = $tag->id; + // } + // } + // } + // $journal->tags()->sync($set); + // } + // + // + // /** + // * @param int|null $budgetId + // * @param null|string $budgetName + // * + // * @return Budget|null + // */ + // protected function findBudget(?int $budgetId, ?string $budgetName): ?Budget + // { + // /** @var BudgetFactory $factory */ + // $factory = app(BudgetFactory::class); + // $factory->setUser($this->user); + // + // return $factory->find($budgetId, $budgetName); + // } + // + // /** + // * @param int|null $categoryId + // * @param null|string $categoryName + // * + // * @return Category|null + // */ + // protected function findCategory(?int $categoryId, ?string $categoryName): ?Category + // { + // Log::debug(sprintf('Going to find or create category #%d, with name "%s"', $categoryId, $categoryName)); + // /** @var CategoryFactory $factory */ + // $factory = app(CategoryFactory::class); + // $factory->setUser($this->user); + // + // return $factory->findOrCreate($categoryId, $categoryName); + // } + // + // + // /** + // * @param TransactionJournal $journal + // * @param Budget|null $budget + // */ + // protected function setBudget(TransactionJournal $journal, ?Budget $budget): void + // { + // if (null === $budget) { + // $journal->budgets()->sync([]); + // + // return; + // } + // $journal->budgets()->sync([$budget->id]); + // + // } + // + // + // /** + // * @param TransactionJournal $journal + // * @param Category|null $category + // */ + // protected function setCategory(TransactionJournal $journal, ?Category $category): void + // { + // if (null === $category) { + // $journal->categories()->sync([]); + // + // return; + // } + // $journal->categories()->sync([$category->id]); + // + // } + // + // + // /** + // * @param TransactionJournal $journal + // * @param array $data + // * @param string $field + // */ + // protected function storeMeta(TransactionJournal $journal, array $data, string $field): void + // { + // $set = [ + // 'journal' => $journal, + // 'name' => $field, + // 'data' => (string)($data[$field] ?? ''), + // ]; + // + // Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data'])); + // + // /** @var TransactionJournalMetaFactory $factory */ + // $factory = app(TransactionJournalMetaFactory::class); + // $factory->updateOrCreate($set); + // } + // + // /** + // * @param TransactionJournal $journal + // * @param string $notes + // */ + // protected function storeNote(TransactionJournal $journal, ?string $notes): void + // { + // $notes = (string)$notes; + // if ('' !== $notes) { + // $note = $journal->notes()->first(); + // if (null === $note) { + // $note = new Note; + // $note->noteable()->associate($journal); + // } + // $note->text = $notes; + // $note->save(); + // + // return; + // } + // $note = $journal->notes()->first(); + // if (null !== $note) { + // try { + // $note->delete(); + // } catch (Exception $e) { + // Log::debug(sprintf('Journal service trait could not delete note: %s', $e->getMessage())); + // } + // } + // + // + // } } diff --git a/app/Services/Internal/Update/GroupUpdateService.php b/app/Services/Internal/Update/GroupUpdateService.php new file mode 100644 index 0000000000..ae54bbe4fd --- /dev/null +++ b/app/Services/Internal/Update/GroupUpdateService.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Internal\Update; + +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use Log; + +/** + * Class GroupUpdateService + */ +class GroupUpdateService +{ + /** + * Update a transaction group. + * + * @param TransactionGroup $transactionGroup + * @param array $data + * + * @return TransactionGroup + */ + public function update(TransactionGroup $transactionGroup, array $data): TransactionGroup + { + Log::debug('Now in group update service'); + $transactions = $data['transactions'] ?? []; + // update group name. + if (array_key_exists('group_title', $data)) { + Log::debug(sprintf('Update transaction group #%d title.', $transactionGroup->id)); + $transactionGroup->title = $data['group_title']; + $transactionGroup->save(); + } + if (1 === count($transactions) && 1 === $transactionGroup->transactionJournals()->count()) { + /** @var TransactionJournal $first */ + $first = $transactionGroup->transactionJournals()->first(); + Log::debug(sprintf('Will now update journal #%d (only journal in group #%d)', $first->id, $transactionGroup->id)); + $this->updateTransactionJournal($transactionGroup, $first, reset($transactions)); + $transactionGroup->refresh(); + + return $transactionGroup; + } + die('cannot update split'); + + app('preferences')->mark(); + } + + /** + * Update single journal. + * + * @param TransactionGroup $transactionGroup + * @param TransactionJournal $journal + * @param array $data + */ + private function updateTransactionJournal(TransactionGroup $transactionGroup, TransactionJournal $journal, array $data): void + { + /** @var JournalUpdateService $updateService */ + $updateService = app(JournalUpdateService::class); + $updateService->setTransactionGroup($transactionGroup); + $updateService->setTransactionJournal($journal); + $updateService->setData($data); + $updateService->update(); + } + +} \ No newline at end of file diff --git a/app/Services/Internal/Update/JournalUpdateService.php b/app/Services/Internal/Update/JournalUpdateService.php index bdd17f41f7..48bebb8690 100644 --- a/app/Services/Internal/Update/JournalUpdateService.php +++ b/app/Services/Internal/Update/JournalUpdateService.php @@ -23,12 +23,25 @@ declare(strict_types=1); namespace FireflyIII\Services\Internal\Update; -use FireflyIII\Factory\TransactionFactory; +use Carbon\Carbon; +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\TagFactory; +use FireflyIII\Factory\TransactionJournalMetaFactory; +use FireflyIII\Factory\TransactionTypeFactory; +use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionGroup; 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; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Services\Internal\Support\JournalServiceTrait; -use Illuminate\Support\Collection; +use FireflyIII\Support\NullArrayObject; +use FireflyIII\Validation\AccountValidator; use Log; /** @@ -40,163 +53,618 @@ class JournalUpdateService { use JournalServiceTrait; + /** @var BillRepositoryInterface */ + private $billRepository; + /** @var CurrencyRepositoryInterface */ + private $currencyRepository; + /** @var array The data to update the journal with. */ + private $data; + /** @var Account The destination account. */ + private $destinationAccount; + /** @var Transaction */ + private $destinationTransaction; + /** @var array All meta values that are dates. */ + private $metaDate; + /** @var array All meta values that are strings. */ + private $metaString; + /** @var Account Source account of the journal */ + private $sourceAccount; + /** @var Transaction Source transaction of the journal. */ + private $sourceTransaction; + /** @var TransactionGroup The parent group. */ + private $transactionGroup; + /** @var TransactionJournal The journal to update. */ + private $transactionJournal; + /** @var Account If new account info is submitted, this array will hold the valid destination. */ + private $validDestination; + /** @var Account If new account info is submitted, this array will hold the valid source. */ + private $validSource; + /** - * Constructor. + * JournalUpdateService constructor. */ public function __construct() { - if ('testing' === config('app.env')) { - Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); + $this->billRepository = app(BillRepositoryInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->budgetRepository = app(BudgetRepositoryInterface::class); + $this->tagFactory = app(TagFactory::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->currencyRepository = app(CurrencyRepositoryInterface::class); + $this->metaString = ['sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_db', 'sepa_country', 'sepa_ep', 'sepa_ci', 'sepa_batch_id', 'recurrence_id', + 'internal_reference', 'bunq_payment_id', 'external_id',]; + $this->metaDate = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date',]; + } + + /** + * @param array $data + */ + public function setData(array $data): void + { + $this->data = $data; + } + + /** + * @param TransactionGroup $transactionGroup + */ + public function setTransactionGroup(TransactionGroup $transactionGroup): void + { + $this->transactionGroup = $transactionGroup; + $this->billRepository->setUser($transactionGroup->user); + $this->categoryRepository->setUser($transactionGroup->user); + $this->budgetRepository->setUser($transactionGroup->user); + $this->tagFactory->setUser($transactionGroup->user); + $this->accountRepository->setUser($transactionGroup->user); + } + + /** + * @param TransactionJournal $transactionJournal + */ + public function setTransactionJournal(TransactionJournal $transactionJournal): void + { + $this->transactionJournal = $transactionJournal; + } + + /** + * + */ + public function update(): void + { + Log::debug(sprintf('Now in JournalUpdateService for journal #%d.', $this->transactionJournal->id)); + + // can we update account data using the new type? + if ($this->hasValidAccounts()) { + Log::info('-- account info is valid, now update.'); + // update accounts: + $this->updateAccounts(); + + // then also update transaction journal type ID: + $this->updateType(); + $this->transactionJournal->refresh(); + } + + // find and update bill, if possible. + $this->updateBill(); + + // update journal fields. + $this->updateField('description'); + $this->updateField('date'); + $this->updateField('order'); + + $this->transactionJournal->save(); + $this->transactionJournal->refresh(); + + // update category + if ($this->hasFields(['category_id', 'category_name'])) { + Log::debug('Will update category.'); + + $this->storeCategory($this->transactionJournal, new NullArrayObject($this->data)); + } + // update budget + if ($this->hasFields(['budget_id', 'budget_name'])) { + Log::debug('Will update budget.'); + $this->storeBudget($this->transactionJournal, new NullArrayObject($this->data)); + } + // update tags + if ($this->hasFields(['tags'])) { + Log::debug('Will update tags.'); + $tags = $this->data['tags'] ?? null; + $this->storeTags($this->transactionJournal, $tags); + } + + // update notes. + if ($this->hasFields(['notes'])) { + $notes = '' === (string)$this->data['notes'] ? null : $this->data['notes']; + $this->storeNotes($this->transactionJournal, $notes); + } + // update meta fields. + // first string + if ($this->hasFields($this->metaString)) { + Log::debug('Meta string fields are present.'); + $this->updateMetaFields(); + } + + // then date fields. + if ($this->hasFields($this->metaDate)) { + Log::debug('Meta date fields are present.'); + $this->updateMetaDateFields(); + } + + + // update transactions. + if ($this->hasFields(['currency_id', 'currency_code'])) { + $this->updateCurrency(); + } + if ($this->hasFields(['amount'])) { + $this->updateAmount(); + } + + // amount, foreign currency. + if ($this->hasFields(['foreign_currency_id', 'foreign_currency_code', 'foreign_amount'])) { + $this->updateForeignAmount(); + } + + // TODO update hash + + app('preferences')->mark(); + + $this->transactionJournal->refresh(); + } + + /** + * Get destination transaction. + * + * @return Transaction + */ + private function getDestinationTransaction(): Transaction + { + if (null === $this->destinationTransaction) { + $this->destinationTransaction = $this->transactionJournal->transactions()->where('amount', '>', 0)->first(); + } + + return $this->destinationTransaction; + } + + /** + * This method returns the current or expected type of the journal (in case of a change) based on the data in the array. + * + * If the array contains key 'type' and the value is correct, this is returned. Otherwise, the original type is returned. + * + * @return string + */ + private function getExpectedType(): string + { + Log::debug('Now in getExpectedType()'); + if ($this->hasFields(['type'])) { + return ucfirst('opening-balance' === $this->data['type'] ? 'opening balance' : $this->data['type']); + } + + return $this->transactionJournal->transactionType->type; + } + + /** + * @return Account + */ + private function getOriginalDestinationAccount(): Account + { + if (null === $this->destinationAccount) { + $destination = $this->getSourceTransaction(); + $this->destinationAccount = $destination->account; + } + + return $this->destinationAccount; + } + + /** + * @return Account + */ + private function getOriginalSourceAccount(): Account + { + if (null === $this->sourceAccount) { + $source = $this->getSourceTransaction(); + $this->sourceAccount = $source->account; + } + + return $this->sourceAccount; + } + + /** + * @return Transaction + */ + private function getSourceTransaction(): Transaction + { + if (null === $this->sourceTransaction) { + $this->sourceTransaction = $this->transactionJournal->transactions()->with(['account'])->where('amount', '<', 0)->first(); + } + + return $this->sourceTransaction; + } + + /** + * Does a validation and returns the destination account. This method will break if the dest isn't really valid. + * + * @return Account + */ + private function getValidDestinationAccount(): Account + { + Log::debug('Now in getValidDestinationAccount().'); + + if (!$this->hasFields(['destination_id', 'destination_name'])) { + return $this->getOriginalDestinationAccount(); + } + + $destId = $this->data['destination_id'] ?? null; + $destName = $this->data['destination_name'] ?? null; + + // make new account validator. + $expectedType = $this->getExpectedType(); + Log::debug(sprintf('Expected type (new or unchanged) is %s', $expectedType)); + try { + $result = $this->getAccount($expectedType, 'destination', $destId, $destName); + } catch (FireflyException $e) { + Log::error(sprintf('getValidDestinationAccount() threw unexpected error: %s', $e->getMessage())); + $result = $this->getOriginalDestinationAccount(); + } + + return $result; + } + + /** + * Does a validation and returns the source account. This method will break if the source isn't really valid. + * + * @return Account + */ + private function getValidSourceAccount(): Account + { + Log::debug('Now in getValidSourceAccount().'); + $sourceId = $this->data['source_id'] ?? null; + $sourceName = $this->data['source_name'] ?? null; + + if (!$this->hasFields(['source_id', 'source_name'])) { + return $this->getOriginalSourceAccount(); + } + + $expectedType = $this->getExpectedType(); + try { + $result = $this->getAccount($expectedType, 'source', $sourceId, $sourceName); + } catch (FireflyException $e) { + Log::error(sprintf('Cant get the valid source account: %s', $e->getMessage())); + + $result = $this->getOriginalSourceAccount(); + } + + Log::debug(sprintf('getValidSourceAccount() will return #%d ("%s")', $result->id, $result->name)); + + return $result; + } + + /** + * @param array $fields + * + * @return bool + */ + private function hasFields(array $fields): bool + { + foreach ($fields as $field) { + if (array_key_exists($field, $this->data)) { + return true; + } + } + + return false; + } + + /** + * @return bool + */ + private function hasValidAccounts(): bool + { + return $this->hasValidSourceAccount() && $this->hasValidDestinationAccount(); + } + + /** + * @return bool + */ + private function hasValidDestinationAccount(): bool + { + Log::debug('Now in hasValidDestinationAccount().'); + $destId = $this->data['destination_id'] ?? null; + $destName = $this->data['destination_name'] ?? null; + + if (!$this->hasFields(['destination_id', 'destination_name'])) { + $destination = $this->getOriginalDestinationAccount(); + $destId = $destination->id; + $destName = $destination->name; + } + + // make new account validator. + $expectedType = $this->getExpectedType(); + Log::debug(sprintf('Expected type (new or unchanged) is %s', $expectedType)); + + // make a new validator. + /** @var AccountValidator $validator */ + $validator = app(AccountValidator::class); + $validator->setTransactionType($expectedType); + $validator->setUser($this->transactionJournal->user); + $validator->source = $this->getValidSourceAccount(); + + + $result = $validator->validateDestination($destId, $destName); + Log::debug(sprintf('hasValidDestinationAccount(%d, "%s") will return %s', $destId, $destName, var_export($result, true))); + + // validate submitted info: + return $result; + } + + /** + * @return bool + */ + private function hasValidSourceAccount(): bool + { + Log::debug('Now in hasValidSourceAccount().'); + $sourceId = $this->data['source_id'] ?? null; + $sourceName = $this->data['source_name'] ?? null; + + if (!$this->hasFields(['source_id', 'source_name'])) { + $sourceAccount = $this->getOriginalSourceAccount(); + $sourceId = $sourceAccount->id; + $sourceName = $sourceAccount->name; + } + + // make new account validator. + $expectedType = $this->getExpectedType(); + Log::debug(sprintf('Expected type (new or unchanged) is %s', $expectedType)); + + // make a new validator. + /** @var AccountValidator $validator */ + $validator = app(AccountValidator::class); + $validator->setTransactionType($expectedType); + $validator->setUser($this->transactionJournal->user); + + $result = $validator->validateSource($sourceId, $sourceName); + Log::debug(sprintf('hasValidSourceAccount(%d, "%s") will return %s', $sourceId, $sourceName, var_export($result, true))); + + // validate submitted info: + return $result; + } + + /** + * Will update the source and destination accounts of this journal. Assumes they are valid. + */ + private function updateAccounts(): void + { + $source = $this->getValidSourceAccount(); + $destination = $this->getValidDestinationAccount(); + + // cowardly refuse to update if both accounts are the same. + if ($source->id === $destination->id) { + Log::error(sprintf('Source + dest accounts are equal (%d, "%s")', $source->id, $source->name)); + + return; + } + + $sourceTransaction = $this->getSourceTransaction(); + $sourceTransaction->account()->associate($source); + $sourceTransaction->save(); + + $destinationTransaction = $this->getDestinationTransaction(); + $destinationTransaction->account()->associate($destination); + $destinationTransaction->save(); + + Log::debug(sprintf('Will set source to #%d ("%s")', $source->id, $source->name)); + Log::debug(sprintf('Will set dest to #%d ("%s")', $destination->id, $destination->name)); + } + + /** + * + */ + private function updateAmount(): void + { + $value = $this->data['amount'] ?? ''; + try { + $amount = $this->getAmount($value); + } catch (FireflyException $e) { + Log::debug(sprintf('getAmount("%s") returns error: %s', $value, $e->getMessage())); + + return; + } + Log::debug(sprintf('Updated amount to %s', $amount)); + $sourceTransaction = $this->getSourceTransaction(); + $sourceTransaction->amount = app('steam')->negative($value); + $sourceTransaction->save(); + + $destinationTransaction = $this->getDestinationTransaction(); + $destinationTransaction->amount = app('steam')->positive($value); + $destinationTransaction->save(); + } + + /** + * Update journal bill information. + */ + private function updateBill(): void + { + $type = $this->transactionJournal->transactionType->type; + if (( + array_key_exists('bill_id', $this->data) + || array_key_exists('bill_name', $this->data) + ) + && TransactionType::WITHDRAWAL === $type + ) { + $billId = (int)($this->data['bill_id'] ?? 0); + $billName = (string)($this->data['bill_name'] ?? ''); + $bill = $this->billRepository->findBill($billId, $billName); + $this->transactionJournal->bill_id = null === $bill ? null : $bill->id; + Log::debug('Updated bill ID'); + } + } + + /** + * + */ + private function updateCurrency(): void + { + $currencyId = $this->data['currency_id'] ?? null; + $currencyCode = $this->data['currency_code'] ?? null; + $currency = $this->currencyRepository->findCurrency($currencyId, $currencyCode); + if (null !== $currency) { + // update currency everywhere. + $this->transactionJournal->transaction_currency_id = $currency->id; + $this->transactionJournal->save(); + + $source = $this->getSourceTransaction(); + $source->transaction_currency_id = $currency->id; + $source->save(); + + $dest = $this->getDestinationTransaction(); + $dest->transaction_currency_id = $currency->id; + $dest->save(); + Log::debug(sprintf('Updated currency to #%d (%s)', $currency->id, $currency->code)); + } + } + + /** + * Update journal generic field. Cannot be set to NULL. + * + * @param $fieldName + */ + private function updateField($fieldName): void + { + if (array_key_exists($fieldName, $this->data) && '' !== (string)$this->data[$fieldName]) { + $this->transactionJournal->$fieldName = $this->data[$fieldName]; + Log::debug(sprintf('Updated %s', $fieldName)); } } /** - * @param TransactionJournal $journal - * @param array $data * - * @return TransactionJournal - * @throws \FireflyIII\Exceptions\FireflyException - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function update(TransactionJournal $journal, array $data): TransactionJournal + private function updateForeignAmount(): void { - // update journal: - $journal->description = $data['description']; - $journal->date = $data['date']; - $journal->save(); + $amount = $this->data['foreign_amount'] ?? null; + $foreignAmount = $this->getForeignAmount($amount); + $source = $this->getSourceTransaction(); + $dest = $this->getDestinationTransaction(); + $foreignCurrency = $source->foreignCurrency; - // update transactions: - /** @var TransactionUpdateService $service */ - $service = app(TransactionUpdateService::class); - $service->setUser($journal->user); + // find currency in data array + $newForeignId = $this->data['foreign_currency_id'] ?? null; + $newForeignCode = $this->data['foreign_currency_code'] ?? null; + $foreignCurrency = $this->currencyRepository->findCurrencyNull($newForeignId, $newForeignCode) ?? $foreignCurrency; - // create transactions: - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($journal->user); + // not the same as normal currency + if (null !== $foreignCurrency && $foreignCurrency->id === $this->transactionJournal->transaction_currency_id) { + Log::error(sprintf('Foreign currency is equal to normal currency (%s)', $foreignCurrency->code)); - Log::debug(sprintf('Found %d rows in array (should result in %d transactions', \count($data['transactions']), \count($data['transactions']) * 2)); - - /** - * @var int $identifier - * @var array $trData - */ - foreach ($data['transactions'] as $identifier => $trData) { - // exists transaction(s) with this identifier? update! - /** @var Collection $existing */ - $existing = $journal->transactions()->where('identifier', $identifier)->get(); - Log::debug(sprintf('Found %d transactions with identifier %d', $existing->count(), $identifier)); - if ($existing->count() > 0) { - $existing->each( - function (Transaction $transaction) use ($service, $trData) { - Log::debug(sprintf('Update transaction #%d (identifier %d)', $transaction->id, $trData['identifier'])); - $service->update($transaction, $trData); - } - ); - continue; - } - Log::debug('Found none, so create a pair.'); - // otherwise, create! - $factory->createPair($journal, $trData); + return; } - // could be that journal has more transactions than submitted (remove split) - $transactions = $journal->transactions()->where('amount', '>', 0)->get(); - Log::debug(sprintf('Journal #%d has %d transactions', $journal->id, $transactions->count())); - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - Log::debug(sprintf('Now at transaction %d with identifier %d', $transaction->id, $transaction->identifier)); - if (!isset($data['transactions'][$transaction->identifier])) { - Log::debug('No such entry in array, delete this set of transactions.'); - $journal->transactions()->where('identifier', $transaction->identifier)->delete(); - } + + // add foreign currency info to source and destination if possible. + if (null !== $foreignCurrency && null !== $foreignAmount) { + $source->foreign_currency_id = $foreignCurrency->id; + $source->foreign_amount = app('steam')->negative($foreignAmount); + $source->save(); + + + $dest->foreign_currency_id = $foreignCurrency->id; + $dest->foreign_amount = app('steam')->positive($foreignAmount); + $dest->save(); + + Log::debug(sprintf('Update foreign info to %s (#%d) %s', $foreignCurrency->code, $foreignCurrency->id, $foreignAmount)); + + return; } - Log::debug(sprintf('New count is %d, transactions array held %d items', $journal->transactions()->count(), \count($data['transactions']))); + if ('0' === $amount) { + $source->foreign_currency_id = null; + $source->foreign_amount = null; + $source->save(); - // connect bill: - $this->connectBill($journal, $data); - - // connect tags: - $this->connectTags($journal, $data); - - // remove category from journal: - $journal->categories()->sync([]); - - // remove budgets from journal: - $journal->budgets()->sync([]); - - // update or create custom fields: - // store date meta fields (if present): - $this->storeMeta($journal, $data, 'interest_date'); - $this->storeMeta($journal, $data, 'book_date'); - $this->storeMeta($journal, $data, 'process_date'); - $this->storeMeta($journal, $data, 'due_date'); - $this->storeMeta($journal, $data, 'payment_date'); - $this->storeMeta($journal, $data, 'invoice_date'); - $this->storeMeta($journal, $data, 'internal_reference'); - - // store note: - $this->storeNote($journal, $data['notes']); - - - return $journal; + $dest->foreign_currency_id = null; + $dest->foreign_amount = null; + $dest->save(); + Log::debug(sprintf('Foreign amount is "%s" so remove foreign amount info.', $amount)); + } + Log::info('Not enough info to update foreign currency info.'); } /** - * Update budget for a journal. * - * @param TransactionJournal $journal - * @param int $budgetId - * - * @return TransactionJournal */ - public function updateBudget(TransactionJournal $journal, int $budgetId): TransactionJournal + private function updateMetaDateFields(): void { - /** @var TransactionUpdateService $service */ - $service = app(TransactionUpdateService::class); - $service->setUser($journal->user); - if (TransactionType::WITHDRAWAL === $journal->transactionType->type) { - /** @var Transaction $transaction */ - foreach ($journal->transactions as $transaction) { - $service->updateBudget($transaction, $budgetId); + /** @var TransactionJournalMetaFactory $factory */ + $factory = app(TransactionJournalMetaFactory::class); + + foreach ($this->metaDate as $field) { + if ($this->hasFields([$field])) { + try { + $value = '' === $this->data[$field] ? null : new Carbon($this->data[$field]); + } catch (Exception $e) { + Log::debug(sprintf('%s is not a valid date value: %s', $this->data[$field], $e->getMessage())); + + return; + } + Log::debug(sprintf('Field "%s" is present ("%s"), try to update it.', $field, $value)); + $set = [ + 'journal' => $this->transactionJournal, + 'name' => $field, + 'data' => $value, + ]; + $factory->updateOrCreate($set); } - - return $journal; } - // clear budget. - /** @var Transaction $transaction */ - foreach ($journal->transactions as $transaction) { - $transaction->budgets()->sync([]); - } - // remove budgets from journal: - $journal->budgets()->sync([]); - - return $journal; } /** - * Update category for a journal. * - * @param TransactionJournal $journal - * @param string $category - * - * @return TransactionJournal */ - public function updateCategory(TransactionJournal $journal, string $category): TransactionJournal + private function updateMetaFields(): void { - /** @var TransactionUpdateService $service */ - $service = app(TransactionUpdateService::class); - $service->setUser($journal->user); + /** @var TransactionJournalMetaFactory $factory */ + $factory = app(TransactionJournalMetaFactory::class); - /** @var Transaction $transaction */ - foreach ($journal->transactions as $transaction) { - $service->updateCategory($transaction, $category); + foreach ($this->metaString as $field) { + if ($this->hasFields([$field])) { + $value = '' === $this->data[$field] ? null : $this->data[$field]; + Log::debug(sprintf('Field "%s" is present ("%s"), try to update it.', $field, $value)); + $set = [ + 'journal' => $this->transactionJournal, + 'name' => $field, + 'data' => $value, + ]; + $factory->updateOrCreate($set); + } } - // make journal empty: - $journal->categories()->sync([]); - - return $journal; } + /** + * Updates journal transaction type. + */ + private function updateType(): void + { + Log::debug('Now in updateType()'); + if ($this->hasFields(['type'])) { + $type = 'opening-balance' === $this->data['type'] ? 'opening balance' : $this->data['type']; + Log::debug( + sprintf( + 'Trying to change journal #%d from a %s to a %s.', + $this->transactionJournal->id, $this->transactionJournal->transactionType->type, $type + ) + ); + + /** @var TransactionTypeFactory $typeFactory */ + $typeFactory = app(TransactionTypeFactory::class); + $result = $typeFactory->find($this->data['type']); + if (null !== $result) { + Log::debug('Changed transaction type!'); + $this->transactionJournal->transaction_type_id = $result->id; + $this->transactionJournal->save(); + + return; + } + + return; + } + Log::debug('No type field present.'); + } } diff --git a/app/Support/Binder/TransactionGroup.php b/app/Support/Binder/TransactionGroup.php new file mode 100644 index 0000000000..abe4fe05e0 --- /dev/null +++ b/app/Support/Binder/TransactionGroup.php @@ -0,0 +1,53 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Binder; + +use FireflyIII\Models\TransactionJournal; +use Illuminate\Routing\Route; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Class TransactionGroup. + */ +class TransactionGroup implements BinderInterface +{ + /** + * @param string $value + * @param Route $route + * + * @return TransactionGroup + * @throws NotFoundHttpException + */ + public static function routeBinder(string $value, Route $route): TransactionGroup + { + if (auth()->check()) { + $group = auth()->user()->transactionGroups() + ->find($value); + if (null !== $group) { + return $group; + } + } + + throw new NotFoundHttpException; + } +} diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php index 7d50d2eb47..15a24b5103 100644 --- a/app/Support/Import/Placeholder/ImportTransaction.php +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -195,8 +195,8 @@ class ImportTransaction return; } - $modifiers = ['rabo-debit-credit', 'ing-debit-credit']; - if (\in_array($role, $modifiers, true)) { + $modifiers = ['generic-debit-credit']; + if (in_array($role, $modifiers, true)) { $this->modifiers[$role] = $columnValue->getValue(); return; diff --git a/app/Transformers/TransactionGroupTransformer.php b/app/Transformers/TransactionGroupTransformer.php index 5599772765..1ae45535af 100644 --- a/app/Transformers/TransactionGroupTransformer.php +++ b/app/Transformers/TransactionGroupTransformer.php @@ -119,6 +119,7 @@ class TransactionGroupTransformer extends AbstractTransformer 'transaction_journal_id' => $row['transaction_journal_id'], 'type' => strtolower($type), 'date' => $row['date']->toAtomString(), + 'order' => $row['order'], 'currency_id' => $row['currency_id'], 'currency_code' => $row['currency_code'], @@ -163,7 +164,7 @@ class TransactionGroupTransformer extends AbstractTransformer 'original_source' => $metaFieldData['original_source'], 'recurrence_id' => $metaFieldData['recurrence_id'], 'bunq_payment_id' => $metaFieldData['bunq_payment_id'], - 'import_hash_v2' => $metaFieldData['import_hash_v2'], + 'import_hash_v2' => $metaFieldData['import_hash_v2'], 'sepa_cc' => $metaFieldData['sepa_cc'], 'sepa_ct_op' => $metaFieldData['sepa_ct_op'], diff --git a/app/Validation/AccountValidator.php b/app/Validation/AccountValidator.php index a0834994be..2ec1d95ab6 100644 --- a/app/Validation/AccountValidator.php +++ b/app/Validation/AccountValidator.php @@ -27,6 +27,7 @@ use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\User; use Log; /** @@ -50,6 +51,8 @@ class AccountValidator private $combinations; /** @var string */ private $transactionType; + /** @var User */ + private $user; /** * AccountValidator constructor. @@ -69,9 +72,19 @@ class AccountValidator */ public function setTransactionType(string $transactionType): void { + Log::debug(sprintf('Transaction type for validator is now %s', ucfirst($transactionType))); $this->transactionType = ucfirst($transactionType); } + /** + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + $this->accountRepository->setUser($user); + } + /** * @param int|null $destinationId * @param $destinationName @@ -83,7 +96,7 @@ class AccountValidator Log::debug(sprintf('Now in AccountValidator::validateDestination(%d, "%s")', $destinationId, $destinationName)); if (null === $this->source) { - Log::error('Source is NULL'); + Log::error('Source is NULL, always FALSE.'); $this->destError = 'No source account validation has taken place yet. Please do this first or overrule the object.'; return false; @@ -121,9 +134,10 @@ class AccountValidator */ public function validateSource(?int $accountId, ?string $accountName): bool { + Log::debug(sprintf('Now in AccountValidator::validateSource(%d, "%s")', $accountId, $accountName)); switch ($this->transactionType) { default: - $result = false; + $result = false; $this->sourceError = sprintf('Cannot handle type "%s"', $this->transactionType); Log::error(sprintf('AccountValidator::validateSource cannot handle "%s", so it will always return false.', $this->transactionType)); break; @@ -241,7 +255,6 @@ class AccountValidator // if the account can be created anyway we don't need to search. if (null === $result && true === $this->canCreateTypes($validTypes)) { Log::debug('Can create some of these types, so return true.'); - $this->createDestinationAccount($accountName); $result = true; } @@ -249,15 +262,18 @@ class AccountValidator // otherwise try to find the account: $search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName); if (null === $search) { + Log::debug('findExistingAccount() returned NULL, so the result is false.'); $this->destError = (string)trans('validation.deposit_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); $result = false; } if (null !== $search) { + Log::debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name)); $this->destination = $search; $result = true; } } $result = $result ?? false; + Log::debug(sprintf('validateDepositDestination(%d, "%s") will return %s', $accountId, $accountName, var_export($result, true))); return $result; } @@ -270,6 +286,7 @@ class AccountValidator */ private function validateDepositSource(?int $accountId, ?string $accountName): bool { + Log::debug(sprintf('Now in validateDepositSource(%d, "%s")', $accountId, $accountName)); $result = null; // source can be any of the following types. $validTypes = array_keys($this->combinations[$this->transactionType]); @@ -281,10 +298,25 @@ class AccountValidator $result = false; } + // if the user submits an ID only but that ID is not of the correct type, + // return false. + if (null !== $accountId && null === $accountName) { + $search = $this->accountRepository->findNull($accountId); + if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) { + Log::debug(sprintf('User submitted only an ID (#%d), which is a "%s", so this is not a valid source.', $accountId, $search->accountType->type)); + $result = false; + } + } + // if the account can be created anyway we don't need to search. if (null === $result && true === $this->canCreateTypes($validTypes)) { - // set the source to be a (dummy) revenue account. $result = true; + + // set the source to be a (dummy) revenue account. + $account = new Account; + $accountType = AccountType::whereType(AccountType::REVENUE)->first(); + $account->accountType = $accountType; + $this->source = $account; } $result = $result ?? false; @@ -332,6 +364,7 @@ class AccountValidator */ private function validateTransferSource(?int $accountId, ?string $accountName): bool { + Log::debug(sprintf('Now in validateTransferSource(%d, "%s")', $accountId, $accountName)); // source can be any of the following types. $validTypes = array_keys($this->combinations[$this->transactionType]); if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) { @@ -362,6 +395,7 @@ class AccountValidator */ private function validateWithdrawalDestination(?int $accountId, ?string $accountName): bool { + Log::debug(sprintf('Now in validateWithdrawalDestination(%d, "%s")', $accountId, $accountName)); // source can be any of the following types. $validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? []; if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) { @@ -377,6 +411,7 @@ class AccountValidator return true; } + // don't expect to end up here: return false; } @@ -389,6 +424,7 @@ class AccountValidator */ private function validateWithdrawalSource(?int $accountId, ?string $accountName): bool { + Log::debug(sprintf('Now in validateWithdrawalSource(%d, "%s")', $accountId, $accountName)); // source can be any of the following types. $validTypes = array_keys($this->combinations[$this->transactionType]); if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) { diff --git a/app/Validation/TransactionValidation.php b/app/Validation/TransactionValidation.php index c122b5ff48..de9b344ca7 100644 --- a/app/Validation/TransactionValidation.php +++ b/app/Validation/TransactionValidation.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Validation; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionGroup; use Illuminate\Validation\Validator; /** @@ -31,6 +32,16 @@ use Illuminate\Validation\Validator; */ trait TransactionValidation { + /** + * If type is set, source + destination info is mandatory. + * + * @param Validator $validator + */ + protected function validateAccountPresence(Validator $validator): void + { + // TODO + } + /** * Validates the given account information. Switches on given transaction type. @@ -98,7 +109,9 @@ trait TransactionValidation // no valid descriptions? if (0 === $validDescriptions) { - $validator->errors()->add('description', (string)trans('validation.filled', ['attribute' => (string)trans('validation.attributes.description')])); + $validator->errors()->add( + 'transactions.0.description', (string)trans('validation.filled', ['attribute' => (string)trans('validation.attributes.description')]) + ); } } @@ -148,7 +161,7 @@ trait TransactionValidation $transactions = $data['transactions'] ?? []; // need at least one transaction if (0 === \count($transactions)) { - $validator->errors()->add('description', (string)trans('validation.at_least_one_transaction')); + $validator->errors()->add('transactions.0.description', (string)trans('validation.at_least_one_transaction')); } } @@ -233,6 +246,47 @@ trait TransactionValidation } } + /** + * All types of splits must be equal. + * + * @param Validator $validator + */ + public function validateTransactionTypesForUpdate(Validator $validator): void + { + $data = $validator->getData(); + $transactions = $data['transactions'] ?? []; + $types = []; + foreach ($transactions as $index => $transaction) { + $types[] = $transaction['type'] ?? 'invalid'; + } + $unique = array_unique($types); + if (count($unique) > 1) { + $validator->errors()->add('transactions.0.type', (string)trans('validation.transaction_types_equal')); + + return; + } + } + + /** + * @param Validator $validator + * @param TransactionGroup $transactionGroup + */ + private function validateJournalIds(Validator $validator, TransactionGroup $transactionGroup): void + { + $data = $validator->getData(); + $transactions = $data['transactions'] ?? []; + if (count($transactions) < 2) { + return; + } + foreach ($transactions as $index => $transaction) { + $journalId = (int)($transaction['transaction_journal_id'] ?? 0); + $count = $transactionGroup->transactionJournals()->where('id', $journalId)->count(); + if (0 === $journalId || 0 === $count) { + $validator->errors()->add(sprintf('transactions.%d.source_name', $index), (string)trans('validation.need_id_in_edit')); + } + } + } + // /** // * Throws an error when this asset account is invalid. // * diff --git a/config/firefly.php b/config/firefly.php index 3589bbd495..1980ca7b82 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -24,10 +24,44 @@ declare(strict_types=1); use FireflyIII\Export\Exporter\CsvExporter; +use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Attachment; +use FireflyIII\Models\AvailableBudget; +use FireflyIII\Models\Bill; +use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\Category; +use FireflyIII\Models\ExportJob; +use FireflyIII\Models\ImportJob; +use FireflyIII\Models\LinkType; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\Preference; +use FireflyIII\Models\Recurrence; +use FireflyIII\Models\Rule; +use FireflyIII\Models\RuleGroup; +use FireflyIII\Models\Tag; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionJournalLink; use FireflyIII\Models\TransactionType as TransactionTypeModel; use FireflyIII\Services\Currency\FixerIOv2; use FireflyIII\Services\Currency\RatesApiIOv1; +use FireflyIII\Support\Binder\AccountList; +use FireflyIII\Support\Binder\BudgetList; +use FireflyIII\Support\Binder\CategoryList; +use FireflyIII\Support\Binder\CLIToken; +use FireflyIII\Support\Binder\ConfigurationName; +use FireflyIII\Support\Binder\CurrencyCode; +use FireflyIII\Support\Binder\Date; +use FireflyIII\Support\Binder\ImportProvider; +use FireflyIII\Support\Binder\JournalList; +use FireflyIII\Support\Binder\SimpleJournalList; +use FireflyIII\Support\Binder\TagList; +use FireflyIII\Support\Binder\TagOrId; +use FireflyIII\Support\Binder\UnfinishedJournal; use FireflyIII\TransactionRules\Actions\AddTag; use FireflyIII\TransactionRules\Actions\AppendDescription; use FireflyIII\TransactionRules\Actions\AppendNotes; @@ -82,6 +116,7 @@ use FireflyIII\TransactionRules\Triggers\ToAccountIs; use FireflyIII\TransactionRules\Triggers\ToAccountStarts; use FireflyIII\TransactionRules\Triggers\TransactionType; use FireflyIII\TransactionRules\Triggers\UserAction; +use FireflyIII\User; /* * DO NOT EDIT THIS FILE. IT IS AUTO GENERATED. @@ -320,54 +355,55 @@ return [ ], 'bindables' => [ // models - 'account' => \FireflyIII\Models\Account::class, - 'attachment' => \FireflyIII\Models\Attachment::class, - 'availableBudget' => \FireflyIII\Models\AvailableBudget::class, - 'bill' => \FireflyIII\Models\Bill::class, - 'budget' => \FireflyIII\Models\Budget::class, - 'budgetLimit' => \FireflyIII\Models\BudgetLimit::class, - 'category' => \FireflyIII\Models\Category::class, - 'linkType' => \FireflyIII\Models\LinkType::class, - 'transactionType' => \FireflyIII\Models\TransactionType::class, - 'journalLink' => \FireflyIII\Models\TransactionJournalLink::class, - 'currency' => \FireflyIII\Models\TransactionCurrency::class, - 'piggyBank' => \FireflyIII\Models\PiggyBank::class, - 'preference' => \FireflyIII\Models\Preference::class, - 'tj' => \FireflyIII\Models\TransactionJournal::class, - 'tag' => \FireflyIII\Models\Tag::class, - 'recurrence' => \FireflyIII\Models\Recurrence::class, - 'rule' => \FireflyIII\Models\Rule::class, - 'ruleGroup' => \FireflyIII\Models\RuleGroup::class, - 'exportJob' => \FireflyIII\Models\ExportJob::class, - 'importJob' => \FireflyIII\Models\ImportJob::class, - 'transaction' => \FireflyIII\Models\Transaction::class, - 'user' => \FireflyIII\User::class, + 'account' => Account::class, + 'attachment' => Attachment::class, + 'availableBudget' => AvailableBudget::class, + 'bill' => Bill::class, + 'budget' => Budget::class, + 'budgetLimit' => BudgetLimit::class, + 'category' => Category::class, + 'linkType' => LinkType::class, + 'transactionType' => TransactionTypeModel::class, + 'journalLink' => TransactionJournalLink::class, + 'currency' => TransactionCurrency::class, + 'piggyBank' => PiggyBank::class, + 'preference' => Preference::class, + 'tj' => TransactionJournal::class, + 'tag' => Tag::class, + 'recurrence' => Recurrence::class, + 'rule' => Rule::class, + 'ruleGroup' => RuleGroup::class, + 'exportJob' => ExportJob::class, + 'importJob' => ImportJob::class, + 'transaction' => Transaction::class, + 'transactionGroup' => TransactionGroup::class, + 'user' => User::class, // strings - 'import_provider' => \FireflyIII\Support\Binder\ImportProvider::class, - 'currency_code' => \FireflyIII\Support\Binder\CurrencyCode::class, + 'import_provider' => ImportProvider::class, + 'currency_code' => CurrencyCode::class, // dates - 'start_date' => \FireflyIII\Support\Binder\Date::class, - 'end_date' => \FireflyIII\Support\Binder\Date::class, - 'date' => \FireflyIII\Support\Binder\Date::class, + 'start_date' => Date::class, + 'end_date' => Date::class, + 'date' => Date::class, // lists - 'accountList' => \FireflyIII\Support\Binder\AccountList::class, - 'expenseList' => \FireflyIII\Support\Binder\AccountList::class, - 'budgetList' => \FireflyIII\Support\Binder\BudgetList::class, - 'journalList' => \FireflyIII\Support\Binder\JournalList::class, - 'categoryList' => \FireflyIII\Support\Binder\CategoryList::class, - 'tagList' => \FireflyIII\Support\Binder\TagList::class, - 'simpleJournalList' => \FireflyIII\Support\Binder\SimpleJournalList::class, + 'accountList' => AccountList::class, + 'expenseList' => AccountList::class, + 'budgetList' => BudgetList::class, + 'journalList' => JournalList::class, + 'categoryList' => CategoryList::class, + 'tagList' => TagList::class, + 'simpleJournalList' => SimpleJournalList::class, // others - 'fromCurrencyCode' => \FireflyIII\Support\Binder\CurrencyCode::class, - 'toCurrencyCode' => \FireflyIII\Support\Binder\CurrencyCode::class, - 'unfinishedJournal' => \FireflyIII\Support\Binder\UnfinishedJournal::class, - 'cliToken' => \FireflyIII\Support\Binder\CLIToken::class, - 'tagOrId' => \FireflyIII\Support\Binder\TagOrId::class, - 'configName' => \FireflyIII\Support\Binder\ConfigurationName::class, + 'fromCurrencyCode' => CurrencyCode::class, + 'toCurrencyCode' => CurrencyCode::class, + 'unfinishedJournal' => UnfinishedJournal::class, + 'cliToken' => CLIToken::class, + 'tagOrId' => TagOrId::class, + 'configName' => ConfigurationName::class, ], diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 11b57b1d6c..0d0544b554 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -182,4 +182,5 @@ return [ 'transfer_source_bad_data' => 'Could not find a valid source account when searching for ID ":id" or name ":name".', 'transfer_dest_need_data' => 'Need to get a valid destination account ID and/or valid destination account name to continue.', 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'need_id_in_edit' => 'When updating a transaction with splits, each split must have a valid transaction journal id (field "transaction_journal_id").', ]; diff --git a/routes/api.php b/routes/api.php index 22f10efd8f..ae7098bd8b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -43,7 +43,7 @@ declare(strict_types=1); Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'about', 'as' => 'api.v1.about.'], - function () { + static function () { // Accounts API routes: Route::get('', ['uses' => 'AboutController@about', 'as' => 'index']); @@ -54,7 +54,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'accounts', 'as' => 'api.v1.accounts.'], - function () { + static function () { // Accounts API routes: Route::get('', ['uses' => 'AccountController@index', 'as' => 'index']); @@ -71,7 +71,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'attachments', 'as' => 'api.v1.attachments.'], - function () { + static function () { // Attachment API routes: Route::get('', ['uses' => 'AttachmentController@index', 'as' => 'index']); @@ -87,7 +87,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'available_budgets', 'as' => 'api.v1.available_budgets.'], - function () { + static function () { // Available Budget API routes: Route::get('', ['uses' => 'AvailableBudgetController@index', 'as' => 'index']); @@ -99,7 +99,7 @@ Route::group( ); Route::group( - ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'bills', 'as' => 'api.v1.bills.'], function () { + ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'bills', 'as' => 'api.v1.bills.'], static function () { // Bills API routes: Route::get('', ['uses' => 'BillController@index', 'as' => 'index']); @@ -117,7 +117,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'budgets/limits', 'as' => 'api.v1.budget_limits.'], - function () { + static function () { // Budget Limit API routes: Route::get('', ['uses' => 'BudgetLimitController@index', 'as' => 'index']); @@ -131,7 +131,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'budgets', 'as' => 'api.v1.budgets.'], - function () { + static function () { // Budget API routes: Route::get('', ['uses' => 'BudgetController@index', 'as' => 'index']); @@ -147,7 +147,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'categories', 'as' => 'api.v1.categories.'], - function () { + static function () { // Category API routes: Route::get('', ['uses' => 'CategoryController@index', 'as' => 'index']); @@ -167,7 +167,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'chart/account', 'as' => 'api.v1.chart.account.'], - function () { + static function () { Route::get('overview', ['uses' => 'AccountController@overview', 'as' => 'overview']); Route::get('expense', ['uses' => 'AccountController@expenseOverview', 'as' => 'expense']); Route::get('revenue', ['uses' => 'AccountController@revenueOverview', 'as' => 'revenue']); @@ -179,7 +179,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'chart/ab', 'as' => 'api.v1.chart.ab.'], - function () { + static function () { // Overview API routes: Route::get('overview/{availableBudget}', ['uses' => 'AvailableBudgetController@overview', 'as' => 'overview']); @@ -190,7 +190,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'chart/category', 'as' => 'api.v1.chart.category.'], - function () { + static function () { // Overview API routes: Route::get('overview', ['uses' => 'CategoryController@overview', 'as' => 'overview']); @@ -203,7 +203,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'configuration', 'as' => 'api.v1.configuration.'], - function () { + static function () { // Configuration API routes: Route::get('', ['uses' => 'ConfigurationController@index', 'as' => 'index']); @@ -213,7 +213,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'cer', 'as' => 'api.v1.cer.'], - function () { + static function () { // Currency Exchange Rate API routes: Route::get('', ['uses' => 'CurrencyExchangeRateController@index', 'as' => 'index']); @@ -222,7 +222,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'import', 'as' => 'api.v1.import.'], - function () { + static function () { // Transaction Links API routes: Route::get('list', ['uses' => 'ImportController@listAll', 'as' => 'list']); @@ -232,7 +232,7 @@ Route::group( ); Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'link_types', 'as' => 'api.v1.link_types.'], - function () { + static function () { // Link Type API routes: Route::get('', ['uses' => 'LinkTypeController@index', 'as' => 'index']); @@ -247,7 +247,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'transaction_links', 'as' => 'api.v1.transaction_links.'], - function () { + static function () { // Transaction Links API routes: Route::get('', ['uses' => 'TransactionLinkController@index', 'as' => 'index']); @@ -261,7 +261,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'piggy_banks', 'as' => 'api.v1.piggy_banks.'], - function () { + static function () { // Piggy Bank API routes: Route::get('', ['uses' => 'PiggyBankController@index', 'as' => 'index']); @@ -275,7 +275,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'preferences', 'as' => 'api.v1.preferences.'], - function () { + static function () { // Preference API routes: Route::get('', ['uses' => 'PreferenceController@index', 'as' => 'index']); @@ -286,7 +286,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'recurrences', 'as' => 'api.v1.recurrences.'], - function () { + static function () { // Recurrence API routes: Route::get('', ['uses' => 'RecurrenceController@index', 'as' => 'index']); @@ -301,7 +301,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'rules', 'as' => 'api.v1.rules.'], - function () { + static function () { // Rules API routes: Route::get('', ['uses' => 'RuleController@index', 'as' => 'index']); @@ -316,7 +316,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'rule_groups', 'as' => 'api.v1.rule_groups.'], - function () { + static function () { // Rules API routes: Route::get('', ['uses' => 'RuleGroupController@index', 'as' => 'index']); @@ -333,7 +333,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'summary', 'as' => 'api.v1.summary.'], - function () { + static function () { // Overview API routes: Route::get('basic', ['uses' => 'SummaryController@basic', 'as' => 'basic']); @@ -343,7 +343,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'currencies', 'as' => 'api.v1.currencies.'], - function () { + static function () { // Transaction currency API routes: Route::get('', ['uses' => 'CurrencyController@index', 'as' => 'index']); @@ -369,7 +369,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'tags', 'as' => 'api.v1.tags.'], - function () { + static function () { // Tag API routes: Route::get('', ['uses' => 'TagController@index', 'as' => 'index']); Route::post('', ['uses' => 'TagController@store', 'as' => 'store']); @@ -382,7 +382,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'tag-cloud', 'as' => 'api.v1.tag-cloud.'], - function () { + static function () { // Tag cloud API routes (to prevent collisions) Route::get('', ['uses' => 'TagController@cloud', 'as' => 'cloud']); } @@ -391,7 +391,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'transactions', 'as' => 'api.v1.transactions.'], - function () { + static function () { // Transaction API routes: Route::get('', ['uses' => 'TransactionController@index', 'as' => 'index']); @@ -409,7 +409,7 @@ Route::group( Route::group( ['middleware' => ['auth:api', 'bindings', \FireflyIII\Http\Middleware\IsAdmin::class], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'users', 'as' => 'api.v1.users.'], - function () { + static function () { // Users API routes: Route::get('', ['uses' => 'UserController@index', 'as' => 'index']);