diff --git a/app/Factory/AccountMetaFactory.php b/app/Factory/AccountMetaFactory.php new file mode 100644 index 0000000000..12290e08a7 --- /dev/null +++ b/app/Factory/AccountMetaFactory.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Factory; + +use FireflyIII\Models\AccountMeta; + +/** + * Class AccountMetaFactory + */ +class AccountMetaFactory +{ + /** + * @param array $data + * + * @return AccountMeta|null + */ + public function create(array $data): ?AccountMeta + { + return AccountMeta::create($data); + } + +} \ No newline at end of file diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index 35bd0338ca..6cc1c79719 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -62,13 +62,15 @@ class TransactionFactory */ public function create(array $data): Transaction { + $currencyId = isset($data['currency']) ? $data['currency']->id : $data['currency_id']; + return Transaction::create( [ 'reconciled' => $data['reconciled'], 'account_id' => $data['account']->id, 'transaction_journal_id' => $data['transaction_journal']->id, 'description' => $data['description'], - 'transaction_currency_id' => $data['currency']->id, + 'transaction_currency_id' => $currencyId, 'amount' => $data['amount'], 'foreign_amount' => $data['foreign_amount'], 'foreign_currency_id' => null, diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index aa55c29aa9..a2887c870d 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -85,7 +85,7 @@ class TransactionJournalFactory $this->connectTags($journal, $data); // store note: - $this->storeNote($journal, $data['notes']); + $this->storeNote($journal, strval($data['notes'])); // store date meta fields (if present): $this->storeMeta($journal, $data, 'interest_date'); @@ -154,6 +154,9 @@ class TransactionJournalFactory { $factory = app(TagFactory::class); $factory->setUser($journal->user); + if (is_null($data['tags'])) { + return; + } foreach ($data['tags'] as $string) { $tag = $factory->findOrCreate($string); $journal->tags()->save($tag); @@ -187,7 +190,7 @@ class TransactionJournalFactory */ protected function storeMeta(TransactionJournal $journal, array $data, string $field): void { - $value = $data[$field]; + $value = $data[$field] ?? null; if (!is_null($value)) { $set = [ 'journal' => $journal, diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index d29d5a458f..3c9df266ea 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -198,6 +198,7 @@ class AccountController extends Controller 'virtualBalance' => $account->virtual_balance, 'currency_id' => $currency->id, 'notes' => '', + 'active' => $account->active, ]; /** @var Note $note */ $note = $this->repository->getNote($account); diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index f31a6f8f52..f32f79d062 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -36,6 +36,7 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Services\Internal\Destroy\AccountDestroyService; +use FireflyIII\Services\Internal\Update\AccountUpdateService; use FireflyIII\User; use Log; use Validator; @@ -276,24 +277,14 @@ class AccountRepository implements AccountRepositoryInterface */ public function update(Account $account, array $data): Account { - // update the account: - $account->name = $data['name']; - $account->active = $data['active']; - $account->virtual_balance = trim($data['virtualBalance']) === '' ? '0' : $data['virtualBalance']; - $account->iban = $data['iban']; - $account->save(); - - $this->updateMetadata($account, $data); - if ($this->validOpeningBalanceData($data)) { - $this->updateInitialBalance($account, $data); + /** @var AccountUpdateService $service */ + $service = app(AccountUpdateService::class); + try { + $account = $service->update($account, $data); + } catch (FireflyException $e) { + } catch (\Exception $e) { } - // update note: - if (isset($data['notes']) && null !== $data['notes']) { - $this->updateNote($account, strval($data['notes'])); - } - - return $account; } diff --git a/app/Services/Internal/Support/AccountServiceTrait.php b/app/Services/Internal/Support/AccountServiceTrait.php new file mode 100644 index 0000000000..3267c5a241 --- /dev/null +++ b/app/Services/Internal/Support/AccountServiceTrait.php @@ -0,0 +1,352 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Internal\Support; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\AccountFactory; +use FireflyIII\Factory\TransactionFactory; +use FireflyIII\Factory\TransactionJournalFactory; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Note; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\User; +use Log; +use Validator; + +/** + * Trait AccountServiceTrait + * + * @package FireflyIII\Services\Internal\Support + */ +trait AccountServiceTrait +{ + /** + * @param Account $account + * + * @return bool + * @throws \Exception + */ + public function deleteIB(Account $account): bool + { + Log::debug(sprintf('deleteIB() for account #%d', $account->id)); + $openingBalance = $this->getIBJournal($account); + + // opening balance data? update it! + if (null !== $openingBalance->id) { + Log::debug('Opening balance journal found, delete journal.'); + $openingBalance->delete(); + + return true; + } + + return true; + } + + /** + * @param null|string $iban + * + * @return null|string + */ + public function filterIban(?string $iban) + { + if (null === $iban) { + return null; + } + $data = ['iban' => $iban]; + $rules = ['iban' => 'required|iban']; + $validator = Validator::make($data, $rules); + if ($validator->fails()) { + Log::error(sprintf('Detected invalid IBAN ("%s"). Return NULL instead.', $iban)); + + return null; + } + + + return $iban; + } + + /** + * Find existing opening balance. + * + * @param Account $account + * + * @return TransactionJournal|null + */ + public function getIBJournal(Account $account): ?TransactionJournal + { + $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->transactionTypes([TransactionType::OPENING_BALANCE]) + ->first(['transaction_journals.*']); + if (null === $journal) { + Log::debug('Could not find a opening balance journal, return NULL.'); + + return null; + } + Log::debug(sprintf('Found opening balance: journal #%d.', $journal->id)); + + return $journal; + } + + /** + * @param Account $account + * @param array $data + * + * @return TransactionJournal|null + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function storeIBJournal(Account $account, array $data): ?TransactionJournal + { + $amount = strval($data['openingBalance']); + Log::debug(sprintf('Submitted amount is %s', $amount)); + + if (0 === bccomp($amount, '0')) { + return null; + } + + // store journal, without transactions: + $name = $data['name']; + $currencyId = $data['currency_id']; + $journalData = [ + 'type' => TransactionType::OPENING_BALANCE, + 'user' => $account->user->id, + 'transaction_currency_id' => $currencyId, + 'description' => strval(trans('firefly.initial_balance_description', ['account' => $account->name])), + 'completed' => true, + 'date' => $data['openingBalanceDate'], + 'bill_id' => null, + 'bill_name' => null, + 'piggy_bank_id' => null, + 'piggy_bank_name' => null, + 'tags' => null, + 'notes' => null, + 'transactions' => [], + + ]; + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($account->user); + $journal = $factory->create($journalData); + $opposing = $this->storeOpposingAccount($account->user, $name); + Log::notice(sprintf('Created new opening balance journal: #%d', $journal->id)); + + $firstAccount = $account; + $secondAccount = $opposing; + $firstAmount = $amount; + $secondAmount = bcmul($amount, '-1'); + Log::notice(sprintf('First amount is %s, second amount is %s', $firstAmount, $secondAmount)); + + if (bccomp($amount, '0') === -1) { + Log::debug(sprintf('%s is a negative number.', $amount)); + $firstAccount = $opposing; + $secondAccount = $account; + $firstAmount = bcmul($amount, '-1'); + $secondAmount = $amount; + Log::notice(sprintf('First amount is %s, second amount is %s', $firstAmount, $secondAmount)); + } + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($account->user); + $one = $factory->create( + [ + 'account' => $firstAccount, + 'transaction_journal' => $journal, + 'amount' => $firstAmount, + 'currency_id' => $currencyId, + 'description' => null, + 'identifier' => 0, + 'foreign_amount' => null, + 'reconciled' => false, + ] + ); + $two = $factory->create( + [ + 'account' => $secondAccount, + 'transaction_journal' => $journal, + 'amount' => $secondAmount, + 'currency_id' => $currencyId, + 'description' => null, + 'identifier' => 0, + 'foreign_amount' => null, + 'reconciled' => false, + ] + ); + Log::notice(sprintf('Stored two transactions for new account, #%d and #%d', $one->id, $two->id)); + + return $journal; + } + + /** + * TODO make sure this works (user ID, etc.) + * + * @param User $user + * @param string $name + * + * @return Account + */ + public function storeOpposingAccount(User $user, string $name): Account + { + $name = $name . ' initial balance'; + Log::debug('Going to create an opening balance opposing account.'); + /** @var AccountFactory $factory */ + $factory = app(AccountFactory::class); + $factory->setUser($user); + + + return $factory->findOrCreate($name, AccountType::INITIAL_BALANCE); + } + + /** + * @param Account $account + * @param array $data + * + * @return bool + * @throws FireflyException + * @throws \Exception + */ + public function updateIB(Account $account, array $data): bool + { + Log::debug(sprintf('updateInitialBalance() for account #%d', $account->id)); + $openingBalance = $this->getIBJournal($account); + + // no opening balance journal? create it: + if (null === $openingBalance) { + Log::debug('No opening balance journal yet, create journal.'); + $this->storeIBJournal($account, $data); + + return true; + } + + // opening balance data? update it! + if (null !== $openingBalance->id) { + Log::debug('Opening balance journal found, update journal.'); + $this->updateIBJournal($account, $openingBalance, $data); + + return true; + } + + return true; + } + + /** + * Verify if array contains valid data to possibly store or update the opening balance. + * + * @param array $data + * + * @return bool + */ + public function validIBData(array $data): bool + { + $data['openingBalance'] = strval($data['openingBalance'] ?? ''); + if (isset($data['openingBalance']) && null !== $data['openingBalance'] && strlen($data['openingBalance']) > 0 + && isset($data['openingBalanceDate'])) { + Log::debug('Array has valid opening balance data.'); + + return true; + } + Log::debug('Array does not have valid opening balance data.'); + + return false; + } + + /** + * @param Account $account + * @param TransactionJournal $journal + * @param array $data + * + * @return bool + * + * @throws \Exception + */ + protected function updateIBJournal(Account $account, TransactionJournal $journal, array $data): bool + { + $date = $data['openingBalanceDate']; + $amount = strval($data['openingBalance']); + $negativeAmount = bcmul($amount, '-1'); + $currencyId = intval($data['currency_id']); + + Log::debug(sprintf('Submitted amount for opening balance to update is "%s"', $amount)); + if (0 === bccomp($amount, '0')) { + Log::notice(sprintf('Amount "%s" is zero, delete opening balance.', $amount)); + $journal->delete(); + + return true; + } + + // update date: + $journal->date = $date; + $journal->transaction_currency_id = $currencyId; + $journal->save(); + + // update transactions: + /** @var Transaction $transaction */ + foreach ($journal->transactions()->get() as $transaction) { + if (intval($account->id) === intval($transaction->account_id)) { + Log::debug(sprintf('Will (eq) change transaction #%d amount from "%s" to "%s"', $transaction->id, $transaction->amount, $amount)); + $transaction->amount = $amount; + $transaction->transaction_currency_id = $currencyId; + $transaction->save(); + } + if (!(intval($account->id) === intval($transaction->account_id))) { + Log::debug(sprintf('Will (neq) change transaction #%d amount from "%s" to "%s"', $transaction->id, $transaction->amount, $negativeAmount)); + $transaction->amount = $negativeAmount; + $transaction->transaction_currency_id = $currencyId; + $transaction->save(); + } + } + Log::debug('Updated opening balance journal.'); + + return true; + } + + /** + * @param Account $account + * @param string $note + * + * @return bool + */ + protected function updateNote(Account $account, string $note): bool + { + if (0 === strlen($note)) { + $dbNote = $account->notes()->first(); + if (null !== $dbNote) { + $dbNote->delete(); + } + + return true; + } + $dbNote = $account->notes()->first(); + if (null === $dbNote) { + $dbNote = new Note; + $dbNote->noteable()->associate($account); + } + $dbNote->text = trim($note); + $dbNote->save(); + + return true; + } +} \ No newline at end of file diff --git a/app/Services/Internal/Update/AccountUpdateService.php b/app/Services/Internal/Update/AccountUpdateService.php new file mode 100644 index 0000000000..18cfd461c2 --- /dev/null +++ b/app/Services/Internal/Update/AccountUpdateService.php @@ -0,0 +1,132 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Internal\Update; + +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\AccountMetaFactory; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountMeta; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Services\Internal\Support\AccountServiceTrait; +use Log; + +/** + * Class AccountUpdateService + */ +class AccountUpdateService +{ + use AccountServiceTrait; + + /** @var array */ + private $validAssetFields = ['accountRole', 'accountNumber', 'currency_id', 'BIC']; + /** @var array */ + private $validCCFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', 'accountNumber', 'currency_id', 'BIC']; + /** @var array */ + private $validFields = ['accountNumber', 'currency_id', 'BIC']; + + /** + * Update account data. + * + * @param Account $account + * @param array $data + * + * @return Account + * @throws FireflyException + * @throws Exception + */ + public function update(Account $account, array $data): Account + { + // update the account itself: + $account->name = $data['name']; + $account->active = $data['active']; + $account->virtual_balance = trim($data['virtualBalance']) === '' ? '0' : $data['virtualBalance']; + $account->iban = $data['iban']; + $account->save(); + + // update all meta data: + $this->updateMetaData($account, $data); + + // has valid initial balance (IB) data? + if ($this->validIBData($data)) { + // then do update! + $this->updateIB($account, $data); + } + + // if not, delete it when exist. + if (!$this->validIBData($data)) { + $this->deleteIB($account); + } + + // update note: + if (isset($data['notes']) && null !== $data['notes']) { + $this->updateNote($account, strval($data['notes'])); + } + + return $account; + } + + /** + * Update meta data for account. Depends on type which fields are valid. + * + * @param Account $account + * @param array $data + */ + protected function updateMetaData(Account $account, array $data) + { + $fields = $this->validFields; + + if ($account->accountType->type === AccountType::ASSET) { + $fields = $this->validAssetFields; + } + if ($account->accountType->type === AccountType::ASSET && $data['accountRole'] === 'ccAsset') { + $fields = $this->validCCFields; + } + /** @var AccountMetaFactory $factory */ + $factory = app(AccountMetaFactory::class); + foreach ($fields as $field) { + /** @var AccountMeta $entry */ + $entry = $account->accountMeta()->where('name', $field)->first(); + + // if $data has field and $entry is null, create new one: + if (isset($data[$field]) && null === $entry) { + Log::debug(sprintf('Created meta-field "%s":"%s" for account #%d ("%s") ', $field, $data[$field], $account->id, $account->name)); + $factory->create(['account_id' => $account->id, 'name' => $field, 'data' => $data[$field],]); + } + + // if $data has field and $entry is not null, update $entry: + // let's not bother with a service. + if (isset($data[$field]) && null !== $entry) { + $entry->data = $data[$field]; + $entry->save(); + Log::debug(sprintf('Updated meta-field "%s":"%s" for #%d ("%s") ', $field, $data[$field], $account->id, $account->name)); + } + } + } + + + +} \ No newline at end of file