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 @@
{{ errors.first('rule-action.'~count) }}
- {% endif %} - #} - + {# todo error when invalid name. #}- {{ errors.first('rule-trigger-value.'~count) }} + {{ errors.first('rule_triggers.'~count~'.value') }}
{% endif %} - #}