From b692cccdfb62c083674f30ffe3dc4b1dde460065 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 31 Mar 2019 13:36:49 +0200 Subject: [PATCH] User can submit new journal through API. --- app/Api/V1/Requests/TransactionRequest.php | 241 ++++++---- .../Commands/Correction/RenameMetaFields.php | 87 ++++ .../Commands/Upgrade/MigrateToGroups.php | 40 +- app/Factory/TransactionFactory.php | 182 ++++---- app/Factory/TransactionGroupFactory.php | 84 ++++ app/Factory/TransactionJournalFactory.php | 245 ++++++----- app/Helpers/Collector/GroupCollector.php | 47 +- app/Http/Requests/Request.php | 88 +++- app/Import/Storage/ImportArrayStorage.php | 6 +- app/Providers/FireflyServiceProvider.php | 19 +- app/Repositories/Bill/BillRepository.php | 10 +- .../Bill/BillRepositoryInterface.php | 3 +- .../Currency/CurrencyRepository.php | 26 +- .../Currency/CurrencyRepositoryInterface.php | 3 +- .../Journal/JournalRepository.php | 7 +- .../Import/Placeholder/ImportTransaction.php | 4 +- .../Routine/File/ImportableConverter.php | 16 +- .../TransactionGroupTransformer.php | 115 ++--- app/Transformers/TransactionTransformer.php | 18 +- app/Validation/AccountValidator.php | 415 ++++++++++++++++++ app/Validation/TransactionValidation.php | 341 +++++++------- config/csv.php | 16 +- resources/lang/en_US/import.php | 16 +- resources/lang/en_US/list.php | 16 +- resources/lang/en_US/validation.php | 20 +- resources/views/v1/list/groups-tiny.twig | 2 +- resources/views/v1/list/journals-tiny.twig | 20 +- resources/views/v1/list/journals.twig | 8 +- resources/views/v1/transactions/show.twig | 2 +- routes/web.php | 75 ++-- 30 files changed, 1461 insertions(+), 711 deletions(-) create mode 100644 app/Console/Commands/Correction/RenameMetaFields.php create mode 100644 app/Factory/TransactionGroupFactory.php create mode 100644 app/Validation/AccountValidator.php diff --git a/app/Api/V1/Requests/TransactionRequest.php b/app/Api/V1/Requests/TransactionRequest.php index a9b024cc37..ddd2654b65 100644 --- a/app/Api/V1/Requests/TransactionRequest.php +++ b/app/Api/V1/Requests/TransactionRequest.php @@ -27,6 +27,7 @@ namespace FireflyIII\Api\V1\Requests; use FireflyIII\Rules\BelongsUser; use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsDateOrTime; +use FireflyIII\Support\NullArrayObject; use FireflyIII\Validation\TransactionValidation; use Illuminate\Validation\Validator; @@ -59,34 +60,8 @@ class TransactionRequest extends Request public function getAll(): array { $data = [ - 'type' => $this->string('type'), - 'date' => $this->dateTime('date'), - 'description' => $this->string('description'), - 'piggy_bank_id' => $this->integer('piggy_bank_id'), - 'piggy_bank_name' => $this->string('piggy_bank_name'), - 'bill_id' => $this->integer('bill_id'), - 'bill_name' => $this->string('bill_name'), - 'tags' => explode(',', $this->string('tags')), - 'notes' => $this->string('notes'), - 'sepa-cc' => $this->string('sepa_cc'), - 'sepa-ct-op' => $this->string('sepa_ct_op'), - 'sepa-ct-id' => $this->string('sepa_ct_id'), - 'sepa-db' => $this->string('sepa_db'), - 'sepa-country' => $this->string('sepa_country'), - 'sepa-ep' => $this->string('sepa_ep'), - 'sepa-ci' => $this->string('sepa_ci'), - 'sepa-batch-id' => $this->string('sepa_batch_id'), - 'interest_date' => $this->date('interest_date'), - 'book_date' => $this->date('book_date'), - 'process_date' => $this->date('process_date'), - 'due_date' => $this->date('due_date'), - 'payment_date' => $this->date('payment_date'), - 'invoice_date' => $this->date('invoice_date'), - 'internal_reference' => $this->string('internal_reference'), - 'bunq_payment_id' => $this->string('bunq_payment_id'), - 'external_id' => $this->string('external_id'), - 'original-source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')), - 'transactions' => $this->getTransactionData(), + 'group_title' => $this->string('group_title'), + 'transactions' => $this->getTransactionData(), ]; return $data; @@ -101,61 +76,76 @@ class TransactionRequest extends Request public function rules(): array { $rules = [ - // basic fields for journal: - 'type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation', - 'description' => 'between:1,255', - 'date' => ['required', new IsDateOrTime], - 'piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUser], - 'piggy_bank_name' => ['between:1,255', 'nullable', new BelongsUser], - 'bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser], - 'bill_name' => ['between:1,255', 'nullable', new BelongsUser], - 'tags' => 'between:1,255', - - // then, custom fields for journal - 'notes' => 'min:1,max:50000|nullable', - - // SEPA fields: - 'sepa_cc' => 'min:1,max:255|nullable', - 'sepa_ct_op' => 'min:1,max:255|nullable', - 'sepa_ct_id' => 'min:1,max:255|nullable', - 'sepa_db' => 'min:1,max:255|nullable', - 'sepa_country' => 'min:1,max:255|nullable', - 'sepa_ep' => 'min:1,max:255|nullable', - 'sepa_ci' => 'min:1,max:255|nullable', - 'sepa_batch_id' => 'min:1,max:255|nullable', - - // dates - 'interest_date' => 'date|nullable', - 'book_date' => 'date|nullable', - 'process_date' => 'date|nullable', - 'due_date' => 'date|nullable', - 'payment_date' => 'date|nullable', - 'invoice_date' => 'date|nullable', - 'internal_reference' => 'min:1,max:255|nullable', - 'bunq_payment_id' => 'min:1,max:255|nullable', - 'external_id' => 'min:1,max:255|nullable', + // basic fields for group: + 'group_title' => 'between:1,255', // transaction rules (in array for splits): - 'transactions.*.amount' => 'required|numeric|more:0', - 'transactions.*.description' => 'nullable|between:1,255', + 'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation', + 'transactions.*.date' => ['required', new IsDateOrTime], + + // currency info 'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id', 'transactions.*.currency_code' => 'min:3|max:3|exists:transaction_currencies,code', - 'transactions.*.foreign_amount' => 'numeric|more:0', 'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id', 'transactions.*.foreign_currency_code' => 'min:3|max:3|exists:transaction_currencies,code', + + // amount + 'transactions.*.amount' => 'required|numeric|more:0', + 'transactions.*.foreign_amount' => 'numeric|more: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], + 'transactions.*.piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUser], + 'transactions.*.piggy_bank_name' => ['between:1,255', 'nullable', new BelongsUser], + + // other interesting fields 'transactions.*.reconciled' => [new IsBoolean], - 'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser], - 'transactions.*.source_name' => 'between:1,255|nullable', - 'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser], - 'transactions.*.destination_name' => 'between:1,255|nullable', + 'transactions.*.notes' => 'min:1,max:50000|nullable', + 'transactions.*.tags' => 'between:1,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', ]; if ('PUT' === $this->method()) { - unset($rules['type'], $rules['piggy_bank_id'], $rules['piggy_bank_name']); + unset($rules['transactions.*.type'], $rules['transactions.*.piggy_bank_id'], $rules['transactions.*.piggy_bank_name']); } return $rules; @@ -174,13 +164,28 @@ class TransactionRequest extends Request { $validator->after( function (Validator $validator) { + // must submit at least one transaction. $this->validateOneTransaction($validator); + + // all journals must have a description $this->validateDescriptions($validator); - $this->validateJournalDescription($validator); - $this->validateSplitDescriptions($validator); + + // all transaction types must be equal: + $this->validateTransactionTypes($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); } ); } @@ -195,28 +200,88 @@ class TransactionRequest extends Request private function getTransactionData(): array { $return = []; + /** + * @var int $index + * @var array $transaction + */ foreach ($this->get('transactions') as $index => $transaction) { + $object = new NullArrayObject($transaction); $return[] = [ - 'amount' => $transaction['amount'], - 'description' => $transaction['description'] ?? null, - 'currency_id' => isset($transaction['currency_id']) ? (int)$transaction['currency_id'] : null, - 'currency_code' => $transaction['currency_code'] ?? null, - 'foreign_amount' => $transaction['foreign_amount'] ?? null, - 'foreign_currency_id' => isset($transaction['foreign_currency_id']) ? (int)$transaction['foreign_currency_id'] : null, - 'foreign_currency_code' => $transaction['foreign_currency_code'] ?? null, - 'budget_id' => isset($transaction['budget_id']) ? (int)$transaction['budget_id'] : null, - 'budget_name' => $transaction['budget_name'] ?? null, - 'category_id' => isset($transaction['category_id']) ? (int)$transaction['category_id'] : null, - 'category_name' => $transaction['category_name'] ?? null, - 'source_id' => isset($transaction['source_id']) ? (int)$transaction['source_id'] : null, - 'source_name' => isset($transaction['source_name']) ? (string)$transaction['source_name'] : null, - 'destination_id' => isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null, - 'destination_name' => isset($transaction['destination_name']) ? (string)$transaction['destination_name'] : null, - 'reconciled' => $this->convertBoolean((string)($transaction['reconciled'] ?? 'false')), - 'identifier' => $index, + // $this->dateFromValue($object['']) + 'type' => $this->stringFromValue($object['type']), + 'date' => $this->dateFromValue($object['date']), + 'currency_id' => $this->integerFromValue($object['currency_id']), + 'currency_code' => $this->stringFromValue($object['currency_code']), + + // foreign currency info: + 'foreign_currency_id' => $this->integerFromValue((string)$object['foreign_currency_id']), + 'foreign_currency_code' => $this->stringFromValue($object['foreign_currency_code']), + + // amount and foreign amount. Cannot be 0. + 'amount' => $this->stringFromValue($object['amount']), + 'foreign_amount' => $this->stringFromValue($object['foreign_amount']), + + // description. + 'description' => $this->stringFromValue($object['description']), + + // 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']), + + // 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']), + + // budget info + 'budget_id' => $this->integerFromValue((string)$object['budget_id']), + 'budget_name' => $this->stringFromValue($object['budget_name']), + + // category info + 'category_id' => $this->integerFromValue((string)$object['category_id']), + 'category_name' => $this->stringFromValue($object['category_name']), + + // journal bill reference. Optional. Will only work for withdrawals + 'bill_id' => $this->integerFromValue((string)$object['bill_id']), + 'bill_name' => $this->stringFromValue($object['bill_name']), + + // piggy bank reference. Optional. Will only work for transfers + 'piggy_bank_id' => $this->integerFromValue((string)$object['piggy_bank_id']), + 'piggy_bank_name' => $this->stringFromValue($object['piggy_bank_name']), + + // some other interesting properties + 'reconciled' => $this->convertBoolean((string)$object['reconciled']), + 'notes' => $this->stringFromValue($object['notes']), + 'tags' => $this->arrayFromValue($object['tags']), + + // all custom fields: + 'internal_reference' => $this->stringFromValue($object['internal_reference']), + 'external_id' => $this->stringFromValue($object['external_id']), + 'original_source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')), + 'recurrence_id' => $this->integerFromValue($object['recurrence_id']), + 'bunq_payment_id' => $this->stringFromValue($object['bunq_payment_id']), + + 'sepa_cc' => $this->stringFromValue($object['sepa_cc']), + 'sepa_ct_op' => $this->stringFromValue($object['sepa_ct_op']), + 'sepa_ct_id' => $this->stringFromValue($object['sepa_ct_id']), + 'sepa_db' => $this->stringFromValue($object['sepa_db']), + 'sepa_country' => $this->stringFromValue($object['sepa_country']), + 'sepa_ep' => $this->stringFromValue($object['sepa_ep']), + 'sepa_ci' => $this->stringFromValue($object['sepa_ci']), + 'sepa_batch_id' => $this->stringFromValue($object['sepa_batch_id']), + + + // custom date fields. Must be Carbon objects. Presence is optional. + 'interest_date' => $this->dateFromValue($object['interest_date']), + 'book_date' => $this->dateFromValue($object['book_date']), + 'process_date' => $this->dateFromValue($object['process_date']), + 'due_date' => $this->dateFromValue($object['due_date']), + 'payment_date' => $this->dateFromValue($object['payment_date']), + 'invoice_date' => $this->dateFromValue($object['invoice_date']), + ]; } return $return; } + } diff --git a/app/Console/Commands/Correction/RenameMetaFields.php b/app/Console/Commands/Correction/RenameMetaFields.php new file mode 100644 index 0000000000..4b4656d613 --- /dev/null +++ b/app/Console/Commands/Correction/RenameMetaFields.php @@ -0,0 +1,87 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use DB; +use Illuminate\Console\Command; + +/** + * Class RenameMetaFields + */ +class RenameMetaFields extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Rename changed meta fields.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:rename-meta-fields'; + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $start = microtime(true); + + $changes = [ + 'original-source' => 'original_source', + 'importHash' => 'import_hash', + 'importHashV2' => 'import_hash_v2', + 'sepa-cc' => 'sepa_cc', + 'sepa-ct-op' => 'sepa_ct_op', + 'sepa-ct-id' => 'sepa_ct_id', + 'sepa-db' => 'sepa_db', + 'sepa-country' => 'sepa_country', + 'sepa-ep' => 'sepa_ep', + 'sepa-ci' => 'sepa_ci', + 'sepa-batch-id' => 'sepa_batch_id', + ]; + foreach ($changes as $original => $update) { + $this->rename($original, $update); + } + + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('Renamed meta fields in %s seconds', $end)); + + return 0; + } + + /** + * @param string $original + * @param string $update + */ + private function rename(string $original, string $update): void + { + DB::table('journal_meta') + ->where('name', '=', $original) + ->update(['name' => $update]); + } +} diff --git a/app/Console/Commands/Upgrade/MigrateToGroups.php b/app/Console/Commands/Upgrade/MigrateToGroups.php index 9fcd5fe234..0dd6f17d5f 100644 --- a/app/Console/Commands/Upgrade/MigrateToGroups.php +++ b/app/Console/Commands/Upgrade/MigrateToGroups.php @@ -240,20 +240,20 @@ class MigrateToGroups extends Command $notes = $this->journalRepository->getNoteText($journal); $tags = $this->journalRepository->getTags($journal); $internalRef = $this->journalRepository->getMetaField($journal, 'internal-reference'); - $sepaCC = $this->journalRepository->getMetaField($journal, 'sepa-cc'); - $sepaCtOp = $this->journalRepository->getMetaField($journal, 'sepa-ct-op'); - $sepaCtId = $this->journalRepository->getMetaField($journal, 'sepa-ct-id'); - $sepaDb = $this->journalRepository->getMetaField($journal, 'sepa-db'); - $sepaCountry = $this->journalRepository->getMetaField($journal, 'sepa-country'); - $sepaEp = $this->journalRepository->getMetaField($journal, 'sepa-ep'); - $sepaCi = $this->journalRepository->getMetaField($journal, 'sepa-ci'); - $sepaBatchId = $this->journalRepository->getMetaField($journal, 'sepa-batch-id'); + $sepaCC = $this->journalRepository->getMetaField($journal, 'sepa_cc'); + $sepaCtOp = $this->journalRepository->getMetaField($journal, 'sepa_ct_op'); + $sepaCtId = $this->journalRepository->getMetaField($journal, 'sepa_ct_id'); + $sepaDb = $this->journalRepository->getMetaField($journal, 'sepa_db'); + $sepaCountry = $this->journalRepository->getMetaField($journal, 'sepa_country'); + $sepaEp = $this->journalRepository->getMetaField($journal, 'sepa_ep'); + $sepaCi = $this->journalRepository->getMetaField($journal, 'sepa_ci'); + $sepaBatchId = $this->journalRepository->getMetaField($journal, 'sepa_batch_id'); $externalId = $this->journalRepository->getMetaField($journal, 'external-id'); $originalSource = $this->journalRepository->getMetaField($journal, 'original-source'); $recurrenceId = $this->journalRepository->getMetaField($journal, 'recurrence_id'); $bunq = $this->journalRepository->getMetaField($journal, 'bunq_payment_id'); - $hash = $this->journalRepository->getMetaField($journal, 'importHash'); - $hashTwo = $this->journalRepository->getMetaField($journal, 'importHashV2'); + $hash = $this->journalRepository->getMetaField($journal, 'import_hash'); + $hashTwo = $this->journalRepository->getMetaField($journal, 'import_hash_v2'); $interestDate = $this->journalRepository->getMetaDate($journal, 'interest_date'); $bookDate = $this->journalRepository->getMetaDate($journal, 'book_date'); $processDate = $this->journalRepository->getMetaDate($journal, 'process_date'); @@ -293,20 +293,20 @@ class MigrateToGroups extends Command 'notes' => $notes, 'tags' => $tags, 'internal_reference' => $internalRef, - 'sepa-cc' => $sepaCC, - 'sepa-ct-op' => $sepaCtOp, - 'sepa-ct-id' => $sepaCtId, - 'sepa-db' => $sepaDb, - 'sepa-country' => $sepaCountry, - 'sepa-ep' => $sepaEp, - 'sepa-ci' => $sepaCi, - 'sepa-batch-id' => $sepaBatchId, + 'sepa_cc' => $sepaCC, + 'sepa_ct_op' => $sepaCtOp, + 'sepa_ct_id' => $sepaCtId, + 'sepa_db' => $sepaDb, + 'sepa_country' => $sepaCountry, + 'sepa_ep' => $sepaEp, + 'sepa_ci' => $sepaCi, + 'sepa_batch_id' => $sepaBatchId, 'external_id' => $externalId, 'original-source' => $originalSource, 'recurrence_id' => $recurrenceId, 'bunq_payment_id' => $bunq, - 'importHash' => $hash, - 'importHashV2' => $hashTwo, + 'import_hash' => $hash, + 'import_hash_v2' => $hashTwo, 'interest_date' => $interestDate, 'book_date' => $bookDate, 'process_date' => $processDate, diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index 564980e431..329397fa08 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -31,10 +31,10 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\NullArrayObject; use FireflyIII\User; +use FireflyIII\Validation\AccountValidator; use Illuminate\Database\QueryException; use Illuminate\Support\Collection; use Log; @@ -46,6 +46,8 @@ class TransactionFactory { /** @var AccountRepositoryInterface */ private $accountRepository; + /** @var AccountValidator */ + private $accountValidator; /** @var TransactionJournal */ private $journal; /** @var User */ @@ -60,6 +62,7 @@ class TransactionFactory Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); } $this->accountRepository = app(AccountRepositoryInterface::class); + $this->accountValidator = app(AccountValidator::class); } /** @@ -110,16 +113,17 @@ class TransactionFactory */ public function createPair(NullArrayObject $data, TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): Collection { - $sourceAccount = $this->getAccount('source', $data['source'], (int)$data['source_id'], $data['source_name']); - $destinationAccount = $this->getAccount('destination', $data['destination'], (int)$data['destination_id'], $data['destination_name']); - $amount = $this->getAmount($data['amount']); - $foreignAmount = $this->getForeignAmount($data['foreign_amount']); + // validate source and destination using a new Validator. + $this->validateAccounts($data); - $this->makeDramaOverAccountTypes($sourceAccount, $destinationAccount); + // 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']); - - $one = $this->create($sourceAccount, $currency, app('steam')->negative($amount)); - $two = $this->create($destinationAccount, $currency, app('steam')->positive($amount)); + $amount = $this->getAmount($data['amount']); + $foreignAmount = $this->getForeignAmount($data['foreign_amount']); + $one = $this->create($sourceAccount, $currency, app('steam')->negative($amount)); + $two = $this->create($destinationAccount, $currency, app('steam')->positive($amount)); $one->reconciled = $data['reconciled'] ?? false; $two->reconciled = $data['reconciled'] ?? false; @@ -128,8 +132,8 @@ class TransactionFactory if (null !== $foreignCurrency) { $one->foreign_currency_id = $foreignCurrency->id; $two->foreign_currency_id = $foreignCurrency->id; - $one->foreign_amount = $foreignAmount; - $two->foreign_amount = $foreignAmount; + $one->foreign_amount = app('steam')->negative($foreignAmount); + $two->foreign_amount = app('steam')->positive($foreignAmount); } @@ -141,18 +145,21 @@ class TransactionFactory } /** - * @param string $direction - * @param Account|null $source - * @param int|null $sourceId - * @param string|null $sourceName + * @param string $direction + * @param int|null $accountId + * @param string|null $accountName * * @return Account * @throws FireflyException */ - public function getAccount(string $direction, ?Account $source, ?int $sourceId, ?string $sourceName): Account + public function getAccount(string $direction, ?int $accountId, ?string $accountName): Account { - Log::debug(sprintf('Now in getAccount(%s)', $direction)); - Log::debug(sprintf('Parameters: ((account), %s, %s)', var_export($sourceId, true), var_export($sourceName, true))); + // 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'); @@ -161,64 +168,62 @@ class TransactionFactory // and now try to find it, based on the type of transaction. $transactionType = $this->journal->transactionType->type; - Log::debug( - sprintf( - 'Based on the fact that the transaction is a %s, the %s account should be in: %s', $transactionType, $direction, - implode(', ', $expectedTypes[$transactionType]) - ) - ); + $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, check the "source" object. - if (null !== $source && $source->user_id === $this->user->id && \in_array($source->accountType->type, $expectedTypes[$transactionType], true)) { - Log::debug(sprintf('Found "account" object for %s: #%d, %s', $direction, $source->id, $source->name)); - - return $source; - } - - // second attempt, find by ID. - if (null !== $sourceId) { - $source = $this->accountRepository->findNull($sourceId); - if (null !== $source && \in_array($source->accountType->type, $expectedTypes[$transactionType], true)) { + // 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, $source->id, $source->name, $source->accountType->type) + sprintf('Found "account_id" object for %s: #%d, "%s" of type %s', $direction, $search->id, $search->name, $search->accountType->type) ); - - return $source; + $result = $search; } } - // third attempt, find by name. - if (null !== $sourceName) { + // 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($sourceName, [$expectedTypes[$transactionType][0]]); - // or any type. - $source = $source ?? $this->accountRepository->findByName($sourceName, $expectedTypes[$transactionType]); + $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)); - return $source; + $result = $source; } } - if (null === $sourceName && \in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) { - return $this->accountRepository->getCashAccount(); - } - $sourceName = $sourceName ?? '(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".', $sourceId, $sourceName)); + + // return cash account. + if (null === $result && null === $accountName + && in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) { + $result = $this->accountRepository->getCashAccount(); } - return $this->accountRepository->store( - [ - 'account_type_id' => null, - 'accountType' => $preferredType, - 'name' => $sourceName, - 'active' => true, - 'iban' => null, - ] - ); + // 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; } /** @@ -246,6 +251,7 @@ class TransactionFactory */ public function getForeignAmount(?string $amount): ?string { + $result = null; if (null === $amount) { Log::debug('No foreign amount info in array. Return NULL'); @@ -266,31 +272,6 @@ class TransactionFactory return $amount; } - /** - * This method will throw a Firefly III Exception of the source and destination account types are not OK. - * - * @throws FireflyException - * - * @param Account $source - * @param Account $destination - */ - public function makeDramaOverAccountTypes(Account $source, Account $destination): void - { - // if the source is X, then Y is allowed as destination. - $combinations = config('firefly.source_dests'); - $sourceType = $source->accountType->type; - $destType = $destination->accountType->type; - $journalType = $this->journal->transactionType->type; - $allowed = $combinations[$journalType][$sourceType] ?? []; - if (!\in_array($destType, $allowed, true)) { - throw new FireflyException( - sprintf( - 'Journal of type "%s" has a source account of type "%s" and cannot accept a "%s"-account as destination, but only accounts of: %s', $journalType, $sourceType, - $destType, implode(', ', $combinations[$journalType][$sourceType]) - ) - ); - } - } /** * @param TransactionJournal $journal @@ -308,4 +289,33 @@ class TransactionFactory $this->user = $user; $this->accountRepository->setUser($user); } + + /** + * @param NullArrayObject $data + * + * @throws FireflyException + */ + private function validateAccounts(NullArrayObject $data): void + { + $transactionType = $data['type'] ?? 'invalid'; + $this->accountValidator->setTransactionType($transactionType); + + // validate source account. + $sourceId = isset($data['source_id']) ? (int)$data['source_id'] : null; + $sourceName = $data['source_name'] ?? null; + $validSource = $this->accountValidator->validateSource($sourceId, $sourceName); + + // do something with result: + if (false === $validSource) { + throw new FireflyException($this->accountValidator->sourceError); + } + // validate destination account + $destinationId = isset($data['destination_id']) ? (int)$data['destination_id'] : null; + $destinationName = $data['destination_name'] ?? null; + $validDestination = $this->accountValidator->validateDestination($destinationId, $destinationName); + // do something with result: + if (false === $validDestination) { + throw new FireflyException($this->accountValidator->destError); + } + } } diff --git a/app/Factory/TransactionGroupFactory.php b/app/Factory/TransactionGroupFactory.php new file mode 100644 index 0000000000..fae7bb2b93 --- /dev/null +++ b/app/Factory/TransactionGroupFactory.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Factory; + +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\User; + +/** + * Class TransactionGroupFactory + */ +class TransactionGroupFactory +{ + /** @var TransactionJournalFactory */ + private $journalFactory; + /** @var User The user */ + private $user; + + /** + * TransactionGroupFactory constructor. + */ + public function __construct() + { + $this->journalFactory = app(TransactionJournalFactory::class); + } + + /** + * Store a new transaction journal. + * + * @param array $data + * + * @return TransactionGroup + * @throws FireflyException + */ + public function create(array $data): TransactionGroup + { + $this->journalFactory->setUser($this->user); + $collection = $this->journalFactory->create($data); + $title = $data['group_title'] ?? null; + /** @var TransactionJournal $first */ + $first = $collection->first(); + $group = new TransactionGroup; + $group->user()->associate($first->user); + $group->title = $title ?? $first->description; + $group->save(); + + $group->transactionJournals()->saveMany($collection); + + return $group; + } + + /** + * Set the user. + * + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + } +} \ No newline at end of file diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index 2dabc38944..4f2ab89126 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -26,9 +26,9 @@ namespace FireflyIII\Factory; use Carbon\Carbon; use Exception; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Note; use FireflyIII\Models\TransactionCurrency; -use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Bill\BillRepositoryInterface; @@ -77,14 +77,25 @@ class TransactionJournalFactory */ public function __construct() { - $this->fields = ['sepa-cc', 'sepa-ct-op', 'sepa-ct-id', 'sepa-db', 'sepa-country', 'sepa-ep', 'sepa-ci', 'interest_date', 'book_date', 'process_date', - 'due_date', 'recurrence_id', 'payment_date', 'invoice_date', 'internal_reference', 'bunq_payment_id', 'importHash', 'importHashV2', - 'external_id', 'sepa-batch-id', 'original-source']; + $this->fields = [ + // sepa + 'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', + 'sepa_db', 'sepa_country', 'sepa_ep', + 'sepa_ci', 'sepa_batch_id', + + // dates + 'interest_date', 'book_date', 'process_date', + 'due_date', 'payment_date', 'invoice_date', + + // others + 'recurrence_id', 'internal_reference', 'bunq_payment_id', + 'import_hash', 'import_hash_v2', 'external_id', 'original_source']; if ('testing' === config('app.env')) { Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); } + $this->currencyRepository = app(CurrencyRepositoryInterface::class); $this->typeRepository = app(TransactionTypeRepositoryInterface::class); $this->transactionFactory = app(TransactionFactory::class); @@ -97,26 +108,22 @@ class TransactionJournalFactory } /** - * Store a new transaction journal. + * Store a new (set of) transaction journals. * * @param array $data * - * @return TransactionGroup - * @throws Exception + * @return Collection + * @throws FireflyException */ - public function create(array $data): TransactionGroup + public function create(array $data): Collection { + // convert to special object. $data = new NullArrayObject($data); + Log::debug('Start of TransactionJournalFactory::create()'); $collection = new Collection; $transactions = $data['transactions'] ?? []; - $type = $this->typeRepository->findTransactionType(null, $data['type']); - $carbon = $data['date'] ?? new Carbon; - $carbon->setTimezone(config('app.timezone')); - - Log::debug(sprintf('Going to store a %s.', $type->type)); - - if (0 === \count($transactions)) { + if (0 === count($transactions)) { Log::error('There are no transactions in the array, the TransactionJournalFactory cannot continue.'); return new Collection; @@ -124,79 +131,15 @@ class TransactionJournalFactory /** @var array $row */ foreach ($transactions as $index => $row) { - $transaction = new NullArrayObject($row); - Log::debug(sprintf('Now creating journal %d/%d', $index + 1, \count($transactions))); - /** Get basic fields */ + Log::debug(sprintf('Now creating journal %d/%d', $index + 1, count($transactions))); - $currency = $this->currencyRepository->findCurrency( - $transaction['currency'], (int)$transaction['currency_id'], $transaction['currency_code'] - ); - $foreignCurrency = $this->findForeignCurrency($transaction); - - $bill = $this->billRepository->findBill($transaction['bill'], (int)$transaction['bill_id'], $transaction['bill_name']); - $billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null; - $description = app('steam')->cleanString((string)$transaction['description']); - - /** Create a basic journal. */ - $journal = TransactionJournal::create( - [ - 'user_id' => $this->user->id, - 'transaction_type_id' => $type->id, - 'bill_id' => $billId, - 'transaction_currency_id' => $currency->id, - 'description' => '' === $description ? '(empty description)' : $description, - 'date' => $carbon->format('Y-m-d H:i:s'), - 'order' => 0, - 'tag_count' => 0, - 'completed' => 0, - ] - ); - Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description)); - - /** Create two transactions. */ - $this->transactionFactory->setJournal($journal); - $this->transactionFactory->createPair($transaction, $currency, $foreignCurrency); - - // verify that journal has two transactions. Otherwise, delete and cancel. - $count = $journal->transactions()->count(); - if (2 !== $count) { - // @codeCoverageIgnoreStart - Log::error(sprintf('The journal unexpectedly has %d transaction(s). This is not OK. Cancel operation.', $count)); - $journal->delete(); - - return new Collection; - // @codeCoverageIgnoreEnd + $journal = $this->createJournal(new NullArrayObject($row)); + if (null !== $journal) { + $collection->push($journal); } - $journal->completed =true; - $journal->save(); - - /** Link all other data to the journal. */ - - /** Link budget */ - $this->storeBudget($journal, $transaction); - - /** Link category */ - $this->storeCategory($journal, $transaction); - - /** Set notes */ - $this->storeNote($journal, $transaction['notes']); - - /** Set piggy bank */ - $this->storePiggyEvent($journal, $transaction); - - /** Set tags */ - $this->storeTags($journal, $transaction['tags']); - - /** set all meta fields */ - $this->storeMetaFields($journal, $transaction); - - $collection->push($journal); } - $group = $this->storeGroup($collection, $data['group_title']); - - return $group; - + return $collection; } /** @@ -215,31 +158,6 @@ class TransactionJournalFactory $this->piggyRepository->setUser($this->user); } - /** - * Join multiple journals in a group. - * - * @param Collection $collection - * @param string|null $title - * - * @return TransactionGroup|null - */ - public function storeGroup(Collection $collection, ?string $title): ?TransactionGroup - { - if ($collection->count() < 2) { - return null; // @codeCoverageIgnore - } - /** @var TransactionJournal $first */ - $first = $collection->first(); - $group = new TransactionGroup; - $group->user()->associate($first->user); - $group->title = $title ?? $first->description; - $group->save(); - - $group->transactionJournals()->saveMany($collection); - - return $group; - } - /** * Link a piggy bank to this journal. * @@ -332,6 +250,88 @@ class TransactionJournalFactory } } + /** + * @param NullArrayObject $row + * + * @return TransactionJournal|null + * @throws FireflyException + */ + private function createJournal(NullArrayObject $row): ?TransactionJournal + { + $row['import_hash_v2'] = $this->hashArray($row); + + /** Get basic fields */ + $type = $this->typeRepository->findTransactionType(null, $row['type']); + $carbon = $row['date'] ?? new Carbon; + $currency = $this->currencyRepository->findCurrency((int)$row['currency_id'], $row['currency_code']); + $foreignCurrency = $this->findForeignCurrency($row); + $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']); + + /** Manipulate basic fields */ + $carbon->setTimezone(config('app.timezone')); + + /** Create a basic journal. */ + $journal = TransactionJournal::create( + [ + 'user_id' => $this->user->id, + 'transaction_type_id' => $type->id, + 'bill_id' => $billId, + 'transaction_currency_id' => $currency->id, + 'description' => '' === $description ? '(empty description)' : $description, + 'date' => $carbon->format('Y-m-d H:i:s'), + 'order' => 0, + 'tag_count' => 0, + 'completed' => 0, + ] + ); + Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description)); + + /** Create two transactions. */ + $this->transactionFactory->setJournal($journal); + $this->transactionFactory->createPair($row, $currency, $foreignCurrency); + + // verify that journal has two transactions. Otherwise, delete and cancel. + $count = $journal->transactions()->count(); + if (2 !== $count) { + // @codeCoverageIgnoreStart + Log::error(sprintf('The journal unexpectedly has %d transaction(s). This is not OK. Cancel operation.', $count)); + try { + $journal->delete(); + } catch (Exception $e) { + Log::debug(sprintf('Dont care: %s.', $e->getMessage())); + } + + return null; + // @codeCoverageIgnoreEnd + } + $journal->completed = true; + $journal->save(); + + /** Link all other data to the journal. */ + + /** Link budget */ + $this->storeBudget($journal, $row); + + /** Link category */ + $this->storeCategory($journal, $row); + + /** Set notes */ + $this->storeNote($journal, $row['notes']); + + /** Set piggy bank */ + $this->storePiggyEvent($journal, $row); + + /** Set tags */ + $this->storeTags($journal, $row['tags']); + + /** set all meta fields */ + $this->storeMetaFields($journal, $row); + + return $journal; + } + /** * This is a separate function because "findCurrency" will default to EUR and that may not be what we want. * @@ -345,17 +345,38 @@ class TransactionJournalFactory return null; } - return $this->currencyRepository->findCurrency( - $transaction['foreign_currency'], (int)$transaction['foreign_currency_id'], $transaction['foreign_currency_code'] - ); + return $this->currencyRepository->findCurrency((int)$transaction['foreign_currency_id'], $transaction['foreign_currency_code']); } + /** + * @param NullArrayObject $row + * + * @return string + */ + private function hashArray(NullArrayObject $row): string + { + $row['import_hash_v2'] = null; + $row['original_source'] = null; + $json = json_encode($row); + if (false === $json) { + $json = json_encode((string)microtime()); + } + $hash = hash('sha256', $json); + Log::debug(sprintf('The hash is: %s', $hash)); + + 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)); diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index e4a2dbe4ea..bb464dfba0 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -86,18 +86,24 @@ class GroupCollector implements GroupCollectorInterface $this->limit = 50; $this->page = 0; $this->fields = [ + # group 'transaction_groups.id as transaction_group_id', + 'transaction_groups.user_id as user_id', 'transaction_groups.created_at as created_at', 'transaction_groups.updated_at as updated_at', 'transaction_groups.title as transaction_group_title', + # journal 'transaction_journals.id as transaction_journal_id', 'transaction_journals.transaction_type_id', 'transaction_types.type as transaction_type_type', 'transaction_journals.description', 'transaction_journals.date', + + # source info (always present) 'source.id as source_transaction_id', 'source.account_id as source_account_id', + 'source.reconciled', # currency info: 'source.amount as amount', @@ -113,7 +119,8 @@ class GroupCollector implements GroupCollectorInterface 'foreign_currency.symbol as foreign_currency_symbol', 'foreign_currency.decimal_places as foreign_currency_decimal_places', - # destination account info: + # destination account info (always present) + #'destination.id as destination_transaction_id', // not interesting. 'destination.account_id as destination_account_id', ]; } @@ -368,6 +375,8 @@ class GroupCollector implements GroupCollectorInterface public function setTransactionGroup(TransactionGroup $transactionGroup): GroupCollectorInterface { $this->query->where('transaction_groups.id', $transactionGroup->id); + + return $this; } /** @@ -534,46 +543,48 @@ class GroupCollector implements GroupCollectorInterface private function parseArray(Collection $collection): Collection { $groups = []; - /** @var TransactionGroup $augumentedGroup */ - foreach ($collection as $augumentedGroup) { - $groupId = $augumentedGroup->transaction_group_id; + /** @var TransactionGroup $augmentedGroup */ + foreach ($collection as $augmentedGroup) { + $groupId = $augmentedGroup->transaction_group_id; if (!isset($groups[$groupId])) { // make new array $groupArray = [ - 'id' => $augumentedGroup->id, - 'title' => $augumentedGroup->title, + 'id' => $augmentedGroup->transaction_group_id, + 'user_id' => $augmentedGroup->user_id, + 'title' => $augmentedGroup->title, 'count' => 1, - 'sum' => $augumentedGroup->amount, - 'foreign_sum' => $augumentedGroup->foreign_amount ?? '0', + 'sum' => $augmentedGroup->amount, + 'foreign_sum' => $augmentedGroup->foreign_amount ?? '0', 'transactions' => [], ]; - $journalId = (int)$augumentedGroup->transaction_journal_id; - $groupArray['transactions'][$journalId] = $this->parseAugumentedGroup($augumentedGroup); + $journalId = (int)$augmentedGroup->transaction_journal_id; + $groupArray['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup); $groups[$groupId] = $groupArray; continue; } $groups[$groupId]['count']++; - $groups[$groupId]['sum'] = bcadd($augumentedGroup->amount, $groups[$groupId]['sum']); - $groups[$groupId]['foreign_sum'] = bcadd($augumentedGroup->foreign_amount ?? '0', $groups[$groupId]['foreign_sum']); - $journalId = (int)$augumentedGroup->transaction_journal_id; - $groups[$groupId]['transactions'][$journalId] = $this->parseAugumentedGroup($augumentedGroup); + $groups[$groupId]['sum'] = bcadd($augmentedGroup->amount, $groups[$groupId]['sum']); + $groups[$groupId]['foreign_sum'] = bcadd($augmentedGroup->foreign_amount ?? '0', $groups[$groupId]['foreign_sum']); + $journalId = (int)$augmentedGroup->transaction_journal_id; + $groups[$groupId]['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup); } return new Collection($groups); } /** - * @param TransactionGroup $augumentedGroup + * @param TransactionGroup $augmentedGroup * - * @throws Exception * @return array + * @throws Exception */ - private function parseAugumentedGroup(TransactionGroup $augumentedGroup): array + private function parseAugmentedGroup(TransactionGroup $augmentedGroup): array { - $result = $augumentedGroup->toArray(); + $result = $augmentedGroup->toArray(); $result['date'] = new Carbon($result['date']); $result['created_at'] = new Carbon($result['created_at']); $result['updated_at'] = new Carbon($result['updated_at']); + $result['reconciled'] = 1 === (int)$result['reconciled']; return $result; } diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index be04cca2ba..82eb6e1fd7 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -24,6 +24,7 @@ namespace FireflyIII\Http\Requests; use Carbon\Carbon; use Carbon\Exceptions\InvalidDateException; +use Exception; use Illuminate\Foundation\Http\FormRequest; use Log; @@ -36,6 +37,26 @@ use Log; */ class Request extends FormRequest { + /** + * @param $array + * + * @return array|null + */ + public function arrayFromValue($array): ?array + { + if (is_array($array)) { + return $array; + } + if (null === $array) { + return null; + } + if (is_string($array)) { + return explode(',', $array); + } + + return null; + } + /** * Return a boolean value. * @@ -60,11 +81,17 @@ class Request extends FormRequest * * @return bool */ - public function convertBoolean(string $value): bool + public function convertBoolean(?string $value): bool { + if (null === $value) { + return false; + } if ('true' === $value) { return true; } + if ('yes' === $value) { + return true; + } if (1 === $value) { return true; } @@ -78,6 +105,30 @@ class Request extends FormRequest return false; } + /** + * @param string|null $string + * + * @return Carbon|null + */ + public function dateFromValue(?string $string): ?Carbon + { + if (null === $string) { + return null; + } + if ('' === $string) { + return null; + } + try { + $carbon = new Carbon($string); + } catch (Exception $e) { + Log::debug(sprintf('Invalid date: %s: %s', $string, $e->getMessage())); + + return null; + } + + return $carbon; + } + /** * Return floating value. * @@ -107,6 +158,22 @@ class Request extends FormRequest return (int)$this->get($field); } + /** + * Parse to integer + * + * @param string|null $string + * + * @return int|null + */ + public function integerFromValue(?string $string): ?int + { + if (null === $string) { + return null; + } + + return (int)$string; + } + /** * Return string value. * @@ -121,12 +188,31 @@ class Request extends FormRequest return app('steam')->cleanString((string)($this->get($field) ?? '')); } + /** + * Parse and clean a string. + * + * @param string|null $string + * + * @return string|null + */ + public function stringFromValue(?string $string): ?string + { + if (null === $string) { + return null; + } + $result = app('steam')->cleanString($string); + + return '' === $result ? null : $result; + + } + /** * Return date or NULL. * * @param string $field * * @return Carbon|null + * @throws Exception */ protected function date(string $field): ?Carbon { diff --git a/app/Import/Storage/ImportArrayStorage.php b/app/Import/Storage/ImportArrayStorage.php index a533793bc6..42b631b2ec 100644 --- a/app/Import/Storage/ImportArrayStorage.php +++ b/app/Import/Storage/ImportArrayStorage.php @@ -220,12 +220,12 @@ class ImportArrayStorage */ private function getHash(array $transaction): string { - unset($transaction['importHashV2'], $transaction['original-source']); + unset($transaction['import_hash_v2'], $transaction['original_source']); $json = json_encode($transaction); if (false === $json) { // @codeCoverageIgnoreStart /** @noinspection ForgottenDebugOutputInspection */ - Log::error('Could not encode import array.', print_r($transaction, true)); + Log::error('Could not encode import array.', $transaction); throw new FireflyException('Could not encode import array. Please see the logs.'); // @codeCoverageIgnoreEnd } @@ -438,7 +438,7 @@ class ImportArrayStorage Log::warning(sprintf('Row #%d seems to be a duplicate entry and will be ignored.', $index)); continue; } - $transaction['importHashV2'] = $this->getHash($transaction); + $transaction['import_hash_v2'] = $this->getHash($transaction); $toStore[] = $transaction; } $count = \count($toStore); diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 110ccc2e64..e4f3f9b299 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -75,6 +75,8 @@ use FireflyIII\Validation\FireflyValidator; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; use Twig; +use Twig_Extension_Debug; +use TwigBridge\Extension\Loader\Functions; use Validator; /** @@ -98,19 +100,18 @@ class FireflyServiceProvider extends ServiceProvider } ); $config = app('config'); - //Twig::addExtension(new Functions($config)); - //Twig::addRuntimeLoader(new TransactionLoader); - //Twig::addRuntimeLoader(new AccountLoader); - //Twig::addRuntimeLoader(new TransactionJournalLoader); - //Twig::addRuntimeLoader(new TransactionGroupLoader); + Twig::addExtension(new Functions($config)); + Twig::addRuntimeLoader(new TransactionLoader); + Twig::addRuntimeLoader(new AccountLoader); + Twig::addRuntimeLoader(new TransactionJournalLoader); Twig::addExtension(new General); Twig::addExtension(new TransactionGroupTwig); - //Twig::addExtension(new Journal); + Twig::addExtension(new Journal); Twig::addExtension(new Translation); - //Twig::addExtension(new Transaction); - //Twig::addExtension(new Rule); + Twig::addExtension(new Transaction); + Twig::addExtension(new Rule); Twig::addExtension(new AmountFormat); - //Twig::addExtension(new Twig_Extension_Debug); + Twig::addExtension(new Twig_Extension_Debug); } /** diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 6167e25227..6ebd1563d6 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -89,21 +89,13 @@ class BillRepository implements BillRepositoryInterface /** * Find bill by parameters. * - * @param Bill|null $bill * @param int|null $billId * @param string|null $billName * * @return Bill|null */ - public function findBill(?Bill $bill, ?int $billId, ?string $billName): ?Bill + public function findBill( ?int $billId, ?string $billName): ?Bill { - Log::debug('Searching for bill information.'); - if ($bill instanceof Bill && $bill->user_id === $this->user->id) { - Log::debug(sprintf('Bill object in parameters, will return Bill #%d', $bill->id)); - - return $bill; - } - if (null !== $billId) { $searchResult = $this->find((int)$billId); if (null !== $searchResult) { diff --git a/app/Repositories/Bill/BillRepositoryInterface.php b/app/Repositories/Bill/BillRepositoryInterface.php index c883465fc3..fe6b6aa387 100644 --- a/app/Repositories/Bill/BillRepositoryInterface.php +++ b/app/Repositories/Bill/BillRepositoryInterface.php @@ -52,13 +52,12 @@ interface BillRepositoryInterface /** * Find bill by parameters. * - * @param Bill|null $bill * @param int|null $billId * @param string|null $billName * * @return Bill|null */ - public function findBill(?Bill $bill, ?int $billId, ?string $billName): ?Bill; + public function findBill(?int $billId, ?string $billName): ?Bill; /** * Find a bill by name. diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index 17653f258d..296092e7ca 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -195,8 +195,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @param string $currencyCode * - * @deprecated * @return TransactionCurrency|null + * @deprecated */ public function findByCodeNull(string $currencyCode): ?TransactionCurrency { @@ -221,8 +221,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @param string $currencyName * - * @deprecated * @return TransactionCurrency + * @deprecated */ public function findByNameNull(string $currencyName): ?TransactionCurrency { @@ -247,8 +247,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @param string $currencySymbol * - * @deprecated * @return TransactionCurrency + * @deprecated */ public function findBySymbolNull(string $currencySymbol): ?TransactionCurrency { @@ -258,25 +258,15 @@ class CurrencyRepository implements CurrencyRepositoryInterface /** * Find by object, ID or code. Returns user default or system default. * - * @param TransactionCurrency|null $currency - * @param int|null $currencyId - * @param string|null $currencyCode + * @param int|null $currencyId + * @param string|null $currencyCode * * @return TransactionCurrency|null */ - public function findCurrency(?TransactionCurrency $currency, ?int $currencyId, ?string $currencyCode): TransactionCurrency + public function findCurrency(?int $currencyId, ?string $currencyCode): TransactionCurrency { Log::debug('Now in findCurrency()'); - $result = null; - if (null !== $currency) { - Log::debug(sprintf('Parameters contain %s, will return this.', $currency->code)); - $result = $currency; - } - - if (null === $result) { - Log::debug(sprintf('Searching for currency with ID #%d...', $currencyId)); - $result = $this->find((int)$currencyId); - } + $result = $this->find((int)$currencyId); if (null === $result) { Log::debug(sprintf('Searching for currency with code %s...', $currencyCode)); $result = $this->findByCode((string)$currencyCode); @@ -304,8 +294,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface * * @param int $currencyId * - * @deprecated * @return TransactionCurrency|null + * @deprecated */ public function findNull(int $currencyId): ?TransactionCurrency { diff --git a/app/Repositories/Currency/CurrencyRepositoryInterface.php b/app/Repositories/Currency/CurrencyRepositoryInterface.php index 665ff6633c..1ceddc70c6 100644 --- a/app/Repositories/Currency/CurrencyRepositoryInterface.php +++ b/app/Repositories/Currency/CurrencyRepositoryInterface.php @@ -135,13 +135,12 @@ interface CurrencyRepositoryInterface /** * Find by object, ID or code. Returns user default or system default. * - * @param TransactionCurrency|null $currency * @param int|null $currencyId * @param string|null $currencyCode * * @return TransactionCurrency|null */ - public function findCurrency(?TransactionCurrency $currency, ?int $currencyId, ?string $currencyCode): TransactionCurrency; + public function findCurrency(?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 047dedfa61..9f0b5de822 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -26,6 +26,7 @@ use Carbon\Carbon; use DB; use Exception; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\TransactionGroupFactory; use FireflyIII\Factory\TransactionJournalFactory; use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Filter\InternalTransferFilter; @@ -175,7 +176,7 @@ class JournalRepository implements JournalRepositoryInterface $result = TransactionJournalMeta::withTrashed() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') ->where('hash', $hashOfHash) - ->where('name', 'importHashV2') + ->where('name', 'import_hash_v2') ->first(['journal_meta.*']); if (null === $result) { Log::debug('Result is null'); @@ -782,8 +783,8 @@ class JournalRepository implements JournalRepositoryInterface */ public function store(array $data): TransactionGroup { - /** @var TransactionJournalFactory $factory */ - $factory = app(TransactionJournalFactory::class); + /** @var TransactionGroupFactory $factory */ + $factory = app(TransactionGroupFactory::class); $factory->setUser($this->user); return $factory->create($data); diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php index 6a4204b24a..7d50d2eb47 100644 --- a/app/Support/Import/Placeholder/ImportTransaction.php +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -185,8 +185,8 @@ class ImportTransaction return; } - $meta = ['sepa-ct-id', 'sepa-ct-op', 'sepa-db', 'sepa-cc', 'sepa-country', 'sepa-batch-id', 'sepa-ep', 'sepa-ci', 'internal-reference', 'date-interest', - 'date-invoice', 'date-book', 'date-payment', 'date-process', 'date-due', 'original-source']; + $meta = ['sepa_ct_id', 'sepa_ct_op', 'sepa_db', 'sepa_cc', 'sepa_country', 'sepa_batch_id', 'sepa_ep', 'sepa_ci', 'internal_reference', 'date_interest', + 'date_invoice', 'date_book', 'date_payment', 'date_process', 'date_due', 'original_source']; Log::debug(sprintf('Now going to check role "%s".', $role)); if (\in_array($role, $meta, true)) { Log::debug(sprintf('Role "%s" is in allowed meta roles, so store its value "%s".', $role, $columnValue->getValue())); diff --git a/app/Support/Import/Routine/File/ImportableConverter.php b/app/Support/Import/Routine/File/ImportableConverter.php index 99c33680d6..afcdd29c2d 100644 --- a/app/Support/Import/Routine/File/ImportableConverter.php +++ b/app/Support/Import/Routine/File/ImportableConverter.php @@ -226,14 +226,14 @@ class ImportableConverter // all custom fields: 'internal_reference' => $importable->meta['internal-reference'] ?? null, - 'sepa-cc' => $importable->meta['sepa-cc'] ?? null, - 'sepa-ct-op' => $importable->meta['sepa-ct-op'] ?? null, - 'sepa-ct-id' => $importable->meta['sepa-ct-id'] ?? null, - 'sepa-db' => $importable->meta['sepa-db'] ?? null, - 'sepa-country' => $importable->meta['sepa-country'] ?? null, - 'sepa-ep' => $importable->meta['sepa-ep'] ?? null, - 'sepa-ci' => $importable->meta['sepa-ci'] ?? null, - 'sepa-batch-id' => $importable->meta['sepa-batch-id'] ?? null, + 'sepa_cc' => $importable->meta['sepa_cc'] ?? null, + 'sepa_ct_op' => $importable->meta['sepa_ct_op'] ?? null, + 'sepa_ct_id' => $importable->meta['sepa_ct_id'] ?? null, + 'sepa_db' => $importable->meta['sepa_db'] ?? null, + 'sepa_country' => $importable->meta['sepa_country'] ?? null, + 'sepa_ep' => $importable->meta['sepa_ep'] ?? null, + 'sepa_ci' => $importable->meta['sepa_ci'] ?? null, + 'sepa_batch_id' => $importable->meta['sepa_batch_id'] ?? null, 'interest_date' => $this->convertDateValue($importable->meta['date-interest'] ?? null), 'book_date' => $this->convertDateValue($importable->meta['date-book'] ?? null), 'process_date' => $this->convertDateValue($importable->meta['date-process'] ?? null), diff --git a/app/Transformers/TransactionGroupTransformer.php b/app/Transformers/TransactionGroupTransformer.php index abb1e3c5fb..5599772765 100644 --- a/app/Transformers/TransactionGroupTransformer.php +++ b/app/Transformers/TransactionGroupTransformer.php @@ -48,9 +48,9 @@ class TransactionGroupTransformer extends AbstractTransformer { $this->groupRepos = app(TransactionGroupRepositoryInterface::class); $this->metaFields = [ - 'sepa-cc', 'sepa-ct-op', 'sepa-ct-id', 'sepa-db', 'sepa-country', 'sepa-ep', - 'sepa-ci', 'sepa-batch-id', 'internal_reference', 'bunq_payment_id', 'importHashV2', - 'recurrence_id', 'external_id', 'original-source', + 'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_db', 'sepa_country', 'sepa_ep', + 'sepa_ci', 'sepa_batch_id', 'internal_reference', 'bunq_payment_id', 'import_hash_v2', + 'recurrence_id', 'external_id', 'original_source', ]; $this->metaDateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date']; @@ -72,6 +72,7 @@ class TransactionGroupTransformer extends AbstractTransformer 'id' => (int)$first['transaction_group_id'], 'created_at' => $first['created_at']->toAtomString(), 'updated_at' => $first['updated_at']->toAtomString(), + 'user' => (int)$data['user_id'], 'group_title' => $data['title'], 'transactions' => $this->transformTransactions($data), 'links' => [ @@ -114,57 +115,71 @@ class TransactionGroupTransformer extends AbstractTransformer $metaDateData = $this->groupRepos->getMetaDateFields((int)$row['transaction_journal_id'], $this->metaDateFields); $result[] = [ - 'transaction_journal_id' => $row['transaction_journal_id'], - 'description' => $row['description'], - 'date' => $row['date']->toAtomString(), - 'type' => $type, - 'reconciled' => $row['reconciled'], - 'source_id' => $row['source_account_id'], - 'source_name' => $row['source_account_name'], - 'source_iban' => $row['source_account_iban'], - 'source_type' => $row['source_account_type'], - 'destination_id' => $row['destination_account_id'], - 'destination_name' => $row['destination_account_name'], - 'destination_iban' => $row['destination_account_iban'], - 'destination_type' => $row['destination_account_type'], - 'amount' => $amount, - 'currency_id' => $row['currency_id'], - 'currency_code' => $row['currency_code'], - 'currency_symbol' => $row['currency_symbol'], - 'currency_decimal_places' => $row['currency_decimal_places'], - 'foreign_amount' => $foreignAmount, + 'user' => (int)$row['user_id'], + 'transaction_journal_id' => $row['transaction_journal_id'], + 'type' => strtolower($type), + 'date' => $row['date']->toAtomString(), + + 'currency_id' => $row['currency_id'], + 'currency_code' => $row['currency_code'], + 'currency_symbol' => $row['currency_symbol'], + 'currency_decimal_places' => $row['currency_decimal_places'], + 'foreign_currency_id' => $row['foreign_currency_id'], 'foreign_currency_code' => $row['foreign_currency_code'], 'foreign_currency_symbol' => $row['foreign_currency_symbol'], 'foreign_currency_decimal_places' => $row['foreign_currency_decimal_places'], - 'bill_id' => $row['bill_id'], - 'bill_name' => $row['bill_name'], - 'category_id' => $row['category_id'], - 'category_name' => $row['category_name'], - 'budget_id' => $row['budget_id'], - 'budget_name' => $row['budget_name'], - 'notes' => $this->groupRepos->getNoteText((int)$row['transaction_journal_id']), - 'tags' => $this->groupRepos->getTags((int)$row['transaction_journal_id']), - 'sepa_cc' => $metaFieldData['sepa-cc'], - 'sepa_ct_op' => $metaFieldData['sepa-ct-op'], - 'sepa_ct_id' => $metaFieldData['sepa-ct-id'], - 'sepa_db' => $metaFieldData['sepa-ddb'], - 'sepa_country' => $metaFieldData['sepa-country'], - 'sepa_ep' => $metaFieldData['sepa-ep'], - 'sepa_ci' => $metaFieldData['sepa-ci'], - 'sepa_batch_id' => $metaFieldData['sepa-batch-id'], - 'internal_reference' => $metaFieldData['internal_reference'], - 'bunq_payment_id' => $metaFieldData['bunq_payment_id'], - 'importHashV2' => $metaFieldData['importHashV2'], - 'recurrence_id' => $metaFieldData['recurrence_id'], - 'external_id' => $metaFieldData['external_id'], - 'original_source' => $metaFieldData['original-source'], - 'interest_date' => $metaDateData['interest_date'] ? $metaDateData['interest_date']->toAtomString() : null, - 'book_date' => $metaDateData['book_date'] ? $metaDateData['book_date']->toAtomString() : null, - 'process_date' => $metaDateData['process_date'] ? $metaDateData['process_date']->toAtomString() : null, - 'due_date' => $metaDateData['due_date'] ? $metaDateData['due_date']->toAtomString() : null, - 'payment_date' => $metaDateData['payment_date'] ? $metaDateData['payment_date']->toAtomString() : null, - 'invoice_date' => $metaDateData['invoice_date'] ? $metaDateData['invoice_date']->toAtomString() : null, + + 'amount' => $amount, + 'foreign_amount' => $foreignAmount, + + 'description' => $row['description'], + + 'source_id' => $row['source_account_id'], + 'source_name' => $row['source_account_name'], + 'source_iban' => $row['source_account_iban'], + 'source_type' => $row['source_account_type'], + + 'destination_id' => $row['destination_account_id'], + 'destination_name' => $row['destination_account_name'], + 'destination_iban' => $row['destination_account_iban'], + 'destination_type' => $row['destination_account_type'], + + 'budget_id' => $row['budget_id'], + 'budget_name' => $row['budget_name'], + + 'category_id' => $row['category_id'], + 'category_name' => $row['category_name'], + + 'bill_id' => $row['bill_id'], + 'bill_name' => $row['bill_name'], + + 'reconciled' => $row['reconciled'], + 'notes' => $this->groupRepos->getNoteText((int)$row['transaction_journal_id']), + 'tags' => $this->groupRepos->getTags((int)$row['transaction_journal_id']), + + 'internal_reference' => $metaFieldData['internal_reference'], + 'external_id' => $metaFieldData['external_id'], + 'original_source' => $metaFieldData['original_source'], + 'recurrence_id' => $metaFieldData['recurrence_id'], + 'bunq_payment_id' => $metaFieldData['bunq_payment_id'], + 'import_hash_v2' => $metaFieldData['import_hash_v2'], + + 'sepa_cc' => $metaFieldData['sepa_cc'], + 'sepa_ct_op' => $metaFieldData['sepa_ct_op'], + 'sepa_ct_id' => $metaFieldData['sepa_ct_id'], + 'sepa_db' => $metaFieldData['sepa_ddb'], + 'sepa_country' => $metaFieldData['sepa_country'], + 'sepa_ep' => $metaFieldData['sepa_ep'], + 'sepa_ci' => $metaFieldData['sepa_ci'], + 'sepa_batch_id' => $metaFieldData['sepa_batch_id'], + + 'interest_date' => $metaDateData['interest_date'] ? $metaDateData['interest_date']->toAtomString() : null, + 'book_date' => $metaDateData['book_date'] ? $metaDateData['book_date']->toAtomString() : null, + 'process_date' => $metaDateData['process_date'] ? $metaDateData['process_date']->toAtomString() : null, + 'due_date' => $metaDateData['due_date'] ? $metaDateData['due_date']->toAtomString() : null, + 'payment_date' => $metaDateData['payment_date'] ? $metaDateData['payment_date']->toAtomString() : null, + 'invoice_date' => $metaDateData['invoice_date'] ? $metaDateData['invoice_date']->toAtomString() : null, ]; } diff --git a/app/Transformers/TransactionTransformer.php b/app/Transformers/TransactionTransformer.php index 3f3b42a80f..4750ec3a44 100644 --- a/app/Transformers/TransactionTransformer.php +++ b/app/Transformers/TransactionTransformer.php @@ -100,14 +100,14 @@ class TransactionTransformer extends AbstractTransformer 'budget_id' => $budget['budget_id'], 'budget_name' => $budget['budget_name'], 'notes' => $notes, - 'sepa_cc' => $this->repository->getMetaField($journal, 'sepa-cc'), - 'sepa_ct_op' => $this->repository->getMetaField($journal, 'sepa-ct-op'), - 'sepa_ct_id' => $this->repository->getMetaField($journal, 'sepa-ct-ud'), - 'sepa_db' => $this->repository->getMetaField($journal, 'sepa-db'), - 'sepa_country' => $this->repository->getMetaField($journal, 'sepa-country'), - 'sepa_ep' => $this->repository->getMetaField($journal, 'sepa-ep'), - 'sepa_ci' => $this->repository->getMetaField($journal, 'sepa-ci'), - 'sepa_batch_id' => $this->repository->getMetaField($journal, 'sepa-batch-id'), + 'sepa_cc' => $this->repository->getMetaField($journal, 'sepa_cc'), + 'sepa_ct_op' => $this->repository->getMetaField($journal, 'sepa_ct_op'), + 'sepa_ct_id' => $this->repository->getMetaField($journal, 'sepa_ct_ud'), + 'sepa_db' => $this->repository->getMetaField($journal, 'sepa_db'), + 'sepa_country' => $this->repository->getMetaField($journal, 'sepa_country'), + 'sepa_ep' => $this->repository->getMetaField($journal, 'sepa_ep'), + 'sepa_ci' => $this->repository->getMetaField($journal, 'sepa_ci'), + 'sepa_batch_id' => $this->repository->getMetaField($journal, 'sepa_batch_id'), 'interest_date' => $this->repository->getMetaDateString($journal, 'interest_date'), 'book_date' => $this->repository->getMetaDateString($journal, 'book_date'), 'process_date' => $this->repository->getMetaDateString($journal, 'process_date'), @@ -116,7 +116,7 @@ class TransactionTransformer extends AbstractTransformer 'invoice_date' => $this->repository->getMetaDateString($journal, 'invoice_date'), 'internal_reference' => $this->repository->getMetaField($journal, 'internal_reference'), 'bunq_payment_id' => $this->repository->getMetaField($journal, 'bunq_payment_id'), - 'importHashV2' => $this->repository->getMetaField($journal, 'importHashV2'), + 'import_hash_v2' => $this->repository->getMetaField($journal, 'import_hash_v2'), 'recurrence_id' => (int)$this->repository->getMetaField($journal, 'recurrence_id'), 'external_id' => $this->repository->getMetaField($journal, 'external_id'), 'original_source' => $this->repository->getMetaField($journal, 'original-source'), diff --git a/app/Validation/AccountValidator.php b/app/Validation/AccountValidator.php new file mode 100644 index 0000000000..a0834994be --- /dev/null +++ b/app/Validation/AccountValidator.php @@ -0,0 +1,415 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Validation; + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use Log; + +/** + * Class AccountValidator + */ +class AccountValidator +{ + /** @var bool */ + public $createMode; + /** @var string */ + public $destError; + /** @var Account */ + public $destination; + /** @var Account */ + public $source; + /** @var string */ + public $sourceError; + /** @var AccountRepositoryInterface */ + private $accountRepository; + /** @var array */ + private $combinations; + /** @var string */ + private $transactionType; + + /** + * AccountValidator constructor. + */ + public function __construct() + { + $this->createMode = false; + $this->destError = 'No error yet.'; + $this->sourceError = 'No error yet.'; + $this->combinations = config('firefly.source_dests'); + /** @var AccountRepositoryInterface accountRepository */ + $this->accountRepository = app(AccountRepositoryInterface::class); + } + + /** + * @param string $transactionType + */ + public function setTransactionType(string $transactionType): void + { + $this->transactionType = ucfirst($transactionType); + } + + /** + * @param int|null $destinationId + * @param $destinationName + * + * @return bool + */ + public function validateDestination(?int $destinationId, $destinationName): bool + { + + Log::debug(sprintf('Now in AccountValidator::validateDestination(%d, "%s")', $destinationId, $destinationName)); + if (null === $this->source) { + Log::error('Source is NULL'); + $this->destError = 'No source account validation has taken place yet. Please do this first or overrule the object.'; + + return false; + } + switch ($this->transactionType) { + default: + $this->destError = sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType); + Log::error(sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType)); + + $result = false; + break; + + case TransactionType::WITHDRAWAL: + $result = $this->validateWithdrawalDestination($destinationId, $destinationName); + break; + case TransactionType::DEPOSIT: + $result = $this->validateDepositDestination($destinationId, $destinationName); + break; + case TransactionType::TRANSFER: + $result = $this->validateTransferDestination($destinationId, $destinationName); + break; + //case TransactionType::OPENING_BALANCE: + //case TransactionType::RECONCILIATION: + // die(sprintf('Cannot handle type "%s"', $this->transactionType)); + } + + return $result; + } + + /** + * @param int|null $accountId + * @param string|null $accountName + * + * @return bool + */ + public function validateSource(?int $accountId, ?string $accountName): bool + { + switch ($this->transactionType) { + default: + $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; + case TransactionType::WITHDRAWAL: + $result = $this->validateWithdrawalSource($accountId, $accountName); + break; + case TransactionType::DEPOSIT: + $result = $this->validateDepositSource($accountId, $accountName); + break; + case TransactionType::TRANSFER: + $result = $this->validateTransferSource($accountId, $accountName); + break; + //case TransactionType::OPENING_BALANCE: + //case TransactionType::RECONCILIATION: + // die(sprintf('Cannot handle type "%s"', $this->transactionType)); + } + + return $result; + } + + /** + * @param string $accountType + * + * @return bool + */ + private function canCreateType(string $accountType): bool + { + $result = false; + switch ($accountType) { + default: + Log::error(sprintf('AccountValidator::validateSource cannot handle "%s".', $this->transactionType)); + break; + case AccountType::ASSET: + case AccountType::LOAN: + case AccountType::MORTGAGE: + case AccountType::DEBT: + $result = false; + break; + case AccountType::EXPENSE: + case AccountType::REVENUE: + $result = true; + break; + } + + return $result; + } + + /** + * @param array $accountTypes + * + * @return bool + */ + private function canCreateTypes(array $accountTypes): bool + { + /** @var string $accountType */ + foreach ($accountTypes as $accountType) { + if ($this->canCreateType($accountType)) { + return true; + } + } + + return false; + } + + /** + * @param array $validTypes + * @param int|null $accountId + * @param string|null $accountName + * + * @return Account|null + */ + private function findExistingAccount(array $validTypes, int $accountId, string $accountName): ?Account + { + $result = null; + + // find by ID + if ($accountId > 0) { + $first = $this->accountRepository->findNull($accountId); + if ((null !== $first) && in_array($first->accountType->type, $validTypes, true)) { + $result = $first; + } + } + + // find by name: + if (null === $result && '' !== $accountName) { + $second = $this->accountRepository->findByName($accountName, $validTypes); + if (null !== $second) { + $result = $second; + } + } + + return $result; + } + + /** + * @param int|null $accountId + * @param $accountName + * + * @return bool + */ + private function validateDepositDestination(?int $accountId, $accountName): bool + { + $result = null; + Log::debug(sprintf('Now in validateDepositDestination(%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)) { + // if both values are NULL we return false, + // because the destination of a deposit can't be created. + $this->destError = (string)trans('validation.deposit_dest_need_data'); + Log::error('Both values are NULL, cant create deposit destination.'); + $result = false; + } + // 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; + } + + if (null === $result) { + // otherwise try to find the account: + $search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName); + if (null === $search) { + $this->destError = (string)trans('validation.deposit_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); + $result = false; + } + if (null !== $search) { + $this->destination = $search; + $result = true; + } + } + $result = $result ?? false; + + return $result; + } + + /** + * @param int|null $accountId + * @param string|null $accountName + * + * @return bool + */ + private function validateDepositSource(?int $accountId, ?string $accountName): bool + { + $result = null; + // source can be any of the following types. + $validTypes = array_keys($this->combinations[$this->transactionType]); + if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) { + // if both values are NULL return false, + // because the source of a deposit can't be created. + // (this never happens). + $this->sourceError = (string)trans('validation.deposit_source_need_data'); + $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; + } + $result = $result ?? false; + + // don't expect to end up here: + return $result; + } + + /** + * @param int|null $accountId + * @param $accountName + * + * @return bool + */ + private function validateTransferDestination(?int $accountId, $accountName): bool + { + Log::debug(sprintf('Now in validateTransferDestination(%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)) { + // if both values are NULL we return false, + // because the destination of a transfer can't be created. + $this->destError = (string)trans('validation.transfer_dest_need_data'); + Log::error('Both values are NULL, cant create transfer destination.'); + + return false; + } + + // otherwise try to find the account: + $search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName); + if (null === $search) { + $this->destError = (string)trans('validation.transfer_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); + + return false; + } + $this->destination = $search; + + return true; + } + + /** + * @param int|null $accountId + * @param string|null $accountName + * + * @return bool + */ + private function validateTransferSource(?int $accountId, ?string $accountName): bool + { + // source can be any of the following types. + $validTypes = array_keys($this->combinations[$this->transactionType]); + if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) { + // if both values are NULL we return false, + // because the source of a withdrawal can't be created. + $this->sourceError = (string)trans('validation.transfer_source_need_data'); + + return false; + } + + // otherwise try to find the account: + $search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName); + if (null === $search) { + $this->sourceError = (string)trans('validation.transfer_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + + return false; + } + $this->source = $search; + + return true; + } + + /** + * @param int|null $accountId + * @param string|null $accountName + * + * @return bool + */ + private function validateWithdrawalDestination(?int $accountId, ?string $accountName): bool + { + // 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)) { + // if both values are NULL return false, + // because the destination of a withdrawal can never be created automatically. + $this->destError = (string)trans('validation.withdrawal_dest_need_data'); + + return false; + } + + // if the account can be created anyway don't need to search. + if (true === $this->canCreateTypes($validTypes)) { + + return true; + } + // don't expect to end up here: + return false; + } + + /** + * @param int|null $accountId + * @param string|null $accountName + * + * @return bool + */ + private function validateWithdrawalSource(?int $accountId, ?string $accountName): bool + { + // source can be any of the following types. + $validTypes = array_keys($this->combinations[$this->transactionType]); + if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) { + // if both values are NULL we return false, + // because the source of a withdrawal can't be created. + $this->sourceError = (string)trans('validation.withdrawal_source_need_data'); + + return false; + } + + // otherwise try to find the account: + $search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName); + if (null === $search) { + $this->sourceError = (string)trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + + return false; + } + $this->source = $search; + + return true; + } + + +} \ No newline at end of file diff --git a/app/Validation/TransactionValidation.php b/app/Validation/TransactionValidation.php index 4f46b3fc1e..c122b5ff48 100644 --- a/app/Validation/TransactionValidation.php +++ b/app/Validation/TransactionValidation.php @@ -23,82 +23,59 @@ declare(strict_types=1); namespace FireflyIII\Validation; -use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; -use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\User; use Illuminate\Validation\Validator; -use Log; /** * Trait TransactionValidation */ trait TransactionValidation { + /** * Validates the given account information. Switches on given transaction type. * * @param Validator $validator + * + * @throws \FireflyIII\Exceptions\FireflyException */ public function validateAccountInformation(Validator $validator): void { - $data = $validator->getData(); - $transactions = $data['transactions'] ?? []; - $idField = 'description'; - $transactionType = $data['type'] ?? 'invalid'; - // get transaction type: - if (!isset($data['type'])) { - // the journal may exist in the request: - /** @var Transaction $transaction */ - $transaction = $this->route()->parameter('transaction'); - if (null !== $transaction) { - $transactionType = strtolower($transaction->transactionJournal->transactionType->type); - } - } + $data = $validator->getData(); + $transactions = $data['transactions'] ?? []; + + /** @var AccountValidator $accountValidator */ + $accountValidator = app(AccountValidator::class); + foreach ($transactions as $index => $transaction) { - $sourceId = isset($transaction['source_id']) ? (int)$transaction['source_id'] : null; - $sourceName = $transaction['source_name'] ?? null; - $destinationId = isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null; - $destinationName = $transaction['destination_name'] ?? null; - $sourceAccount = null; - $destinationAccount = null; - switch ($transactionType) { - case 'withdrawal': - $idField = 'transactions.' . $index . '.source_id'; - $nameField = 'transactions.' . $index . '.source_name'; - $sourceAccount = $this->assetAccountExists($validator, $sourceId, $sourceName, $idField, $nameField); - $idField = 'transactions.' . $index . '.destination_id'; - $destinationAccount = $this->opposingAccountExists($validator, AccountType::EXPENSE, $destinationId, $destinationName, $idField); - break; - case 'deposit': - $idField = 'transactions.' . $index . '.source_id'; - $sourceAccount = $this->opposingAccountExists($validator, AccountType::REVENUE, $sourceId, $sourceName, $idField); + $transactionType = $transaction['type'] ?? 'invalid'; + $accountValidator->setTransactionType($transactionType); - $idField = 'transactions.' . $index . '.destination_id'; - $nameField = 'transactions.' . $index . '.destination_name'; - $destinationAccount = $this->assetAccountExists($validator, $destinationId, $destinationName, $idField, $nameField); - break; - case 'transfer': - $idField = 'transactions.' . $index . '.source_id'; - $nameField = 'transactions.' . $index . '.source_name'; - $sourceAccount = $this->assetAccountExists($validator, $sourceId, $sourceName, $idField, $nameField); + // validate source account. + $sourceId = isset($transaction['source_id']) ? (int)$transaction['source_id'] : null; + $sourceName = $transaction['source_name'] ?? null; + $validSource = $accountValidator->validateSource($sourceId, $sourceName); - $idField = 'transactions.' . $index . '.destination_id'; - $nameField = 'transactions.' . $index . '.destination_name'; - $destinationAccount = $this->assetAccountExists($validator, $destinationId, $destinationName, $idField, $nameField); - break; - default: - $validator->errors()->add($idField, (string)trans('validation.invalid_account_info')); - - return; + // do something with result: + if (false === $validSource) { + $validator->errors()->add(sprintf('transactions.%d.source_id', $index), $accountValidator->sourceError); + $validator->errors()->add(sprintf('transactions.%d.source_name', $index), $accountValidator->sourceError); + return; } - // add some errors in case of same account submitted: - if (null !== $sourceAccount && null !== $destinationAccount && $sourceAccount->id === $destinationAccount->id) { - $validator->errors()->add($idField, (string)trans('validation.source_equals_destination')); + // validate destination account + $destinationId = isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null; + $destinationName = $transaction['destination_name'] ?? null; + $validDestination = $accountValidator->validateDestination($destinationId, $destinationName); + // do something with result: + if (false === $validDestination) { + $validator->errors()->add(sprintf('transactions.%d.destination_id', $index), $accountValidator->destError); + $validator->errors()->add(sprintf('transactions.%d.destination_name', $index), $accountValidator->destError); + + return; } + } } @@ -110,18 +87,17 @@ trait TransactionValidation */ public function validateDescriptions(Validator $validator): void { - $data = $validator->getData(); - $transactions = $data['transactions'] ?? []; - $journalDescription = (string)($data['description'] ?? null); - $validDescriptions = 0; + $data = $validator->getData(); + $transactions = $data['transactions'] ?? []; + $validDescriptions = 0; foreach ($transactions as $index => $transaction) { if ('' !== (string)($transaction['description'] ?? null)) { $validDescriptions++; } } - // no valid descriptions and empty journal description? error. - if (0 === $validDescriptions && '' === $journalDescription) { + // no valid descriptions? + if (0 === $validDescriptions) { $validator->errors()->add('description', (string)trans('validation.filled', ['attribute' => (string)trans('validation.attributes.description')])); } } @@ -149,21 +125,15 @@ trait TransactionValidation } /** - * Adds an error to the validator when any transaction descriptions are equal to the journal description. - * * @param Validator $validator */ - public function validateJournalDescription(Validator $validator): void + public function validateGroupDescription(Validator $validator): void { - $data = $validator->getData(); - $transactions = $data['transactions'] ?? []; - $journalDescription = (string)($data['description'] ?? null); - foreach ($transactions as $index => $transaction) { - $description = (string)($transaction['description'] ?? null); - // description cannot be equal to journal description. - if ($description === $journalDescription) { - $validator->errors()->add('transactions.' . $index . '.description', (string)trans('validation.equal_description')); - } + $data = $validator->getData(); + $transactions = $data['transactions'] ?? []; + $groupTitle = $data['group_title'] ?? ''; + if ('' === $groupTitle && \count($transactions) > 1) { + $validator->errors()->add('group_title', (string)trans('validation.group_title_mandatory')); } } @@ -239,126 +209,129 @@ trait TransactionValidation } /** - * Adds an error to the validator when the user submits a split transaction (more than 1 transactions) - * but does not give them a description. + * All types of splits must be equal. * * @param Validator $validator */ - public function validateSplitDescriptions(Validator $validator): void + public function validateTransactionTypes(Validator $validator): void { $data = $validator->getData(); $transactions = $data['transactions'] ?? []; + $types = []; foreach ($transactions as $index => $transaction) { - $description = (string)($transaction['description'] ?? null); - // filled description is mandatory for split transactions. - if ('' === $description && \count($transactions) > 1) { - $validator->errors()->add( - 'transactions.' . $index . '.description', - (string)trans('validation.filled', ['attribute' => (string)trans('validation.attributes.transaction_description')]) - ); - } + $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; + } + $first = $unique[0] ?? 'invalid'; + if ('invalid' === $first) { + $validator->errors()->add('transactions.0.type', (string)trans('validation.invalid_transaction_type')); } } - /** - * Throws an error when this asset account is invalid. - * - * @noinspection MoreThanThreeArgumentsInspection - * - * @param Validator $validator - * @param int|null $accountId - * @param null|string $accountName - * @param string $idField - * @param string $nameField - * - * @return null|Account - */ - protected function assetAccountExists(Validator $validator, ?int $accountId, ?string $accountName, string $idField, string $nameField): ?Account - { - /** @var User $admin */ - $admin = auth()->user(); - $accountId = (int)$accountId; - $accountName = (string)$accountName; - // both empty? hard exit. - if ($accountId < 1 && '' === $accountName) { - $validator->errors()->add($idField, (string)trans('validation.filled', ['attribute' => $idField])); - - return null; - } - // ID belongs to user and is asset account: - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($admin); - $set = $repository->getAccountsById([$accountId]); - Log::debug(sprintf('Count of accounts found by ID %d is: %d', $accountId, $set->count())); - if (1 === $set->count()) { - /** @var Account $first */ - $first = $set->first(); - if ($first->accountType->type !== AccountType::ASSET) { - $validator->errors()->add($idField, (string)trans('validation.belongs_user')); - - return null; - } - - // we ignore the account name at this point. - return $first; - } - - $account = $repository->findByName($accountName, [AccountType::ASSET]); - if (null === $account) { - $validator->errors()->add($nameField, (string)trans('validation.belongs_user')); - - return null; - } - - return $account; - } - - /** - * Throws an error when the given opposing account (of type $type) is invalid. - * Empty data is allowed, system will default to cash. - * - * @noinspection MoreThanThreeArgumentsInspection - * - * @param Validator $validator - * @param string $type - * @param int|null $accountId - * @param null|string $accountName - * @param string $idField - * - * @return null|Account - */ - protected function opposingAccountExists(Validator $validator, string $type, ?int $accountId, ?string $accountName, string $idField): ?Account - { - /** @var User $admin */ - $admin = auth()->user(); - $accountId = (int)$accountId; - $accountName = (string)$accountName; - // both empty? done! - if ($accountId < 1 && '' === $accountName) { - return null; - } - if (0 !== $accountId) { - // ID belongs to user and is $type account: - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($admin); - $set = $repository->getAccountsById([$accountId]); - if (1 === $set->count()) { - /** @var Account $first */ - $first = $set->first(); - if ($first->accountType->type !== $type) { - $validator->errors()->add($idField, (string)trans('validation.belongs_user')); - - return null; - } - - // we ignore the account name at this point. - return $first; - } - } - - // not having an opposing account by this name is NOT a problem. - return null; - } + // /** + // * Throws an error when this asset account is invalid. + // * + // * @noinspection MoreThanThreeArgumentsInspection + // * + // * @param Validator $validator + // * @param int|null $accountId + // * @param null|string $accountName + // * @param string $idField + // * @param string $nameField + // * + // * @return null|Account + // */ + // protected function assetAccountExists(Validator $validator, ?int $accountId, ?string $accountName, string $idField, string $nameField): ?Account + // { + // /** @var User $admin */ + // $admin = auth()->user(); + // $accountId = (int)$accountId; + // $accountName = (string)$accountName; + // // both empty? hard exit. + // if ($accountId < 1 && '' === $accountName) { + // $validator->errors()->add($idField, (string)trans('validation.filled', ['attribute' => $idField])); + // + // return null; + // } + // // ID belongs to user and is asset account: + // /** @var AccountRepositoryInterface $repository */ + // $repository = app(AccountRepositoryInterface::class); + // $repository->setUser($admin); + // $set = $repository->getAccountsById([$accountId]); + // Log::debug(sprintf('Count of accounts found by ID %d is: %d', $accountId, $set->count())); + // if (1 === $set->count()) { + // /** @var Account $first */ + // $first = $set->first(); + // if ($first->accountType->type !== AccountType::ASSET) { + // $validator->errors()->add($idField, (string)trans('validation.belongs_user')); + // + // return null; + // } + // + // // we ignore the account name at this point. + // return $first; + // } + // + // $account = $repository->findByName($accountName, [AccountType::ASSET]); + // if (null === $account) { + // $validator->errors()->add($nameField, (string)trans('validation.belongs_user')); + // + // return null; + // } + // + // return $account; + // } + // + // /** + // * Throws an error when the given opposing account (of type $type) is invalid. + // * Empty data is allowed, system will default to cash. + // * + // * @noinspection MoreThanThreeArgumentsInspection + // * + // * @param Validator $validator + // * @param string $type + // * @param int|null $accountId + // * @param null|string $accountName + // * @param string $idField + // * + // * @return null|Account + // */ + // protected function opposingAccountExists(Validator $validator, string $type, ?int $accountId, ?string $accountName, string $idField): ?Account + // { + // /** @var User $admin */ + // $admin = auth()->user(); + // $accountId = (int)$accountId; + // $accountName = (string)$accountName; + // // both empty? done! + // if ($accountId < 1 && '' === $accountName) { + // return null; + // } + // if (0 !== $accountId) { + // // ID belongs to user and is $type account: + // /** @var AccountRepositoryInterface $repository */ + // $repository = app(AccountRepositoryInterface::class); + // $repository->setUser($admin); + // $set = $repository->getAccountsById([$accountId]); + // if (1 === $set->count()) { + // /** @var Account $first */ + // $first = $set->first(); + // if ($first->accountType->type !== $type) { + // $validator->errors()->add($idField, (string)trans('validation.belongs_user')); + // + // return null; + // } + // + // // we ignore the account name at this point. + // return $first; + // } + // } + // + // // not having an opposing account by this name is NOT a problem. + // return null; + // } } diff --git a/config/csv.php b/config/csv.php index 02303455ee..45a24d113f 100644 --- a/config/csv.php +++ b/config/csv.php @@ -359,56 +359,56 @@ return [ ], // SEPA end to end ID - 'sepa-ct-id' => [ + 'sepa_ct_id' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', 'field' => 'sepa_ct_id', ], // SEPA opposing account identifier - 'sepa-ct-op' => [ + 'sepa_ct_op' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', 'field' => 'sepa_ct_op', ], // SEPA Direct Debit Mandate Identifier - 'sepa-db' => [ + 'sepa_db' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', 'field' => 'sepa_db', ], // SEPA clearing code - 'sepa-cc' => [ + 'sepa_cc' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', 'field' => 'sepa_cc', ], // SEPA country - 'sepa-country' => [ + 'sepa_country' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', 'field' => 'sepa_country', ], // SEPA external purpose - 'sepa-ep' => [ + 'sepa_ep' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', 'field' => 'sepa_ep', ], // SEPA creditor identifier - 'sepa-ci' => [ + 'sepa_ci' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', 'field' => 'sepa_ci', ], // SEPA Batch ID - 'sepa-batch-id' => [ + 'sepa_batch_id' => [ 'mappable' => false, 'pre-process-map' => false, 'converter' => 'Description', diff --git a/resources/lang/en_US/import.php b/resources/lang/en_US/import.php index 3f19501654..95af81f10d 100644 --- a/resources/lang/en_US/import.php +++ b/resources/lang/en_US/import.php @@ -291,14 +291,14 @@ return [ 'column_rabo-debit-credit' => 'Rabobank specific debit/credit indicator', 'column_ing-debit-credit' => 'ING specific debit/credit indicator', 'column_generic-debit-credit' => 'Generic bank debit/credit indicator', - 'column_sepa-ct-id' => 'SEPA end-to-end Identifier', - 'column_sepa-ct-op' => 'SEPA Opposing Account Identifier', - 'column_sepa-db' => 'SEPA Mandate Identifier', - 'column_sepa-cc' => 'SEPA Clearing Code', - 'column_sepa-ci' => 'SEPA Creditor Identifier', - 'column_sepa-ep' => 'SEPA External Purpose', - 'column_sepa-country' => 'SEPA Country Code', - 'column_sepa-batch-id' => 'SEPA Batch ID', + 'column_sepa_ct_id' => 'SEPA end-to-end Identifier', + 'column_sepa_ct_op' => 'SEPA Opposing Account Identifier', + 'column_sepa_db' => 'SEPA Mandate Identifier', + 'column_sepa_cc' => 'SEPA Clearing Code', + 'column_sepa_ci' => 'SEPA Creditor Identifier', + 'column_sepa_ep' => 'SEPA External Purpose', + 'column_sepa_country' => 'SEPA Country Code', + 'column_sepa_batch_id' => 'SEPA Batch ID', 'column_tags-comma' => 'Tags (comma separated)', 'column_tags-space' => 'Tags (space separated)', 'column_account-number' => 'Asset account (account number)', diff --git a/resources/lang/en_US/list.php b/resources/lang/en_US/list.php index 4a50b795c8..9224f259ad 100644 --- a/resources/lang/en_US/list.php +++ b/resources/lang/en_US/list.php @@ -106,14 +106,14 @@ return [ 'account_on_spectre' => 'Account (Spectre)', 'account_on_ynab' => 'Account (YNAB)', 'do_import' => 'Import from this account', - 'sepa-ct-id' => 'SEPA End to End Identifier', - 'sepa-ct-op' => 'SEPA Opposing Account Identifier', - 'sepa-db' => 'SEPA Mandate Identifier', - 'sepa-country' => 'SEPA Country', - 'sepa-cc' => 'SEPA Clearing Code', - 'sepa-ep' => 'SEPA External Purpose', - 'sepa-ci' => 'SEPA Creditor Identifier', - 'sepa-batch-id' => 'SEPA Batch ID', + 'sepa_ct_id' => 'SEPA End to End Identifier', + 'sepa_ct_op' => 'SEPA Opposing Account Identifier', + 'sepa_db' => 'SEPA Mandate Identifier', + 'sepa_country' => 'SEPA Country', + 'sepa_cc' => 'SEPA Clearing Code', + 'sepa_ep' => 'SEPA External Purpose', + 'sepa_ci' => 'SEPA Creditor Identifier', + 'sepa_batch_id' => 'SEPA Batch ID', 'external_id' => 'External ID', 'account_at_bunq' => 'Account with bunq', 'file_name' => 'File name', diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 6bbd33b028..11b57b1d6c 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -33,9 +33,12 @@ return [ 'rule_trigger_value' => 'This value is invalid for the selected trigger.', 'rule_action_value' => 'This value is invalid for the selected action.', 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', - 'file_attached' => 'Succesfully uploaded file ":name".', + 'file_attached' => 'Successfully uploaded file ":name".', 'must_exist' => 'The ID in field :attribute does not exist in the database.', 'all_accounts_equal' => 'All accounts in this field must be equal.', + 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', + 'transaction_types_equal' => 'All splits must be of the same type.', + 'invalid_transaction_type' => 'Invalid transaction type.', 'invalid_selection' => 'Your selection is invalid.', 'belongs_user' => 'This value is invalid for this field.', 'at_least_one_transaction' => 'Need at least one transaction.', @@ -164,4 +167,19 @@ return [ 'rule-trigger.4' => 'rule trigger #4', 'rule-trigger.5' => 'rule trigger #5', ], + + // validation of accounts: + 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'withdrawal_source_bad_data' => 'Could not find a valid source account when searching for ID ":id" or name ":name".', + 'withdrawal_dest_need_data' => 'Need to get a valid destination account ID and/or valid destination account name to continue.', + + 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'deposit_source_bad_data' => 'Could not find a valid source account when searching for ID ":id" or name ":name".', + 'deposit_dest_need_data' => 'Need to get a valid destination account ID and/or valid destination account name to continue.', + 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + + 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + '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".', ]; diff --git a/resources/views/v1/list/groups-tiny.twig b/resources/views/v1/list/groups-tiny.twig index ff782fb1e0..149a3669b3 100644 --- a/resources/views/v1/list/groups-tiny.twig +++ b/resources/views/v1/list/groups-tiny.twig @@ -1,6 +1,6 @@
{% for group in groups %} - + {% for transaction in group.transactions %} {{ transaction.description }} diff --git a/resources/views/v1/list/journals-tiny.twig b/resources/views/v1/list/journals-tiny.twig index e78b721bcb..47715f955c 100644 --- a/resources/views/v1/list/journals-tiny.twig +++ b/resources/views/v1/list/journals-tiny.twig @@ -1,19 +1 @@ -

DO NOT USE ME

-{#
-#} \ No newline at end of file +

DO NOT USE ME

\ No newline at end of file diff --git a/resources/views/v1/list/journals.twig b/resources/views/v1/list/journals.twig index fa2440e856..9a1d5d1837 100644 --- a/resources/views/v1/list/journals.twig +++ b/resources/views/v1/list/journals.twig @@ -1,3 +1,7 @@ +

DO NOT USE

+ + +{# {{ transactions.render|raw }} @@ -10,17 +14,14 @@ - {# Hide budgets? #} {% if not hideBudgets %} {% endif %} - {# Hide categories? #} {% if not hideCategories %} {% endif %} - {# Hide bills? #} {% if not hideBills %} {% endif %} @@ -75,3 +76,4 @@ var edit_bulk_selected_txt = "{{ trans('firefly.bulk_edit')|escape('js') }}"; var delete_selected_txt = "{{ trans('firefly.delete')|escape('js') }}"; +#} \ No newline at end of file diff --git a/resources/views/v1/transactions/show.twig b/resources/views/v1/transactions/show.twig index 611df36916..dcf934aa4e 100644 --- a/resources/views/v1/transactions/show.twig +++ b/resources/views/v1/transactions/show.twig @@ -214,7 +214,7 @@ {% endif %} {% endfor %} {# all other meta values #} - {% for metaField in ['external_id','bunq_payment_id','internal_reference','sepa-batch-id','sepa-ct-id','sepa-ct-op','sepa-db','sepa-country','sepa-cc','sepa-ep','sepa-ci'] %} + {% for metaField in ['external_id','bunq_payment_id','internal_reference','sepa_batch_id','sepa_ct_id','sepa_ct_op','sepa_db','sepa_country','sepa_cc','sepa_ep','sepa_ci'] %} {% if journalHasMeta(journal, metaField) %} diff --git a/routes/web.php b/routes/web.php index f39f6b1d76..56576dc00f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,23 +21,17 @@ declare(strict_types=1); - Route::group( ['namespace' => 'FireflyIII\Http\Controllers\System', 'as' => 'installer.', 'prefix' => 'install'], function () { Route::get('', ['uses' => 'InstallController@index', 'as' => 'index']); Route::post('runCommand', ['uses' => 'InstallController@runCommand', 'as' => 'runCommand']); -// Route::post('migrate', ['uses' => 'InstallController@migrate', 'as' => 'migrate']); -// Route::post('keys', ['uses' => 'InstallController@keys', 'as' => 'keys']); -// Route::post('upgrade', ['uses' => 'InstallController@upgrade', 'as' => 'upgrade']); -// Route::post('verify', ['uses' => 'InstallController@verify', 'as' => 'verify']); -// Route::post('decrypt', ['uses' => 'InstallController@decrypt', 'as' => 'decrypt']); } ); Route::group( - ['middleware' => 'binders-only','namespace' => 'FireflyIII\Http\Controllers\System', 'as' => 'cron.', 'prefix' => 'cron'], function () { - Route::get('run/{cliToken}', ['uses' => 'CronController@cron', 'as' => 'cron']); + ['middleware' => 'binders-only', 'namespace' => 'FireflyIII\Http\Controllers\System', 'as' => 'cron.', 'prefix' => 'cron'], static function () { + Route::get('run/{cliToken}', ['uses' => 'CronController@cron', 'as' => 'cron']); } ); @@ -45,7 +39,7 @@ Route::group( * These routes only work when the user is NOT logged in. */ Route::group( - ['middleware' => 'user-not-logged-in', 'namespace' => 'FireflyIII\Http\Controllers'], function () { + ['middleware' => 'user-not-logged-in', 'namespace' => 'FireflyIII\Http\Controllers'], static function () { // Authentication Routes... Route::get('login', 'Auth\LoginController@showLoginForm')->name('login'); @@ -58,7 +52,7 @@ Route::group( // Password Reset Routes... Route::get('password/reset/{token}', ['uses' => 'Auth\ResetPasswordController@showResetForm', 'as' => 'password.reset']); Route::post('password/email', ['uses' => 'Auth\ForgotPasswordController@sendResetLinkEmail', 'as' => 'password.email']); - Route::post('password/reset',['uses' => 'Auth\ResetPasswordController@reset']); + Route::post('password/reset', ['uses' => 'Auth\ResetPasswordController@reset']); Route::get('password/reset', ['uses' => 'Auth\ForgotPasswordController@showLinkRequestForm', 'as' => 'password.reset.request']); // Change email routes: @@ -148,9 +142,10 @@ Route::group( ); // show reconciliation - Route::get('reconcile/show/{tj}', ['uses' => 'Account\ReconcileController@show', 'as' => 'reconcile.show']); - Route::get('reconcile/edit/{tj}', ['uses' => 'Account\ReconcileController@edit', 'as' => 'reconcile.edit']); - Route::post('reconcile/update/{tj}', ['uses' => 'Account\ReconcileController@update', 'as' => 'reconcile.update']); + // TODO improve me + //Route::get('reconcile/show/{transactionGroup}', ['uses' => 'Account\ReconcileController@show', 'as' => 'reconcile.show']); + //Route::get('reconcile/edit/{transactionGroup}', ['uses' => 'Account\ReconcileController@edit', 'as' => 'reconcile.edit']); + //Route::post('reconcile/update/{transactionGroup}', ['uses' => 'Account\ReconcileController@update', 'as' => 'reconcile.update']); } @@ -553,10 +548,11 @@ Route::group( // for auto complete - Route::get('transaction-journals/all', ['uses' => 'Json\AutoCompleteController@allTransactionJournals', 'as' => 'all-transaction-journals']); - Route::get('transaction-journals/with-id/{tj}', ['uses' => 'Json\AutoCompleteController@journalsWithId', 'as' => 'journals-with-id']); - Route::get('transaction-journals/{what}', ['uses' => 'Json\AutoCompleteController@transactionJournals', 'as' => 'transaction-journals']); -// Route::get('transaction-types', ['uses' => 'Json\AutoCompleteController@transactionTypes', 'as' => 'transaction-types']); + // TODO improve me. + //Route::get('transaction-journals/all', ['uses' => 'Json\AutoCompleteController@allTransactionJournals', 'as' => 'all-transaction-journals']); + //Route::get('transaction-journals/with-id/{tj}', ['uses' => 'Json\AutoCompleteController@journalsWithId', 'as' => 'journals-with-id']); + //Route::get('transaction-journals/{what}', ['uses' => 'Json\AutoCompleteController@transactionJournals', 'as' => 'transaction-journals']); + // Route::get('transaction-types', ['uses' => 'Json\AutoCompleteController@transactionTypes', 'as' => 'transaction-types']); // boxes Route::get('box/balance', ['uses' => 'Json\BoxController@balance', 'as' => 'box.balance']); @@ -875,15 +871,19 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'transactions', 'as' => 'transactions.'], function () { - Route::get('{what}/all', ['uses' => 'TransactionController@indexAll', 'as' => 'index.all'])->where(['what' => 'withdrawal|deposit|transfers|transfer']); - Route::get('{what}/{start_date?}/{end_date?}', ['uses' => 'TransactionController@index', 'as' => 'index'])->where( - ['what' => 'withdrawal|deposit|transfers|transfer'] - ); +// Route::get('{what}/all', ['uses' => 'TransactionController@indexAll', 'as' => 'index.all'])->where(['what' => 'withdrawal|deposit|transfers|transfer']); +// Route::get('{what}/{start_date?}/{end_date?}', ['uses' => 'TransactionController@index', 'as' => 'index'])->where( +// ['what' => 'withdrawal|deposit|transfers|transfer'] +// ); - Route::get('show/{tj}', ['uses' => 'TransactionController@show', 'as' => 'show']); - Route::get('debug/{tj}', ['uses' => 'Transaction\SingleController@debugShow', 'as' => 'debug']); - Route::post('reorder', ['uses' => 'TransactionController@reorder', 'as' => 'reorder']); - Route::post('reconcile', ['uses' => 'TransactionController@reconcile', 'as' => 'reconcile']); + //Route::get('show/{tj}', ['uses' => 'TransactionController@show', 'as' => 'show']); + //Route::get('debug/{tj}', ['uses' => 'Transaction\SingleController@debugShow', 'as' => 'debug']); + + //Route::get('show/{tj}', ['uses' => 'TransactionController@show', 'as' => 'show']); + //Route::get('debug/{tj}', ['uses' => 'Transaction\SingleController@debugShow', 'as' => 'debug']); + + //Route::post('reorder', ['uses' => 'TransactionController@reorder', 'as' => 'reorder']); + //Route::post('reconcile', ['uses' => 'TransactionController@reconcile', 'as' => 'reconcile']); } ); @@ -893,13 +893,13 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions', 'as' => 'transactions.'], function () { - Route::get('create/{what}', ['uses' => 'SingleController@create', 'as' => 'create'])->where(['what' => 'withdrawal|deposit|transfer']); - Route::get('edit/{tj}', ['uses' => 'SingleController@edit', 'as' => 'edit']); - Route::get('delete/{tj}', ['uses' => 'SingleController@delete', 'as' => 'delete']); - Route::post('store/{what}', ['uses' => 'SingleController@store', 'as' => 'store'])->where(['what' => 'withdrawal|deposit|transfer']); - Route::post('update/{tj}', ['uses' => 'SingleController@update', 'as' => 'update']); - Route::post('destroy/{tj}', ['uses' => 'SingleController@destroy', 'as' => 'destroy']); - Route::get('clone/{tj}', ['uses' => 'SingleController@cloneTransaction', 'as' => 'clone']); +// Route::get('create/{what}', ['uses' => 'SingleController@create', 'as' => 'create'])->where(['what' => 'withdrawal|deposit|transfer']); +// Route::get('edit/{tj}', ['uses' => 'SingleController@edit', 'as' => 'edit']); +// Route::get('delete/{tj}', ['uses' => 'SingleController@delete', 'as' => 'delete']); +// Route::post('store/{what}', ['uses' => 'SingleController@store', 'as' => 'store'])->where(['what' => 'withdrawal|deposit|transfer']); +// Route::post('update/{tj}', ['uses' => 'SingleController@update', 'as' => 'update']); +// Route::post('destroy/{tj}', ['uses' => 'SingleController@destroy', 'as' => 'destroy']); +// Route::get('clone/{tj}', ['uses' => 'SingleController@cloneTransaction', 'as' => 'clone']); } ); @@ -933,8 +933,8 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/split', 'as' => 'transactions.split.'], function () { - Route::get('edit/{tj}', ['uses' => 'SplitController@edit', 'as' => 'edit']); - Route::post('update/{tj}', ['uses' => 'SplitController@update', 'as' => 'update']); +// Route::get('edit/{tj}', ['uses' => 'SplitController@edit', 'as' => 'edit']); +// Route::post('update/{tj}', ['uses' => 'SplitController@update', 'as' => 'update']); } ); @@ -945,8 +945,8 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/convert', 'as' => 'transactions.convert.'], function () { - Route::get('{transactionType}/{tj}', ['uses' => 'ConvertController@index', 'as' => 'index']); - Route::post('{transactionType}/{tj}', ['uses' => 'ConvertController@postIndex', 'as' => 'index.post']); +// Route::get('{transactionType}/{tj}', ['uses' => 'ConvertController@index', 'as' => 'index']); +// Route::post('{transactionType}/{tj}', ['uses' => 'ConvertController@postIndex', 'as' => 'index.post']); } ); @@ -956,8 +956,7 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/link', 'as' => 'transactions.link.'], function () { - Route::post('store/{tj}', ['uses' => 'LinkController@store', 'as' => 'store']); - + //Route::post('store/{tj}', ['uses' => 'LinkController@store', 'as' => 'store']); Route::get('delete/{journalLink}', ['uses' => 'LinkController@delete', 'as' => 'delete']); Route::get('switch/{journalLink}', ['uses' => 'LinkController@switchLink', 'as' => 'switch']);
{{ trans('list.'~metaField) }}