diff --git a/app/Console/Commands/Correction/FixIbans.php b/app/Console/Commands/Correction/FixIbans.php index f527e84f9a..7750abb7c0 100644 --- a/app/Console/Commands/Correction/FixIbans.php +++ b/app/Console/Commands/Correction/FixIbans.php @@ -25,7 +25,9 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; use Illuminate\Console\Command; +use Illuminate\Support\Collection; /** * Class FixIbans @@ -53,6 +55,52 @@ class FixIbans extends Command public function handle(): int { $accounts = Account::whereNotNull('iban')->get(); + $this->filterIbans($accounts); + $this->countAndCorrectIbans($accounts); + + return 0; + } + + /** + * @param Collection $accounts + * @return void + */ + private function countAndCorrectIbans(Collection $accounts): void + { + $set = []; + /** @var Account $account */ + foreach($accounts as $account) { + $userId = (int)$account->user_id; + $set[$userId] = $set[$userId] ?? []; + $iban = (string)$account->iban; + $type = $account->accountType->type; + if(in_array($type, [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE])) { + $type = 'liabilities'; + } + if(array_key_exists($iban, $set[$userId])) { + // iban already in use! two exceptions exist: + if( + !(AccountType::EXPENSE === $set[$userId][$iban] && AccountType::REVENUE === $type) && // allowed combination + !(AccountType::REVENUE === $set[$userId][$iban] && AccountType::EXPENSE === $type) // also allowed combination. + ){ + $this->line(sprintf('IBAN "%s" is used more than once and will be removed from %s #%d ("%s")', $iban, $account->accountType->type, $account->id, $account->name)); + $account->iban = null; + $account->save(); + } + } + + if(!array_key_exists($iban, $set[$userId])) { + $set[$userId][$iban] = $type; + } + } + } + + /** + * @param Collection $accounts + * @return void + */ + private function filterIbans(Collection $accounts): void + { /** @var Account $account */ foreach ($accounts as $account) { $iban = $account->iban; @@ -65,7 +113,5 @@ class FixIbans extends Command } } } - - return 0; } } diff --git a/app/Rules/UniqueIban.php b/app/Rules/UniqueIban.php index 75c247d82d..114d0e661a 100644 --- a/app/Rules/UniqueIban.php +++ b/app/Rules/UniqueIban.php @@ -23,18 +23,20 @@ declare(strict_types=1); namespace FireflyIII\Rules; +use Closure; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use Illuminate\Contracts\Validation\Rule; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Support\Facades\Log; /** * Class UniqueIban */ -class UniqueIban implements Rule +class UniqueIban implements ValidationRule { private ?Account $account; - private ?string $expectedType; + private array $expectedTypes; /** * Create a new rule instance. @@ -46,16 +48,22 @@ class UniqueIban implements Rule public function __construct(?Account $account, ?string $expectedType) { $this->account = $account; - $this->expectedType = $expectedType; + if(null === $expectedType){ + return; + } + $this->expectedTypes = [$expectedType]; // a very basic fix to make sure we get the correct account type: if ('expense' === $expectedType) { - $this->expectedType = AccountType::EXPENSE; + $this->expectedTypes = [AccountType::EXPENSE]; } if ('revenue' === $expectedType) { - $this->expectedType = AccountType::REVENUE; + $this->expectedTypes = [AccountType::REVENUE]; } if ('asset' === $expectedType) { - $this->expectedType = AccountType::ASSET; + $this->expectedTypes = [AccountType::ASSET]; + } + if ('liabilities' === $expectedType) { + $this->expectedTypes = [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]; } } @@ -84,7 +92,7 @@ class UniqueIban implements Rule if (!auth()->check()) { return true; } - if (null === $this->expectedType) { + if (0 === count($this->expectedTypes)) { return true; } $maxCounts = $this->getMaxOccurrences(); @@ -95,11 +103,11 @@ class UniqueIban implements Rule if ($count > $max) { Log::debug( sprintf( - 'IBAN "%s" is in use with %d account(s) of type "%s", which is too much for expected type "%s"', + 'IBAN "%s" is in use with %d account(s) of type "%s", which is too much for expected types "%s"', $value, $count, $type, - $this->expectedType + join(', ', $this->expectedTypes) ) ); @@ -120,14 +128,15 @@ class UniqueIban implements Rule AccountType::ASSET => 0, AccountType::EXPENSE => 0, AccountType::REVENUE => 0, + 'liabilities' => 0, ]; - if ('expense' === $this->expectedType || AccountType::EXPENSE === $this->expectedType) { + if (in_array('expense',$this->expectedTypes,true) ||in_array(AccountType::EXPENSE, $this->expectedTypes, true)) { // IBAN should be unique amongst expense and asset accounts. // may appear once in revenue accounts $maxCounts[AccountType::REVENUE] = 1; } - if ('revenue' === $this->expectedType || AccountType::REVENUE === $this->expectedType) { + if (in_array('revenue', $this->expectedTypes, true) || in_array(AccountType::REVENUE, $this->expectedTypes, true)) { // IBAN should be unique amongst revenue and asset accounts. // may appear once in expense accounts $maxCounts[AccountType::EXPENSE] = 1; @@ -144,12 +153,16 @@ class UniqueIban implements Rule */ private function countHits(string $type, string $iban): int { + $typesArray = [$type]; + if('liabilities' === $type) { + $typesArray = [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]; + } $query = auth()->user() ->accounts() ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') ->where('accounts.iban', $iban) - ->where('account_types.type', $type); + ->whereIn('account_types.type', $typesArray); if (null !== $this->account) { $query->where('accounts.id', '!=', $this->account->id); @@ -157,4 +170,14 @@ class UniqueIban implements Rule return $query->count(); } + + /** + * @inheritDoc + */ + public function validate(string $attribute, mixed $value, Closure $fail): void + { + if(!$this->passes($attribute, $value)) { + $fail((string)trans('validation.unique_iban_for_user')); + } + } } diff --git a/public/v1/lib/adminlte/css/skins/skin-light.css b/public/v1/lib/adminlte/css/skins/skin-light.css index 37f2d9db03..f7bec6aeaf 100644 --- a/public/v1/lib/adminlte/css/skins/skin-light.css +++ b/public/v1/lib/adminlte/css/skins/skin-light.css @@ -18,9 +18,6 @@ .skin-firefly-iii .ti-new-tag-input { background: #fff; } -.skin-firefly-iii input { - background: #ecf0f5 !important; -} .skin-firefly-iii .main-header .navbar { background-color: #3c8dbc; } diff --git a/public/v1/lib/adminlte/css/skins/skin-light.min.css b/public/v1/lib/adminlte/css/skins/skin-light.min.css index 9935e6992e..cc313b356a 100644 --- a/public/v1/lib/adminlte/css/skins/skin-light.min.css +++ b/public/v1/lib/adminlte/css/skins/skin-light.min.css @@ -1 +1 @@ -.skin-firefly-iii .money-neutral{color:#999}.skin-firefly-iii .money-positive{color:#3c763d}.skin-firefly-iii .money-negative{color:#a94442}.skin-firefly-iii .money-transfer{color:#31708f}.skin-firefly-iii .ti-new-tag-input{background:#fff}.skin-firefly-iii input{background:#ecf0f5 !important}.skin-firefly-iii .main-header .navbar{background-color:#3c8dbc}.skin-firefly-iii .main-header .navbar .nav>li>a{color:#fff}.skin-firefly-iii .main-header .navbar .nav>li>a:hover,.skin-firefly-iii .main-header .navbar .nav>li>a:active,.skin-firefly-iii .main-header .navbar .nav>li>a:focus,.skin-firefly-iii .main-header .navbar .nav .open>a,.skin-firefly-iii .main-header .navbar .nav .open>a:hover,.skin-firefly-iii .main-header .navbar .nav .open>a:focus,.skin-firefly-iii .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-firefly-iii .main-header .navbar .sidebar-toggle{color:#fff}.skin-firefly-iii .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-firefly-iii .main-header .navbar .sidebar-toggle{color:#fff}.skin-firefly-iii .main-header .navbar .sidebar-toggle:hover{background-color:#367fa9}@media (max-width:767px){.skin-firefly-iii .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-firefly-iii .main-header .navbar .dropdown-menu li a{color:#fff}.skin-firefly-iii .main-header .navbar .dropdown-menu li a:hover{background:#367fa9}}.skin-firefly-iii .main-header .logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-firefly-iii .main-header .logo:hover{background-color:#3b8ab8}.skin-firefly-iii .main-header li.user-header{background-color:#3c8dbc}.skin-firefly-iii .content-header{background:transparent}.skin-firefly-iii .wrapper,.skin-firefly-iii .main-sidebar,.skin-firefly-iii .left-side{background-color:#f9fafc}.skin-firefly-iii .main-sidebar{border-right:1px solid #d2d6de}.skin-firefly-iii .user-panel>.info,.skin-firefly-iii .user-panel>.info>a{color:#444}.skin-firefly-iii .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-firefly-iii .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-firefly-iii .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-firefly-iii .sidebar-menu>li:hover>a,.skin-firefly-iii .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-firefly-iii .sidebar-menu>li.active{border-left-color:#3c8dbc}.skin-firefly-iii .sidebar-menu>li.active>a{font-weight:600}.skin-firefly-iii .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-firefly-iii .sidebar a{color:#444}.skin-firefly-iii .sidebar a:hover{text-decoration:none}.skin-firefly-iii .sidebar-menu .treeview-menu>li>a{color:#777}.skin-firefly-iii .sidebar-menu .treeview-menu>li.active>a,.skin-firefly-iii .sidebar-menu .treeview-menu>li>a:hover{color:#000}.skin-firefly-iii .sidebar-menu .treeview-menu>li.active>a{font-weight:600}.skin-firefly-iii .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-firefly-iii .sidebar-form input[type="text"],.skin-firefly-iii .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-firefly-iii .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-firefly-iii .sidebar-form input[type="text"]:focus,.skin-firefly-iii .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-firefly-iii .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-firefly-iii .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-firefly-iii.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}}.skin-firefly-iii .main-footer{border-top-color:#d2d6de}.skin-blue.layout-top-nav .main-header>.logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#3b8ab8} \ No newline at end of file +.skin-firefly-iii .money-neutral{color:#999}.skin-firefly-iii .money-positive{color:#3c763d}.skin-firefly-iii .money-negative{color:#a94442}.skin-firefly-iii .money-transfer{color:#31708f}.skin-firefly-iii .ti-new-tag-input{background:#fff}.skin-firefly-iii .main-header .navbar{background-color:#3c8dbc}.skin-firefly-iii .main-header .navbar .nav>li>a{color:#fff}.skin-firefly-iii .main-header .navbar .nav>li>a:hover,.skin-firefly-iii .main-header .navbar .nav>li>a:active,.skin-firefly-iii .main-header .navbar .nav>li>a:focus,.skin-firefly-iii .main-header .navbar .nav .open>a,.skin-firefly-iii .main-header .navbar .nav .open>a:hover,.skin-firefly-iii .main-header .navbar .nav .open>a:focus,.skin-firefly-iii .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-firefly-iii .main-header .navbar .sidebar-toggle{color:#fff}.skin-firefly-iii .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-firefly-iii .main-header .navbar .sidebar-toggle{color:#fff}.skin-firefly-iii .main-header .navbar .sidebar-toggle:hover{background-color:#367fa9}@media (max-width:767px){.skin-firefly-iii .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-firefly-iii .main-header .navbar .dropdown-menu li a{color:#fff}.skin-firefly-iii .main-header .navbar .dropdown-menu li a:hover{background:#367fa9}}.skin-firefly-iii .main-header .logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-firefly-iii .main-header .logo:hover{background-color:#3b8ab8}.skin-firefly-iii .main-header li.user-header{background-color:#3c8dbc}.skin-firefly-iii .content-header{background:transparent}.skin-firefly-iii .wrapper,.skin-firefly-iii .main-sidebar,.skin-firefly-iii .left-side{background-color:#f9fafc}.skin-firefly-iii .main-sidebar{border-right:1px solid #d2d6de}.skin-firefly-iii .user-panel>.info,.skin-firefly-iii .user-panel>.info>a{color:#444}.skin-firefly-iii .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-firefly-iii .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-firefly-iii .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-firefly-iii .sidebar-menu>li:hover>a,.skin-firefly-iii .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-firefly-iii .sidebar-menu>li.active{border-left-color:#3c8dbc}.skin-firefly-iii .sidebar-menu>li.active>a{font-weight:600}.skin-firefly-iii .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-firefly-iii .sidebar a{color:#444}.skin-firefly-iii .sidebar a:hover{text-decoration:none}.skin-firefly-iii .sidebar-menu .treeview-menu>li>a{color:#777}.skin-firefly-iii .sidebar-menu .treeview-menu>li.active>a,.skin-firefly-iii .sidebar-menu .treeview-menu>li>a:hover{color:#000}.skin-firefly-iii .sidebar-menu .treeview-menu>li.active>a{font-weight:600}.skin-firefly-iii .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-firefly-iii .sidebar-form input[type="text"],.skin-firefly-iii .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-firefly-iii .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-firefly-iii .sidebar-form input[type="text"]:focus,.skin-firefly-iii .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-firefly-iii .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-firefly-iii .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-firefly-iii.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}}.skin-firefly-iii .main-footer{border-top-color:#d2d6de}.skin-blue.layout-top-nav .main-header>.logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#3b8ab8} \ No newline at end of file