diff --git a/app/Api/V1/Requests/TransactionStoreRequest.php b/app/Api/V1/Requests/TransactionStoreRequest.php index f64b7da36a..6a5efa94e6 100644 --- a/app/Api/V1/Requests/TransactionStoreRequest.php +++ b/app/Api/V1/Requests/TransactionStoreRequest.php @@ -74,7 +74,7 @@ class TransactionStoreRequest extends Request { $rules = [ // basic fields for group: - 'group_title' => 'between:1,1000', + 'group_title' => 'between:1,1000|nullable', // transaction rules (in array for splits): 'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation', @@ -82,10 +82,10 @@ class TransactionStoreRequest extends Request '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', + 'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable', + 'transactions.*.currency_code' => 'min:3|max:3|exists:transaction_currencies,code|nullable', + 'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id|nullable', + 'transactions.*.foreign_currency_code' => 'min:3|max:3|exists:transaction_currencies,code|nullable', // amount 'transactions.*.amount' => 'required|numeric|more:0', @@ -97,10 +97,16 @@ class TransactionStoreRequest extends Request // source of transaction 'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser], 'transactions.*.source_name' => 'between:1,255|nullable', + 'transactions.*.source_iban' => 'between:1,255|nullable|iban', + 'transactions.*.source_number' => 'between:1,255|nullable', + 'transactions.*.source_bic' => 'between:1,255|nullable|bic', // destination of transaction 'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser], 'transactions.*.destination_name' => 'between:1,255|nullable', + 'transactions.*.destination_iban' => 'between:1,255|nullable|iban', + 'transactions.*.destination_number' => 'between:1,255|nullable', + 'transactions.*.destination_bic' => 'between:1,255|nullable|bic', // budget, category, bill and piggy 'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser], @@ -202,7 +208,7 @@ class TransactionStoreRequest extends Request 'date' => $this->dateFromValue($object['date']), 'order' => $this->integerFromValue((string)$object['order']), - 'currency_id' => $this->integerFromValue($object['currency_id']), + 'currency_id' => $this->integerFromValue((string)$object['currency_id']), 'currency_code' => $this->stringFromValue($object['currency_code']), // foreign currency info: @@ -219,10 +225,16 @@ class TransactionStoreRequest extends Request // source of transaction. If everything is null, assume cash account. 'source_id' => $this->integerFromValue((string)$object['source_id']), 'source_name' => $this->stringFromValue($object['source_name']), + 'source_iban' => $this->stringFromValue($object['source_iban']), + 'source_number' => $this->stringFromValue($object['source_number']), + 'source_bic' => $this->stringFromValue($object['source_bic']), // destination of transaction. If everything is null, assume cash account. 'destination_id' => $this->integerFromValue((string)$object['destination_id']), 'destination_name' => $this->stringFromValue($object['destination_name']), + 'destination_iban' => $this->stringFromValue($object['destination_iban']), + 'destination_number' => $this->stringFromValue($object['destination_number']), + 'destination_bic' => $this->stringFromValue($object['destination_bic']), // budget info 'budget_id' => $this->integerFromValue((string)$object['budget_id']), diff --git a/app/Api/V1/Requests/TransactionUpdateRequest.php b/app/Api/V1/Requests/TransactionUpdateRequest.php index e1beb9345c..441a0ef9da 100644 --- a/app/Api/V1/Requests/TransactionUpdateRequest.php +++ b/app/Api/V1/Requests/TransactionUpdateRequest.php @@ -105,7 +105,13 @@ class TransactionUpdateRequest extends Request 'foreign_amount', 'description', 'source_name', + 'source_iban', + 'source_number', + 'source_bic', 'destination_name', + 'destination_iban', + 'destination_number', + 'destination_bic', 'budget_name', 'category_name', 'bill_name', diff --git a/app/Factory/AccountMetaFactory.php b/app/Factory/AccountMetaFactory.php index 55df461643..b5ae6b1b7c 100644 --- a/app/Factory/AccountMetaFactory.php +++ b/app/Factory/AccountMetaFactory.php @@ -68,7 +68,6 @@ class AccountMetaFactory { /** @var AccountMeta $entry */ $entry = $account->accountMeta()->where('name', $field)->first(); - // must not be an empty string: if ('' !== $value) { diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index daf7f95a6d..c7e79306d4 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -212,16 +212,33 @@ class TransactionJournalFactory $carbon->setTimezone(config('app.timezone')); /** Get source + destination account */ - Log::debug(sprintf('Source info: ID #%d, name "%s"', $row['source_id'], $row['source_name'])); - Log::debug(sprintf('Destination info: ID #%d, name "%s"', $row['destination_id'], $row['destination_name'])); - + Log::debug(sprintf('Currency is #%d (%s)', $currency->id, $currency->code)); try { // validate source and destination using a new Validator. $this->validateAccounts($row); /** create or get source and destination accounts */ - $sourceAccount = $this->getAccount($type->type, 'source', (int)$row['source_id'], $row['source_name']); - $destinationAccount = $this->getAccount($type->type, 'destination', (int)$row['destination_id'], $row['destination_name']); + + $sourceInfo = [ + 'id' => (int)$row['source_id'], + 'name' => $row['source_name'], + 'iban' => $row['source_iban'], + 'number' => $row['source_number'], + 'bic' => $row['source_bic'], + ]; + + $destInfo = [ + 'id' => (int)$row['destination_id'], + 'name' => $row['destination_name'], + 'iban' => $row['destination_iban'], + 'number' => $row['destination_number'], + 'bic' => $row['destination_bic'], + ]; + Log::debug('Source info:', $sourceInfo); + Log::debug('Destination info:', $destInfo); + + $sourceAccount = $this->getAccount($type->type, 'source', $sourceInfo); + $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo); // @codeCoverageIgnoreStart } catch (FireflyException $e) { Log::error('Could not validate source or destination.'); @@ -372,8 +389,10 @@ class TransactionJournalFactory // return user's default: return app('amount')->getDefaultCurrencyByUser($this->user); } + $result = $preference ?? $currency; + Log::debug(sprintf('Currency is now #%d (%s) because of account #%d (%s)', $result->id, $result->code, $account->id, $account->name)); - return $preference ?? $currency; + return $result; } /** diff --git a/app/Http/Controllers/Transaction/EditController.php b/app/Http/Controllers/Transaction/EditController.php index 9eac08812c..c9e978c176 100644 --- a/app/Http/Controllers/Transaction/EditController.php +++ b/app/Http/Controllers/Transaction/EditController.php @@ -70,7 +70,7 @@ class EditController extends Controller public function edit(TransactionGroup $transactionGroup) { app('preferences')->mark(); - + if (!$this->isEditableGroup($transactionGroup)) { return $this->redirectGroupToAccount($transactionGroup); // @codeCoverageIgnore } diff --git a/app/Rules/BelongsUser.php b/app/Rules/BelongsUser.php index d83d61a405..1d8fa7b8fc 100644 --- a/app/Rules/BelongsUser.php +++ b/app/Rules/BelongsUser.php @@ -160,6 +160,10 @@ class BelongsUser implements Rule */ private function validateAccountId(int $value): bool { + if (0 === $value) { + // its ok to submit 0. other checks will fail. + return true; + } $count = Account::where('id', '=', $value)->where('user_id', '=', auth()->user()->id)->count(); return 1 === $count; diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php index d990cb13c8..070e86819a 100644 --- a/app/Services/Internal/Support/JournalServiceTrait.php +++ b/app/Services/Internal/Support/JournalServiceTrait.php @@ -25,6 +25,7 @@ namespace FireflyIII\Services\Internal\Support; use Exception; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\AccountMetaFactory; use FireflyIII\Factory\TagFactory; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -82,18 +83,17 @@ trait JournalServiceTrait } /** - * @param string $transactionType - * @param string $direction - * @param int|null $accountId - * @param string|null $accountName + * @param string $transactionType + * @param string $direction + * @param array $data * * @return Account * @codeCoverageIgnore */ - protected function getAccount(string $transactionType, string $direction, ?int $accountId, ?string $accountName): Account + protected function getAccount(string $transactionType, string $direction, array $data): Account { // some debug logging: - Log::debug(sprintf('Now in getAccount(%s, %d, %s)', $direction, $accountId, $accountName)); + Log::debug(sprintf('Now in getAccount(%s)', $direction), $data); // final result: $result = null; @@ -109,8 +109,8 @@ trait JournalServiceTrait Log::debug(sprintf($message, $transactionType, $direction, implode(', ', $expectedTypes[$transactionType]))); // first attempt, find by ID. - if (null !== $accountId) { - $search = $this->accountRepository->findNull($accountId); + if (null !== $data['id']) { + $search = $this->accountRepository->findNull($data['id']); 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) @@ -120,12 +120,12 @@ trait JournalServiceTrait } // second attempt, find by name. - if (null === $result && null !== $accountName) { + if (null === $result && null !== $data['name']) { Log::debug('Found nothing by account ID.'); // find by preferred type. - $source = $this->accountRepository->findByName($accountName, [$expectedTypes[$transactionType][0]]); + $source = $this->accountRepository->findByName($data['name'], [$expectedTypes[$transactionType][0]]); // or any expected type. - $source = $source ?? $this->accountRepository->findByName($accountName, $expectedTypes[$transactionType]); + $source = $source ?? $this->accountRepository->findByName($data['name'], $expectedTypes[$transactionType]); if (null !== $source) { Log::debug(sprintf('Found "account_name" object for %s: #%d, %s', $direction, $source->id, $source->name)); @@ -134,30 +134,57 @@ trait JournalServiceTrait } } + // third attempt, find by IBAN + if (null === $result && null !== $data['iban']) { + Log::debug('Found nothing by account name.'); + // find by preferred type. + $source = $this->accountRepository->findByIbanNull($data['iban'], [$expectedTypes[$transactionType][0]]); + // or any expected type. + $source = $source ?? $this->accountRepository->findByIbanNull($data['iban'], $expectedTypes[$transactionType]); + + if (null !== $source) { + Log::debug(sprintf('Found "account_iban" object for %s: #%d, %s', $direction, $source->id, $source->name)); + + $result = $source; + } + } + // return cash account. - if (null === $result && null === $accountName + if (null === $result && null === $data['name'] && in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) { $result = $this->accountRepository->getCashAccount(); } // return new account. if (null === $result) { - $accountName = $accountName ?? '(no name)'; + $data['name'] = $data['name'] ?? '(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)); + throw new FireflyException('TransactionFactory: Cannot create asset account with these values', $data); } $result = $this->accountRepository->store( [ 'account_type_id' => null, - 'account_type' => $preferredType, - 'name' => $accountName, + 'account_type' => $preferredType, + 'name' => $data['name'], 'active' => true, - 'iban' => null, + 'iban' => $data['iban'], ] ); + // store BIC + if (null !== $data['bic']) { + /** @var AccountMetaFactory $metaFactory */ + $metaFactory = app(AccountMetaFactory::class); + $metaFactory->create(['account_id' => $result->id, 'name' => 'BIC', 'data' => $data['bic']]); + } + // store account number + if (null !== $data['number']) { + /** @var AccountMetaFactory $metaFactory */ + $metaFactory = app(AccountMetaFactory::class); + $metaFactory->create(['account_id' => $result->id, 'name' => 'account_number', 'data' => $data['bic']]); + } } return $result; diff --git a/app/Services/Internal/Update/JournalUpdateService.php b/app/Services/Internal/Update/JournalUpdateService.php index 0749979076..ae49a175b8 100644 --- a/app/Services/Internal/Update/JournalUpdateService.php +++ b/app/Services/Internal/Update/JournalUpdateService.php @@ -294,14 +294,19 @@ class JournalUpdateService return $this->getOriginalDestinationAccount(); } - $destId = $this->data['destination_id'] ?? null; - $destName = $this->data['destination_name'] ?? null; + $destInfo = [ + 'id' => (int)($this->data['destination_id'] ?? null), + 'name' => $this->data['destination_name'] ?? null, + 'iban' => $this->data['destination_iban'] ?? null, + 'number' => $this->data['destination_number'] ?? null, + 'bic' => $this->data['destination_bic'] ?? 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); + $result = $this->getAccount($expectedType, 'destination', $destInfo); } catch (FireflyException $e) { Log::error(sprintf('getValidDestinationAccount() threw unexpected error: %s', $e->getMessage())); $result = $this->getOriginalDestinationAccount(); @@ -318,16 +323,22 @@ class JournalUpdateService 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(); } + $sourceInfo = [ + 'id' => (int)($this->data['source_id'] ?? null), + 'name' => $this->data['source_name'] ?? null, + 'iban' => $this->data['source_iban'] ?? null, + 'number' => $this->data['source_number'] ?? null, + 'bic' => $this->data['source_bic'] ?? null, + ]; + $expectedType = $this->getExpectedType(); try { - $result = $this->getAccount($expectedType, 'source', $sourceId, $sourceName); + $result = $this->getAccount($expectedType, 'source', $sourceInfo); } catch (FireflyException $e) { Log::error(sprintf('Cant get the valid source account: %s', $e->getMessage())); diff --git a/app/Support/Search/AccountSearch.php b/app/Support/Search/AccountSearch.php index 94a6434597..4d3c765a6c 100644 --- a/app/Support/Search/AccountSearch.php +++ b/app/Support/Search/AccountSearch.php @@ -108,7 +108,7 @@ class AccountSearch implements GenericSearchInterface break; } - return $query->get(['accounts.*']); + return $query->distinct()->get(['accounts.*']); } /**