diff --git a/app/Http/Controllers/Json/AutoCompleteController.php b/app/Http/Controllers/Json/AutoCompleteController.php index d6c758bb0d..66f1ddb14e 100644 --- a/app/Http/Controllers/Json/AutoCompleteController.php +++ b/app/Http/Controllers/Json/AutoCompleteController.php @@ -230,6 +230,31 @@ class AutoCompleteController extends Controller return response()->json($return); } + /** + * List of revenue accounts. + * + * @param AccountRepositoryInterface $repository + * + * @return JsonResponse + */ + public function assetAccounts(AccountRepositoryInterface $repository): JsonResponse + { + $set = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $filtered = $set->filter( + function (Account $account) { + if (true === $account->active) { + return $account; + } + + return false; // @codeCoverageIgnore + } + ); + $return = array_unique($filtered->pluck('name')->toArray()); + sort($return); + + return response()->json($return); + } + /** * Returns a JSON list of all beneficiaries. * diff --git a/app/TransactionRules/Actions/ActionInterface.php b/app/TransactionRules/Actions/ActionInterface.php index e2490e1799..71967d7e76 100644 --- a/app/TransactionRules/Actions/ActionInterface.php +++ b/app/TransactionRules/Actions/ActionInterface.php @@ -31,7 +31,7 @@ use FireflyIII\Models\TransactionJournal; interface ActionInterface { /** - * TriggerInterface constructor. + * ActionInterface constructor. * * @param RuleAction $action */ diff --git a/app/TransactionRules/Actions/ConvertToDeposit.php b/app/TransactionRules/Actions/ConvertToDeposit.php new file mode 100644 index 0000000000..47888b8762 --- /dev/null +++ b/app/TransactionRules/Actions/ConvertToDeposit.php @@ -0,0 +1,209 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\TransactionRules\Actions; + + +use FireflyIII\Factory\AccountFactory; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Log; + +/** + * + * Class ConvertToDeposit + */ +class ConvertToDeposit implements ActionInterface +{ + /** @var RuleAction The rule action */ + private $action; + + /** + * TriggerInterface constructor. + * + * @param RuleAction $action + */ + public function __construct(RuleAction $action) + { + $this->action = $action; + } + + /** + * Execute the action. + * + * @param TransactionJournal $journal + * + * @return bool + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function act(TransactionJournal $journal): bool + { + $type = $journal->transactionType->type; + if (TransactionType::DEPOSIT === $type) { + // @codeCoverageIgnoreStart + Log::error(sprintf('Journal #%d is already a deposit (rule "%s").', $journal->id, $this->action->rule->title)); + + return false; + // @codeCoverageIgnoreEnd + } + + $destTransactions = $journal->transactions()->where('amount', '>', 0)->get(); + $sourceTransactions = $journal->transactions()->where('amount', '<', 0)->get(); + + // break if count is zero: + if (1 !== $sourceTransactions->count()) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has %d source transactions. ConvertToDeposit failed. (rule "%s").', + [$journal->id, $sourceTransactions->count(), $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + if (0 === $destTransactions->count()) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has %d dest transactions. ConvertToDeposit failed. (rule "%s").', + [$journal->id, $destTransactions->count(), $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + + + if (TransactionType::WITHDRAWAL === $type) { + Log::debug('Going to transform a withdrawal to a deposit.'); + return $this->convertWithdrawal($journal); + } + if (TransactionType::TRANSFER === $type) { + Log::debug('Going to transform a transfer to a deposit.'); + return $this->convertTransfer($journal); + } + + return false; // @codeCoverageIgnore + } + + /** + * Input is a transfer from A to B. + * Output is a deposit from C to B. + * + * @param TransactionJournal $journal + * + * @return bool + * @throws \FireflyIII\Exceptions\FireflyException + */ + private function convertTransfer(TransactionJournal $journal): bool + { + // find or create revenue account. + /** @var AccountFactory $factory */ + $factory = app(AccountFactory::class); + $factory->setUser($journal->user); + + $sourceTransactions = $journal->transactions()->where('amount', '<', 0)->get(); + + // get the action value, or use the original source name in case the action value is empty: + // this becomes a new or existing revenue account. + /** @var Account $source */ + $source = $sourceTransactions->first()->account; + $revenueName = '' === $this->action->action_value ? $source->name : $this->action->action_value; + $revenue = $factory->findOrCreate($revenueName, AccountType::REVENUE); + + Log::debug(sprintf('ConvertToDeposit. Action value is "%s", revenue name is "%s"', $this->action->action_value, $source->name)); + unset($source); + + // update source transaction(s) to be revenue account + $journal->transactions() + ->where('amount', '<', 0) + ->update(['account_id' => $revenue->id]); + + // change transaction type of journal: + $newType = TransactionType::whereType(TransactionType::DEPOSIT)->first(); + $journal->transaction_type_id = $newType->id; + $journal->save(); + Log::debug('Converted transfer to deposit.'); + + return true; + } + + /** + * Input is a withdrawal from A to B + * Is converted to a deposit from C to A. + * + * @param TransactionJournal $journal + * + * @return bool + * @throws \FireflyIII\Exceptions\FireflyException + */ + private function convertWithdrawal(TransactionJournal $journal): bool + { + // find or create revenue account. + /** @var AccountFactory $factory */ + $factory = app(AccountFactory::class); + $factory->setUser($journal->user); + + $destTransactions = $journal->transactions()->where('amount', '>', 0)->get(); + $sourceTransactions = $journal->transactions()->where('amount', '<', 0)->get(); + + // get the action value, or use the original destination name in case the action value is empty: + // this becomes a new or existing revenue account. + /** @var Account $destination */ + $destination = $destTransactions->first()->account; + $revenueName = '' === $this->action->action_value ? $destination->name : $this->action->action_value; + $revenue = $factory->findOrCreate($revenueName, AccountType::REVENUE); + + Log::debug(sprintf('ConvertToDeposit. Action value is "%s", revenue name is "%s"', $this->action->action_value, $destination->name)); + + + // get source account from transaction(s). + /** @var Account $source */ + $source = $sourceTransactions->first()->account; + + // update source transaction(s) to be revenue account + $journal->transactions() + ->where('amount', '<', 0) + ->update(['account_id' => $revenue->id]); + + // update destination transaction(s) to be original source account(s). + $journal->transactions() + ->where('amount', '>', 0) + ->update(['account_id' => $source->id]); + + // change transaction type of journal: + $newType = TransactionType::whereType(TransactionType::DEPOSIT)->first(); + $journal->transaction_type_id = $newType->id; + $journal->save(); + + Log::debug('Converted withdrawal to deposit.'); + + return true; + } +} \ No newline at end of file diff --git a/app/TransactionRules/Actions/ConvertToTransfer.php b/app/TransactionRules/Actions/ConvertToTransfer.php new file mode 100644 index 0000000000..afd3bfd3a6 --- /dev/null +++ b/app/TransactionRules/Actions/ConvertToTransfer.php @@ -0,0 +1,207 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\TransactionRules\Actions; + + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use Log; + +/** + * + * Class ConvertToTransfer + */ +class ConvertToTransfer implements ActionInterface +{ + /** @var RuleAction The rule action */ + private $action; + + /** + * TriggerInterface constructor. + * + * @param RuleAction $action + */ + public function __construct(RuleAction $action) + { + $this->action = $action; + } + + /** + * Execute the action. + * + * @param TransactionJournal $journal + * + * @return bool + */ + public function act(TransactionJournal $journal): bool + { + $type = $journal->transactionType->type; + if (TransactionType::TRANSFER === $type) { + // @codeCoverageIgnoreStart + Log::error(sprintf('Journal #%d is already a transfer so cannot be converted (rule "%s").', $journal->id, $this->action->rule->title)); + + return false; + // @codeCoverageIgnoreEnd + } + // find the asset account in the action value. + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($journal->user); + $asset = $repository->findByName($this->action->action_value, [AccountType::ASSET, AccountType::DEFAULT]); + if (null === $asset) { + // @codeCoverageIgnoreStart + Log::error( + sprintf( + 'Journal #%d cannot be converted because no asset with name "%s" exists (rule "%s").', $journal->id, $this->action->action_value, + $this->action->rule->title + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + + $destTransactions = $journal->transactions()->where('amount', '>', 0)->get(); + $sourceTransactions = $journal->transactions()->where('amount', '<', 0)->get(); + + // break if count is zero: + if (1 !== $sourceTransactions->count()) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has %d source transactions. ConvertToTransfer failed. (rule "%s").', + [$journal->id, $sourceTransactions->count(), $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + if (0 === $destTransactions->count()) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has %d dest transactions. ConvertToTransfer failed. (rule "%s").', + [$journal->id, $destTransactions->count(), $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + + + if (TransactionType::WITHDRAWAL === $type) { + Log::debug('Going to transform a withdrawal to a transfer.'); + + return $this->convertWithdrawal($journal, $asset); + } + if (TransactionType::DEPOSIT === $type) { + Log::debug('Going to transform a deposit to a transfer.'); + + return $this->convertDeposit($journal, $asset); + } + + return false; // @codeCoverageIgnore + } + /** + * A deposit is from Revenue to Asset. + * We replace the Revenue with another asset. + * + * @param TransactionJournal $journal + * @param Account $assetAccount + * + * @return bool + */ + private function convertDeposit(TransactionJournal $journal, Account $assetAccount): bool + { + /** @var Account $destinationAsset */ + $destinationAsset = $journal->transactions()->where('amount', '>', 0)->first()->account; + if ($destinationAsset->id === $assetAccount->id) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has already has "%s" as a destination asset. ConvertToTransfer failed. (rule "%s").', + [$journal->id, $assetAccount->name, $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + // update source transactions + $journal->transactions()->where('amount', '<', 0) + ->update(['account_id' => $assetAccount->id]); + + // change transaction type of journal: + $newType = TransactionType::whereType(TransactionType::TRANSFER)->first(); + $journal->transaction_type_id = $newType->id; + $journal->save(); + Log::debug('Converted deposit to transfer.'); + + return true; + } + + /** + * A withdrawal is from Asset to Expense. + * We replace the Expense with another asset. + * + * @param TransactionJournal $journal + * @param Account $assetAccount + * + * @return bool + */ + private function convertWithdrawal(TransactionJournal $journal, Account $assetAccount): bool + { + /** @var Account $sourceAsset */ + $sourceAsset = $journal->transactions()->where('amount', '<', 0)->first()->account; + if ($sourceAsset->id === $assetAccount->id) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has already has "%s" as a source asset. ConvertToTransfer failed. (rule "%s").', + [$journal->id, $assetAccount->name, $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + // update destination transactions + $journal->transactions()->where('amount', '>', 0) + ->update(['account_id' => $assetAccount->id]); + + // change transaction type of journal: + $newType = TransactionType::whereType(TransactionType::TRANSFER)->first(); + $journal->transaction_type_id = $newType->id; + $journal->save(); + Log::debug('Converted withdrawal to transfer.'); + + return true; + } +} \ No newline at end of file diff --git a/app/TransactionRules/Actions/ConvertToWithdrawal.php b/app/TransactionRules/Actions/ConvertToWithdrawal.php new file mode 100644 index 0000000000..adbd338d9e --- /dev/null +++ b/app/TransactionRules/Actions/ConvertToWithdrawal.php @@ -0,0 +1,212 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\TransactionRules\Actions; + + +use FireflyIII\Factory\AccountFactory; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Log; + + +/** + * + * Class ConvertToWithdrawal + */ +class ConvertToWithdrawal implements ActionInterface +{ + /** @var RuleAction The rule action */ + private $action; + + /** + * TriggerInterface constructor. + * + * @param RuleAction $action + */ + public function __construct(RuleAction $action) + { + $this->action = $action; + } + + /** + * Execute the action. + * + * @param TransactionJournal $journal + * + * @return bool + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function act(TransactionJournal $journal): bool + { + $type = $journal->transactionType->type; + if (TransactionType::WITHDRAWAL === $type) { + // @codeCoverageIgnoreStart + Log::error(sprintf('Journal #%d is already a withdrawal (rule "%s").', $journal->id, $this->action->rule->title)); + + return false; + // @codeCoverageIgnoreEnd + } + + $destTransactions = $journal->transactions()->where('amount', '>', 0)->get(); + $sourceTransactions = $journal->transactions()->where('amount', '<', 0)->get(); + + // break if count is zero: + if (1 !== $sourceTransactions->count()) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has %d source transactions. ConvertToWithdrawal failed. (rule "%s").', + [$journal->id, $sourceTransactions->count(), $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + if (0 === $destTransactions->count()) { + // @codeCoverageIgnoreStart + Log::error( + vsprintf( + 'Journal #%d has %d dest transactions. ConvertToWithdrawal failed. (rule "%s").', + [$journal->id, $destTransactions->count(), $this->action->rule->title] + ) + ); + + return false; + // @codeCoverageIgnoreEnd + } + + + if (TransactionType::DEPOSIT === $type) { + Log::debug('Going to transform a deposit to a withdrawal.'); + + return $this->convertDeposit($journal); + } + if (TransactionType::TRANSFER === $type) { + Log::debug('Going to transform a transfer to a withdrawal.'); + + return $this->convertTransfer($journal); + } + + return false; // @codeCoverageIgnore + } + + /** + * Input is a deposit from A to B + * Is converted to a withdrawal from B to C. + * + * @param TransactionJournal $journal + * + * @return bool + * @throws \FireflyIII\Exceptions\FireflyException + */ + private function convertDeposit(TransactionJournal $journal): bool + { + // find or create expense account. + /** @var AccountFactory $factory */ + $factory = app(AccountFactory::class); + $factory->setUser($journal->user); + + $destTransactions = $journal->transactions()->where('amount', '>', 0)->get(); + $sourceTransactions = $journal->transactions()->where('amount', '<', 0)->get(); + + // get the action value, or use the original source revenue name in case the action value is empty: + // this becomes a new or existing expense account. + /** @var Account $source */ + $source = $sourceTransactions->first()->account; + $expenseName = '' === $this->action->action_value ? $source->name : $this->action->action_value; + $expense = $factory->findOrCreate($expenseName, AccountType::EXPENSE); + + Log::debug(sprintf('ConvertToWithdrawal. Action value is "%s", expense name is "%s"', $this->action->action_value, $source->name)); + unset($source); + + // get destination asset account from transaction(s). + /** @var Account $destination */ + $destination = $destTransactions->first()->account; + + // update source transaction(s) to be the original destination account + $journal->transactions() + ->where('amount', '<', 0) + ->update(['account_id' => $destination->id]); + + // update destination transaction(s) to be new expense account. + $journal->transactions() + ->where('amount', '>', 0) + ->update(['account_id' => $expense->id]); + + // change transaction type of journal: + $newType = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $journal->transaction_type_id = $newType->id; + $journal->save(); + + Log::debug('Converted deposit to withdrawal.'); + + return true; + } + + /** + * Input is a transfer from A to B. + * Output is a withdrawal from A to C. + * + * @param TransactionJournal $journal + * + * @return bool + * @throws \FireflyIII\Exceptions\FireflyException + */ + private function convertTransfer(TransactionJournal $journal): bool + { + // find or create expense account. + /** @var AccountFactory $factory */ + $factory = app(AccountFactory::class); + $factory->setUser($journal->user); + + $destTransactions = $journal->transactions()->where('amount', '>', 0)->get(); + + // get the action value, or use the original destination name in case the action value is empty: + // this becomes a new or existing expense account. + /** @var Account $destination */ + $destination = $destTransactions->first()->account; + $expenseName = '' === $this->action->action_value ? $destination->name : $this->action->action_value; + $expense = $factory->findOrCreate($expenseName, AccountType::EXPENSE); + + Log::debug(sprintf('ConvertToWithdrawal. Action value is "%s", revenue name is "%s"', $this->action->action_value, $destination->name)); + unset($source); + + // update destination transaction(s) to be the expense account + $journal->transactions() + ->where('amount', '>', 0) + ->update(['account_id' => $expense->id]); + + // change transaction type of journal: + $newType = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $journal->transaction_type_id = $newType->id; + $journal->save(); + Log::debug('Converted transfer to withdrawal.'); + + return true; + } +} \ No newline at end of file diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index a8d1bbc87c..3f54d34d9b 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -31,6 +31,7 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Services\Password\Verifier; @@ -40,7 +41,6 @@ use Google2FA; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Support\Collection; use Illuminate\Validation\Validator; -use Log; /** * Class FireflyValidator. @@ -249,9 +249,10 @@ class FireflyValidator extends Validator * * @return bool */ - public function validateRuleActionValue(string $attribute, string $value): bool + public function validateRuleActionValue(string $attribute, string $value = null): bool { // first, get the index from this string: + $value = $value ?? ''; $parts = explode('.', $attribute); $index = (int)($parts[1] ?? '0'); @@ -287,6 +288,18 @@ class FireflyValidator extends Validator return null !== $bill; } + // if it's convert_transfer, it must be a valid asset account name. + if ('convert_transfer' === $actionType) { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $account = $repository->findByName($value, + [AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, + AccountType::CREDITCARD] + ); + + return null !== $account; + } + // return true for the rest. return true; } diff --git a/config/firefly.php b/config/firefly.php index 9af9a74269..33d93a19ab 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -30,6 +30,9 @@ use FireflyIII\TransactionRules\Actions\AppendNotes; use FireflyIII\TransactionRules\Actions\ClearBudget; use FireflyIII\TransactionRules\Actions\ClearCategory; use FireflyIII\TransactionRules\Actions\ClearNotes; +use FireflyIII\TransactionRules\Actions\ConvertToDeposit; +use FireflyIII\TransactionRules\Actions\ConvertToTransfer; +use FireflyIII\TransactionRules\Actions\ConvertToWithdrawal; use FireflyIII\TransactionRules\Actions\LinkToBill; use FireflyIII\TransactionRules\Actions\PrependDescription; use FireflyIII\TransactionRules\Actions\PrependNotes; @@ -367,6 +370,9 @@ return [ 'prepend_notes' => PrependNotes::class, 'clear_notes' => ClearNotes::class, 'link_to_bill' => LinkToBill::class, + 'convert_withdrawal' => ConvertToWithdrawal::class, + 'convert_deposit' => ConvertToDeposit::class, + 'convert_transfer' => ConvertToTransfer::class, ], 'rule-actions-text' => [ 'set_category', diff --git a/public/js/ff/rules/create-edit.js b/public/js/ff/rules/create-edit.js index e110ca313a..615448cd02 100644 --- a/public/js/ff/rules/create-edit.js +++ b/public/js/ff/rules/create-edit.js @@ -258,6 +258,18 @@ function updateActionInput(selectList) { console.log('Select list value is ' + selectList.val() + ', so input needs auto complete.'); createAutoComplete(inputResult, 'json/all-accounts'); break; + case 'convert_withdrawal': + console.log('Select list value is ' + selectList.val() + ', so input needs expense accounts auto complete.'); + createAutoComplete(inputResult, 'json/expense-accounts'); + break; + case 'convert_deposit': + console.log('Select list value is ' + selectList.val() + ', so input needs revenue accounts auto complete.'); + createAutoComplete(inputResult, 'json/revenue-accounts'); + break; + case 'convert_transfer': + console.log('Select list value is ' + selectList.val() + ', so input needs asset accounts auto complete.'); + createAutoComplete(inputResult, 'json/asset-accounts'); + break; case 'link_to_bill': console.log('Select list value is ' + selectList.val() + ', so input needs auto complete.'); createAutoComplete(inputResult, 'json/bills'); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index c5a09fd81a..be826d4688 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -425,6 +425,12 @@ return [ 'rule_action_link_to_bill_choice' => 'Link to a bill..', 'rule_action_link_to_bill' => 'Link to bill ":action_value"', 'rule_action_set_notes' => 'Set notes to ":action_value"', + 'rule_action_convert_deposit_choice' => 'Convert the transaction to a deposit', + 'rule_action_convert_deposit' => 'Convert the transaction to a deposit from ":action_value"', + 'rule_action_convert_withdrawal_choice' => 'Convert the transaction to a withdrawal', + 'rule_action_convert_withdrawal' => 'Convert the transaction to a withdrawal to ":action_value"', + 'rule_action_convert_transfer_choice' => 'Convert the transaction to a transfer', + 'rule_action_convert_transfer' => 'Convert the transaction to a transfer with ":action_value"', 'rules_have_read_warning' => 'Have you read the warning?', 'apply_rule_warning' => 'Warning: running a rule(group) on a large selection of transactions could take ages, and it could time-out. If it does, the rule(group) will only be applied to an unknown subset of your transactions. This might leave your financial administration in tatters. Please be careful.', diff --git a/resources/views/rules/partials/action.twig b/resources/views/rules/partials/action.twig index 00022eab4d..fda2bc6863 100644 --- a/resources/views/rules/partials/action.twig +++ b/resources/views/rules/partials/action.twig @@ -3,13 +3,7 @@ - {# - {% if errors.has('rule-action.'~count) %} - -

{{ errors.first('rule-action.'~count) }}

- {% endif %} - #} - + {# todo error when invalid name. #} - {# - {% if errors.has(('rule-action-value.'~count)) %} + {% if errors.has('rule_actions.'~count~'.value') %}

- {{ errors.first('rule-action-value.'~count) }} + {{ errors.first('rule_actions.'~count~'.value') }}

{% endif %} - #}
diff --git a/resources/views/rules/partials/trigger.twig b/resources/views/rules/partials/trigger.twig index 2db8dfd5a7..c5aade93c8 100644 --- a/resources/views/rules/partials/trigger.twig +++ b/resources/views/rules/partials/trigger.twig @@ -23,13 +23,11 @@ - {# - {% if errors.has(('rule-trigger-value.'~count)) %} + {% if errors.has('rule_triggers.'~count~'.value') %}

- {{ errors.first('rule-trigger-value.'~count) }} + {{ errors.first('rule_triggers.'~count~'.value') }}

{% endif %} - #}
diff --git a/routes/web.php b/routes/web.php index a492dec37a..37b1e07c94 100755 --- a/routes/web.php +++ b/routes/web.php @@ -546,6 +546,7 @@ Route::group( Route::get('expense-accounts', ['uses' => 'Json\AutoCompleteController@expenseAccounts', 'as' => 'expense-accounts']); Route::get('all-accounts', ['uses' => 'Json\AutoCompleteController@allAccounts', 'as' => 'all-accounts']); Route::get('revenue-accounts', ['uses' => 'Json\AutoCompleteController@revenueAccounts', 'as' => 'revenue-accounts']); + Route::get('asset-accounts', ['uses' => 'Json\AutoCompleteController@assetAccounts', 'as' => 'asset-accounts']); Route::get('categories', ['uses' => 'Json\AutoCompleteController@categories', 'as' => 'categories']); Route::get('budgets', ['uses' => 'Json\AutoCompleteController@budgets', 'as' => 'budgets']); Route::get('tags', ['uses' => 'Json\AutoCompleteController@tags', 'as' => 'tags']); diff --git a/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php b/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php index 727e662f8c..718eb6f86a 100644 --- a/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php +++ b/tests/Feature/Controllers/Json/AutoCompleteControllerTest.php @@ -78,6 +78,25 @@ class AutoCompleteControllerTest extends TestCase } + /** + * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController + */ + public function testAssetAccounts(): void + { + // mock stuff + $accountA = factory(Account::class)->make(); + $collection = new Collection([$accountA]); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $accountRepos->shouldReceive('getAccountsByType') + ->withArgs([[AccountType::DEFAULT, AccountType::ASSET]])->andReturn($collection); + + $this->be($this->user()); + $response = $this->get(route('json.asset-accounts')); + $response->assertStatus(200); + $response->assertExactJson([$accountA->name]); + + } + /** * @covers \FireflyIII\Http\Controllers\Json\AutoCompleteController */ diff --git a/tests/Unit/TransactionRules/Actions/ConvertToDepositTest.php b/tests/Unit/TransactionRules/Actions/ConvertToDepositTest.php new file mode 100644 index 0000000000..d934385e36 --- /dev/null +++ b/tests/Unit/TransactionRules/Actions/ConvertToDepositTest.php @@ -0,0 +1,124 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\TransactionRules\Actions; + + +use Exception; +use FireflyIII\Factory\AccountFactory; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\TransactionType; +use FireflyIII\TransactionRules\Actions\ConvertToDeposit; +use Log; +use Tests\TestCase; + +/** + * + * Class ConvertToDepositTest + */ +class ConvertToDepositTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', \get_class($this))); + } + + /** + * Convert a withdrawal to a deposit. + * + * @covers \FireflyIII\TransactionRules\Actions\ConvertToDeposit + */ + public function testActWithdrawal() + { + $revenue = $this->getRandomRevenue(); + $name = 'Random revenue #' . random_int(1, 10000); + $journal = $this->getRandomWithdrawal(); + + // journal is a withdrawal: + $this->assertEquals(TransactionType::WITHDRAWAL, $journal->transactionType->type); + + // mock used stuff: + $factory = $this->mock(AccountFactory::class); + $factory->shouldReceive('setUser')->once(); + $factory->shouldReceive('findOrCreate')->once()->withArgs([$name, AccountType::REVENUE])->andReturn($revenue); + + + // fire the action: + $ruleAction = new RuleAction; + $ruleAction->action_value = $name; + $action = new ConvertToDeposit($ruleAction); + try { + $result = $action->act($journal); + } catch (Exception $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertTrue($result); + + // journal is now a deposit. + $journal->refresh(); + $this->assertEquals(TransactionType::DEPOSIT, $journal->transactionType->type); + } + + /** + * Convert a transfer to a deposit. + * + * @covers \FireflyIII\TransactionRules\Actions\ConvertToDeposit + */ + public function testActTransfer() + { + $revenue = $this->getRandomRevenue(); + $name = 'Random revenue #' . random_int(1, 10000); + $journal = $this->getRandomTransfer(); + + // journal is a transfer: + $this->assertEquals(TransactionType::TRANSFER, $journal->transactionType->type); + + // mock used stuff: + $factory = $this->mock(AccountFactory::class); + $factory->shouldReceive('setUser')->once(); + $factory->shouldReceive('findOrCreate')->once()->withArgs([$name, AccountType::REVENUE])->andReturn($revenue); + + + // fire the action: + $ruleAction = new RuleAction; + $ruleAction->action_value = $name; + $action = new ConvertToDeposit($ruleAction); + try { + $result = $action->act($journal); + } catch (Exception $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertTrue($result); + + // journal is now a deposit. + $journal->refresh(); + $this->assertEquals(TransactionType::DEPOSIT, $journal->transactionType->type); + } + + +} \ No newline at end of file diff --git a/tests/Unit/TransactionRules/Actions/ConvertToTransferTest.php b/tests/Unit/TransactionRules/Actions/ConvertToTransferTest.php new file mode 100644 index 0000000000..3c3a3efe77 --- /dev/null +++ b/tests/Unit/TransactionRules/Actions/ConvertToTransferTest.php @@ -0,0 +1,123 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\TransactionRules\Actions; + + +use Exception; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\TransactionRules\Actions\ConvertToTransfer; +use Log; +use Tests\TestCase; + +/** + * + * Class ConvertToTransferTest + */ +class ConvertToTransferTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', \get_class($this))); + } + + /** + * Convert a deposit to a transfer. + * + * @covers \FireflyIII\TransactionRules\Actions\ConvertToTransfer + */ + public function testActDeposit(): void + { + $deposit = $this->getRandomDeposit(); + /** @var Account $asset */ + $asset = $this->user()->accounts()->where('name', 'Bitcoin Account')->first(); + // journal is a withdrawal: + $this->assertEquals(TransactionType::DEPOSIT, $deposit->transactionType->type); + + // mock used stuff: + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findByName')->withArgs([$asset->name, [AccountType::ASSET, AccountType::DEFAULT]])->andReturn($asset); + + // fire the action: + $ruleAction = new RuleAction; + $ruleAction->action_value = $asset->name; + $action = new ConvertToTransfer($ruleAction); + + try { + $result = $action->act($deposit); + } catch (Exception $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertTrue($result); + + // journal is now a transfer. + $deposit->refresh(); + $this->assertEquals(TransactionType::TRANSFER, $deposit->transactionType->type); + } + + /** + * Convert a withdrawal to a transfer. + * + * @covers \FireflyIII\TransactionRules\Actions\ConvertToTransfer + */ + public function testActWithdrawal(): void + { + $withdrawal = $this->getRandomWithdrawal(); + /** @var Account $asset */ + $asset = $this->user()->accounts()->where('name', 'Bitcoin Account')->first(); + // journal is a withdrawal: + $this->assertEquals(TransactionType::WITHDRAWAL, $withdrawal->transactionType->type); + + // mock used stuff: + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findByName')->withArgs([$asset->name, [AccountType::ASSET, AccountType::DEFAULT]])->andReturn($asset); + + // fire the action: + $ruleAction = new RuleAction; + $ruleAction->action_value = $asset->name; + $action = new ConvertToTransfer($ruleAction); + + try { + $result = $action->act($withdrawal); + } catch (Exception $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertTrue($result); + + // journal is now a transfer. + $withdrawal->refresh(); + $this->assertEquals(TransactionType::TRANSFER, $withdrawal->transactionType->type); + } + + +} \ No newline at end of file diff --git a/tests/Unit/TransactionRules/Actions/ConvertToWithdrawalTest.php b/tests/Unit/TransactionRules/Actions/ConvertToWithdrawalTest.php new file mode 100644 index 0000000000..720cb0e80d --- /dev/null +++ b/tests/Unit/TransactionRules/Actions/ConvertToWithdrawalTest.php @@ -0,0 +1,124 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\TransactionRules\Actions; + + +use Exception; +use FireflyIII\Factory\AccountFactory; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\TransactionType; +use FireflyIII\TransactionRules\Actions\ConvertToWithdrawal; +use Log; +use Tests\TestCase; + +/** + * + * Class ConvertToWithdrawalTest + */ +class ConvertToWithdrawalTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', \get_class($this))); + } + + /** + * Convert a deposit to a withdrawal. + * + * @covers \FireflyIII\TransactionRules\Actions\ConvertToWithdrawal + */ + public function testActDeposit() + { + $expense = $this->getRandomExpense(); + $name = 'Random expense #' . random_int(1, 10000); + $deposit = $this->getRandomDeposit(); + + // journal is a deposit: + $this->assertEquals(TransactionType::DEPOSIT, $deposit->transactionType->type); + + // mock used stuff: + $factory = $this->mock(AccountFactory::class); + $factory->shouldReceive('setUser')->once(); + $factory->shouldReceive('findOrCreate')->once()->withArgs([$name, AccountType::EXPENSE])->andReturn($expense); + + + // fire the action: + $ruleAction = new RuleAction; + $ruleAction->action_value = $name; + $action = new ConvertToWithdrawal($ruleAction); + try { + $result = $action->act($deposit); + } catch (Exception $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertTrue($result); + + // journal is now a withdrawal. + $deposit->refresh(); + $this->assertEquals(TransactionType::WITHDRAWAL, $deposit->transactionType->type); + } + + /** + * Convert a transfer to a deposit. + * + * @covers \FireflyIII\TransactionRules\Actions\ConvertToWithdrawal + */ + public function testActTransfer() + { + $expense = $this->getRandomExpense(); + $name = 'Random expense #' . random_int(1, 10000); + $transfer = $this->getRandomTransfer(); + + // journal is a transfer: + $this->assertEquals(TransactionType::TRANSFER, $transfer->transactionType->type); + + // mock used stuff: + $factory = $this->mock(AccountFactory::class); + $factory->shouldReceive('setUser')->once(); + $factory->shouldReceive('findOrCreate')->once()->withArgs([$name, AccountType::EXPENSE])->andReturn($expense); + + + // fire the action: + $ruleAction = new RuleAction; + $ruleAction->action_value = $name; + $action = new ConvertToWithdrawal($ruleAction); + try { + $result = $action->act($transfer); + } catch (Exception $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertTrue($result); + + // journal is now a deposit. + $transfer->refresh(); + $this->assertEquals(TransactionType::WITHDRAWAL, $transfer->transactionType->type); + } + + +} \ No newline at end of file