diff --git a/.ci/php-cs-fixer/.php-cs-fixer.php b/.ci/php-cs-fixer/.php-cs-fixer.php index 6d71a455ff..6193477e73 100644 --- a/.ci/php-cs-fixer/.php-cs-fixer.php +++ b/.ci/php-cs-fixer/.php-cs-fixer.php @@ -29,7 +29,7 @@ $paths = [ $current . '/../../database', $current . '/../../routes', $current . '/../../tests', - $current . '/../../resources/lang', + $current . '/../../resources/lang/en_US', ]; $finder = PhpCsFixer\Finder::create() diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock index 916c20c88e..d63d6ea412 100644 --- a/.ci/php-cs-fixer/composer.lock +++ b/.ci/php-cs-fixer/composer.lock @@ -97,13 +97,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { @@ -406,16 +406,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.65.0", + "version": "v3.66.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "79d4f3e77b250a7d8043d76c6af8f0695e8a469f" + "reference": "5f5f2a142ff36b93c41885bca29cc5f861c013e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/79d4f3e77b250a7d8043d76c6af8f0695e8a469f", - "reference": "79d4f3e77b250a7d8043d76c6af8f0695e8a469f", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/5f5f2a142ff36b93c41885bca29cc5f861c013e6", + "reference": "5f5f2a142ff36b93c41885bca29cc5f861c013e6", "shasum": "" }, "require": { @@ -441,7 +441,7 @@ "symfony/polyfill-mbstring": "^1.28", "symfony/polyfill-php80": "^1.28", "symfony/polyfill-php81": "^1.28", - "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0 <7.2", "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { @@ -497,7 +497,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.65.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.66.0" }, "funding": [ { @@ -505,7 +505,7 @@ "type": "github" } ], - "time": "2024-11-25T00:39:24+00:00" + "time": "2024-12-29T13:46:23+00:00" }, { "name": "psr/container", @@ -1369,12 +1369,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1517,12 +1517,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -2246,16 +2246,16 @@ }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/42783370fda6e538771f7c7a36e9fa2ee3a84892", + "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892", "shasum": "" }, "require": { @@ -2287,7 +2287,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.1.8" }, "funding": [ { @@ -2303,7 +2303,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-11-06T14:23:19+00:00" }, { "name": "symfony/service-contracts", @@ -2329,12 +2329,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { diff --git a/.ci/phpcs.sh b/.ci/phpcs.sh index 1639ec4a49..9bb2c33719 100755 --- a/.ci/phpcs.sh +++ b/.ci/phpcs.sh @@ -26,8 +26,7 @@ SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd $SCRIPT_DIR/php-cs-fixer composer update --quiet rm -f .php-cs-fixer.cache -PHP_CS_FIXER_IGNORE_ENV=true -./vendor/bin/php-cs-fixer fix \ +PHP_CS_FIXER_IGNORE_ENV=true ./vendor/bin/php-cs-fixer fix \ --config $SCRIPT_DIR/php-cs-fixer/.php-cs-fixer.php \ --format=txt \ --allow-risky=yes diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 41ef4cfeb9..2f7ca3e532 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -19,7 +19,7 @@ jobs: - name: Setup PHP with Xdebug uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' coverage: xdebug extensions: >- bcmath diff --git a/THANKS.md b/THANKS.md index 6d853990d6..d18178f45d 100755 --- a/THANKS.md +++ b/THANKS.md @@ -4,6 +4,7 @@ Over time, many people have contributed to Firefly III. Their efforts are not al Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution. ## 2024 +- TasneemTantawy - Antônio Franco - yparitcher - Jhon Pedroza diff --git a/app/Api/V1/Controllers/Autocomplete/AccountController.php b/app/Api/V1/Controllers/Autocomplete/AccountController.php index e4f2edf325..b859f661bb 100644 --- a/app/Api/V1/Controllers/Autocomplete/AccountController.php +++ b/app/Api/V1/Controllers/Autocomplete/AccountController.php @@ -26,10 +26,11 @@ namespace FireflyIII\Api\V1\Controllers\Autocomplete; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteRequest; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\User; use Illuminate\Http\JsonResponse; @@ -61,7 +62,7 @@ class AccountController extends Controller return $next($request); } ); - $this->balanceTypes = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]; + $this->balanceTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; } /** @@ -73,40 +74,40 @@ class AccountController extends Controller */ public function accounts(AutocompleteRequest $request): JsonResponse { - $data = $request->getData(); - $types = $data['types']; - $query = $data['query']; - $date = $data['date'] ?? today(config('app.timezone')); - $return = []; - $result = $this->repository->searchAccount((string)$query, $types, $this->parameters->get('limit')); - - // TODO this code is duplicated in the V2 Autocomplete controller, which means this code is due to be deprecated. - $defaultCurrency = app('amount')->getDefaultCurrency(); + $data = $request->getData(); + $types = $data['types']; + $query = $data['query']; + $date = $data['date'] ?? today(config('app.timezone')); + $return = []; + $result = $this->repository->searchAccount((string) $query, $types, $this->parameters->get('limit')); /** @var Account $account */ foreach ($result as $account) { $nameWithBalance = $account->name; - $currency = $this->repository->getAccountCurrency($account) ?? $defaultCurrency; - + $currency = $this->repository->getAccountCurrency($account) ?? $this->defaultCurrency; + $useCurrency = $currency; if (in_array($account->accountType->type, $this->balanceTypes, true)) { - $balance = app('steam')->balance($account, $date); + $balance = Steam::finalAccountBalance($account, $date); + $key = $this->convertToNative && $currency->id !== $this->defaultCurrency->id ? 'native_balance' : 'balance'; + $useCurrency = $this->convertToNative && $currency->id !== $this->defaultCurrency->id ? $this->defaultCurrency : $currency; + $amount = $balance[$key] ?? '0'; $nameWithBalance = sprintf( '%s (%s)', $account->name, - app('amount')->formatAnything($currency, $balance, false) + app('amount')->formatAnything($useCurrency, $amount, false) ); } $return[] = [ - 'id' => (string)$account->id, + 'id' => (string) $account->id, 'name' => $account->name, 'name_with_balance' => $nameWithBalance, 'type' => $account->accountType->type, - 'currency_id' => (string)$currency->id, - 'currency_name' => $currency->name, - 'currency_code' => $currency->code, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, + 'currency_id' => (string) $useCurrency->id, + 'currency_name' => $useCurrency->name, + 'currency_code' => $useCurrency->code, + 'currency_symbol' => $useCurrency->symbol, + 'currency_decimal_places' => $useCurrency->decimal_places, ]; } @@ -114,9 +115,9 @@ class AccountController extends Controller usort( $return, static function (array $left, array $right) { - $order = [AccountType::ASSET, AccountType::REVENUE, AccountType::EXPENSE]; - $posA = (int)array_search($left['type'], $order, true); - $posB = (int)array_search($right['type'], $order, true); + $order = [AccountTypeEnum::ASSET->value, AccountTypeEnum::REVENUE->value, AccountTypeEnum::EXPENSE->value]; + $posA = (int) array_search($left['type'], $order, true); + $posB = (int) array_search($right['type'], $order, true); return $posA - $posB; } diff --git a/app/Api/V1/Controllers/Autocomplete/BillController.php b/app/Api/V1/Controllers/Autocomplete/BillController.php index bb4c63d94b..36904d1a89 100644 --- a/app/Api/V1/Controllers/Autocomplete/BillController.php +++ b/app/Api/V1/Controllers/Autocomplete/BillController.php @@ -67,7 +67,7 @@ class BillController extends Controller $filtered = $result->map( static function (Bill $item) { return [ - 'id' => (string)$item->id, + 'id' => (string) $item->id, 'name' => $item->name, 'active' => $item->active, ]; diff --git a/app/Api/V1/Controllers/Autocomplete/BudgetController.php b/app/Api/V1/Controllers/Autocomplete/BudgetController.php index 1a6e44a5f5..4620955a22 100644 --- a/app/Api/V1/Controllers/Autocomplete/BudgetController.php +++ b/app/Api/V1/Controllers/Autocomplete/BudgetController.php @@ -67,7 +67,7 @@ class BudgetController extends Controller $filtered = $result->map( static function (Budget $item) { return [ - 'id' => (string)$item->id, + 'id' => (string) $item->id, 'name' => $item->name, ]; } diff --git a/app/Api/V1/Controllers/Autocomplete/CategoryController.php b/app/Api/V1/Controllers/Autocomplete/CategoryController.php index 1213ae002d..a91a2231ed 100644 --- a/app/Api/V1/Controllers/Autocomplete/CategoryController.php +++ b/app/Api/V1/Controllers/Autocomplete/CategoryController.php @@ -67,7 +67,7 @@ class CategoryController extends Controller $filtered = $result->map( static function (Category $item) { return [ - 'id' => (string)$item->id, + 'id' => (string) $item->id, 'name' => $item->name, ]; } diff --git a/app/Api/V1/Controllers/Autocomplete/CurrencyController.php b/app/Api/V1/Controllers/Autocomplete/CurrencyController.php index 5fe00809e7..2170b5dd5a 100644 --- a/app/Api/V1/Controllers/Autocomplete/CurrencyController.php +++ b/app/Api/V1/Controllers/Autocomplete/CurrencyController.php @@ -69,7 +69,7 @@ class CurrencyController extends Controller /** @var TransactionCurrency $currency */ foreach ($collection as $currency) { $result[] = [ - 'id' => (string)$currency->id, + 'id' => (string) $currency->id, 'name' => $currency->name, 'code' => $currency->code, 'symbol' => $currency->symbol, @@ -95,7 +95,7 @@ class CurrencyController extends Controller /** @var TransactionCurrency $currency */ foreach ($collection as $currency) { $result[] = [ - 'id' => (string)$currency->id, + 'id' => (string) $currency->id, 'name' => sprintf('%s (%s)', $currency->name, $currency->code), 'code' => $currency->code, 'symbol' => $currency->symbol, diff --git a/app/Api/V1/Controllers/Autocomplete/ObjectGroupController.php b/app/Api/V1/Controllers/Autocomplete/ObjectGroupController.php index cea2a3d9ac..5f1014502b 100644 --- a/app/Api/V1/Controllers/Autocomplete/ObjectGroupController.php +++ b/app/Api/V1/Controllers/Autocomplete/ObjectGroupController.php @@ -69,7 +69,7 @@ class ObjectGroupController extends Controller /** @var ObjectGroup $objectGroup */ foreach ($result as $objectGroup) { $return[] = [ - 'id' => (string)$objectGroup->id, + 'id' => (string) $objectGroup->id, 'name' => $objectGroup->title, 'title' => $objectGroup->title, ]; diff --git a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php index 3373d6c708..6565ba78f8 100644 --- a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php +++ b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php @@ -66,24 +66,23 @@ class PiggyBankController extends Controller */ public function piggyBanks(AutocompleteRequest $request): JsonResponse { - $data = $request->getData(); - $piggies = $this->piggyRepository->searchPiggyBank($data['query'], $this->parameters->get('limit')); - $defaultCurrency = app('amount')->getDefaultCurrency(); - $response = []; + $data = $request->getData(); + $piggies = $this->piggyRepository->searchPiggyBank($data['query'], $this->parameters->get('limit')); + $response = []; /** @var PiggyBank $piggy */ foreach ($piggies as $piggy) { - $currency = $this->accountRepository->getAccountCurrency($piggy->account) ?? $defaultCurrency; + $currency = $piggy->transactionCurrency; $objectGroup = $piggy->objectGroups()->first(); $response[] = [ - 'id' => (string)$piggy->id, + 'id' => (string) $piggy->id, 'name' => $piggy->name, - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_name' => $currency->name, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, - 'object_group_id' => null === $objectGroup ? null : (string)$objectGroup->id, + 'object_group_id' => null === $objectGroup ? null : (string) $objectGroup->id, 'object_group_title' => $objectGroup?->title, ]; } @@ -97,31 +96,30 @@ class PiggyBankController extends Controller */ public function piggyBanksWithBalance(AutocompleteRequest $request): JsonResponse { - $data = $request->getData(); - $piggies = $this->piggyRepository->searchPiggyBank($data['query'], $this->parameters->get('limit')); - $defaultCurrency = app('amount')->getDefaultCurrency(); - $response = []; + $data = $request->getData(); + $piggies = $this->piggyRepository->searchPiggyBank($data['query'], $this->parameters->get('limit')); + $response = []; /** @var PiggyBank $piggy */ foreach ($piggies as $piggy) { - $currency = $this->accountRepository->getAccountCurrency($piggy->account) ?? $defaultCurrency; - $currentAmount = $this->piggyRepository->getRepetition($piggy)->currentamount ?? '0'; + $currency = $piggy->transactionCurrency; + $currentAmount = $this->piggyRepository->getCurrentAmount($piggy); $objectGroup = $piggy->objectGroups()->first(); $response[] = [ - 'id' => (string)$piggy->id, + 'id' => (string) $piggy->id, 'name' => $piggy->name, 'name_with_balance' => sprintf( '%s (%s / %s)', $piggy->name, app('amount')->formatAnything($currency, $currentAmount, false), - app('amount')->formatAnything($currency, $piggy->targetamount, false), + app('amount')->formatAnything($currency, $piggy->target_amount, false), ), - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_name' => $currency->name, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, - 'object_group_id' => null === $objectGroup ? null : (string)$objectGroup->id, + 'object_group_id' => null === $objectGroup ? null : (string) $objectGroup->id, 'object_group_title' => $objectGroup?->title, ]; } diff --git a/app/Api/V1/Controllers/Autocomplete/RecurrenceController.php b/app/Api/V1/Controllers/Autocomplete/RecurrenceController.php index 032490594d..c9c6f2d4f1 100644 --- a/app/Api/V1/Controllers/Autocomplete/RecurrenceController.php +++ b/app/Api/V1/Controllers/Autocomplete/RecurrenceController.php @@ -67,7 +67,7 @@ class RecurrenceController extends Controller /** @var Recurrence $recurrence */ foreach ($recurrences as $recurrence) { $response[] = [ - 'id' => (string)$recurrence->id, + 'id' => (string) $recurrence->id, 'name' => $recurrence->title, 'description' => $recurrence->description, ]; diff --git a/app/Api/V1/Controllers/Autocomplete/RuleController.php b/app/Api/V1/Controllers/Autocomplete/RuleController.php index 2dc08d3847..5a727f0ee9 100644 --- a/app/Api/V1/Controllers/Autocomplete/RuleController.php +++ b/app/Api/V1/Controllers/Autocomplete/RuleController.php @@ -66,7 +66,7 @@ class RuleController extends Controller /** @var Rule $rule */ foreach ($rules as $rule) { $response[] = [ - 'id' => (string)$rule->id, + 'id' => (string) $rule->id, 'name' => $rule->title, 'description' => $rule->description, ]; diff --git a/app/Api/V1/Controllers/Autocomplete/RuleGroupController.php b/app/Api/V1/Controllers/Autocomplete/RuleGroupController.php index 918ac5d5de..cd77505b8b 100644 --- a/app/Api/V1/Controllers/Autocomplete/RuleGroupController.php +++ b/app/Api/V1/Controllers/Autocomplete/RuleGroupController.php @@ -66,7 +66,7 @@ class RuleGroupController extends Controller /** @var RuleGroup $group */ foreach ($groups as $group) { $response[] = [ - 'id' => (string)$group->id, + 'id' => (string) $group->id, 'name' => $group->title, 'description' => $group->description, ]; diff --git a/app/Api/V1/Controllers/Autocomplete/TagController.php b/app/Api/V1/Controllers/Autocomplete/TagController.php index 4939126241..c086b6b71b 100644 --- a/app/Api/V1/Controllers/Autocomplete/TagController.php +++ b/app/Api/V1/Controllers/Autocomplete/TagController.php @@ -69,7 +69,7 @@ class TagController extends Controller /** @var Tag $tag */ foreach ($result as $tag) { $array[] = [ - 'id' => (string)$tag->id, + 'id' => (string) $tag->id, 'name' => $tag->tag, 'tag' => $tag->tag, ]; diff --git a/app/Api/V1/Controllers/Autocomplete/TransactionController.php b/app/Api/V1/Controllers/Autocomplete/TransactionController.php index bed8e6a701..1624d89d47 100644 --- a/app/Api/V1/Controllers/Autocomplete/TransactionController.php +++ b/app/Api/V1/Controllers/Autocomplete/TransactionController.php @@ -77,8 +77,8 @@ class TransactionController extends Controller /** @var TransactionJournal $journal */ foreach ($filtered as $journal) { $array[] = [ - 'id' => (string)$journal->id, - 'transaction_group_id' => (string)$journal->transaction_group_id, + 'id' => (string) $journal->id, + 'transaction_group_id' => (string) $journal->transaction_group_id, 'name' => $journal->description, 'description' => $journal->description, ]; @@ -97,7 +97,7 @@ class TransactionController extends Controller $result = new Collection(); if (is_numeric($data['query'])) { // search for group, not journal. - $firstResult = $this->groupRepository->find((int)$data['query']); + $firstResult = $this->groupRepository->find((int) $data['query']); if (null !== $firstResult) { // group may contain multiple journals, each a result: foreach ($firstResult->transactionJournals as $journal) { @@ -115,8 +115,8 @@ class TransactionController extends Controller /** @var TransactionJournal $journal */ foreach ($result as $journal) { $array[] = [ - 'id' => (string)$journal->id, - 'transaction_group_id' => (string)$journal->transaction_group_id, + 'id' => (string) $journal->id, + 'transaction_group_id' => (string) $journal->transaction_group_id, 'name' => sprintf('#%d: %s', $journal->transaction_group_id, $journal->description), 'description' => sprintf('#%d: %s', $journal->transaction_group_id, $journal->description), ]; diff --git a/app/Api/V1/Controllers/Autocomplete/TransactionTypeController.php b/app/Api/V1/Controllers/Autocomplete/TransactionTypeController.php index 1ea77fae38..b34924b6d8 100644 --- a/app/Api/V1/Controllers/Autocomplete/TransactionTypeController.php +++ b/app/Api/V1/Controllers/Autocomplete/TransactionTypeController.php @@ -66,7 +66,7 @@ class TransactionTypeController extends Controller foreach ($types as $type) { // different key for consistency. $array[] = [ - 'id' => (string)$type->id, + 'id' => (string) $type->id, 'name' => $type->type, 'type' => $type->type, ]; diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php index 3d093e0bba..e47ae5d23e 100644 --- a/app/Api/V1/Controllers/Chart/AccountController.php +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -27,9 +27,9 @@ namespace FireflyIII\Api\V1\Controllers\Chart; use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Data\DateRequest; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\Preference; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Http\Api\ApiSupport; @@ -81,11 +81,10 @@ class AccountController extends Controller $end = $dates['end']; // user's preferences - $defaultSet = $this->repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray(); + $defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); /** @var Preference $frontpage */ $frontpage = app('preferences')->get('frontpageAccounts', $defaultSet); - $default = app('amount')->getDefaultCurrency(); if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) { $frontpage->data = $defaultSet; @@ -98,13 +97,11 @@ class AccountController extends Controller /** @var Account $account */ foreach ($accounts as $account) { - $currency = $this->repository->getAccountCurrency($account); - if (null === $currency) { - $currency = $default; - } + $currency = $this->repository->getAccountCurrency($account) ?? $this->defaultCurrency; + $field = $this->convertToNative && $currency->id !== $this->defaultCurrency->id ? 'native_balance' : 'balance'; $currentSet = [ 'label' => $account->name, - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, @@ -116,13 +113,12 @@ class AccountController extends Controller ]; // TODO this code is also present in the V2 chart account controller so this method is due to be deprecated. $currentStart = clone $start; - $range = app('steam')->balanceInRange($account, $start, clone $end); - // 2022-10-11 this method no longer converts to float. - $previous = array_values($range)[0]; + $range = app('steam')->finalAccountBalanceInRange($account, $start, clone $end, $this->convertToNative); + $previous = array_values($range)[0][$field]; while ($currentStart <= $end) { $format = $currentStart->format('Y-m-d'); $label = $currentStart->toAtomString(); - $balance = array_key_exists($format, $range) ? $range[$format] : $previous; + $balance = array_key_exists($format, $range) ? $range[$format][$field] : $previous; $previous = $balance; $currentStart->addDay(); $currentSet['entries'][$label] = $balance; diff --git a/app/Api/V1/Controllers/Controller.php b/app/Api/V1/Controllers/Controller.php index 74160af153..fcf5b21ef2 100644 --- a/app/Api/V1/Controllers/Controller.php +++ b/app/Api/V1/Controllers/Controller.php @@ -28,6 +28,9 @@ use Carbon\Carbon; use Carbon\Exceptions\InvalidDateException; use Carbon\Exceptions\InvalidFormatException; use FireflyIII\Models\Preference; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; @@ -50,11 +53,13 @@ abstract class Controller extends BaseController use DispatchesJobs; use ValidatesRequests; - protected const string CONTENT_TYPE = 'application/vnd.api+json'; + protected const string CONTENT_TYPE = 'application/vnd.api+json'; /** @var array */ protected array $allowedSort; protected ParameterBag $parameters; + protected bool $convertToNative = false; + protected TransactionCurrency $defaultCurrency; /** * Controller constructor. @@ -67,8 +72,11 @@ abstract class Controller extends BaseController function ($request, $next) { $this->parameters = $this->getParameters(); if (auth()->check()) { - $language = app('steam')->getLanguage(); + $language = Steam::getLanguage(); + $this->convertToNative = Amount::convertToNative(); + $this->defaultCurrency = Amount::getDefaultCurrency(); app()->setLocale($language); + } return $next($request); @@ -82,7 +90,7 @@ abstract class Controller extends BaseController private function getParameters(): ParameterBag { $bag = new ParameterBag(); - $page = (int)request()->get('page'); + $page = (int) request()->get('page'); if ($page < 1) { $page = 1; } @@ -107,13 +115,13 @@ abstract class Controller extends BaseController $obj = null; if (null !== $date) { try { - $obj = Carbon::parse((string)$date); + $obj = Carbon::parse((string) $date); } catch (InvalidDateException|InvalidFormatException $e) { // don't care app('log')->warning( sprintf( 'Ignored invalid date "%s" in API controller parameter check: %s', - substr((string)$date, 0, 20), + substr((string) $date, 0, 20), $e->getMessage() ) ); @@ -134,7 +142,7 @@ abstract class Controller extends BaseController $value = null; } if (null !== $value) { - $bag->set($integer, (int)$value); + $bag->set($integer, (int) $value); } if (null === $value && 'limit' === $integer // @phpstan-ignore-line @@ -144,7 +152,7 @@ abstract class Controller extends BaseController $user = auth()->user(); /** @var Preference $pageSize */ - $pageSize = (int)app('preferences')->getForUser($user, 'listPageSize', 50)->data; + $pageSize = (int) app('preferences')->getForUser($user, 'listPageSize', 50)->data; $bag->set($integer, $pageSize); } } @@ -158,7 +166,7 @@ abstract class Controller extends BaseController $sortParameters = []; try { - $param = (string)request()->query->get('sort'); + $param = (string) request()->query->get('sort'); } catch (BadRequestException $e) { app('log')->error('Request field "sort" contains a non-scalar value. Value set to NULL.'); app('log')->error($e->getMessage()); diff --git a/app/Api/V1/Controllers/Data/Bulk/TransactionController.php b/app/Api/V1/Controllers/Data/Bulk/TransactionController.php index 869c17ae87..f6ae26a5e2 100644 --- a/app/Api/V1/Controllers/Data/Bulk/TransactionController.php +++ b/app/Api/V1/Controllers/Data/Bulk/TransactionController.php @@ -70,8 +70,8 @@ class TransactionController extends Controller // to respond to what is in the $query. // this is OK because only one thing can be in the query at the moment. if ($this->isUpdateTransactionAccount($params)) { - $original = $this->repository->find((int)$params['where']['account_id']); - $destination = $this->repository->find((int)$params['update']['account_id']); + $original = $this->repository->find((int) $params['where']['account_id']); + $destination = $this->repository->find((int) $params['update']['account_id']); /** @var AccountDestroyService $service */ $service = app(AccountDestroyService::class); diff --git a/app/Api/V1/Controllers/Data/DestroyController.php b/app/Api/V1/Controllers/Data/DestroyController.php index 8296d0ee68..c83e18d079 100644 --- a/app/Api/V1/Controllers/Data/DestroyController.php +++ b/app/Api/V1/Controllers/Data/DestroyController.php @@ -26,10 +26,10 @@ namespace FireflyIII\Api\V1\Controllers\Data; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Data\DestroyRequest; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; @@ -66,9 +66,9 @@ class DestroyController extends Controller $objects = $request->getObjects(); $this->unused = $request->boolean('unused', false); - $allExceptAssets = [AccountType::BENEFICIARY, AccountType::CASH, AccountType::CREDITCARD, AccountType::DEFAULT, AccountType::EXPENSE, AccountType::IMPORT, AccountType::INITIAL_BALANCE, AccountType::LIABILITY_CREDIT, AccountType::RECONCILIATION, AccountType::REVENUE]; - $all = [AccountType::ASSET, AccountType::BENEFICIARY, AccountType::CASH, AccountType::CREDITCARD, AccountType::DEBT, AccountType::DEFAULT, AccountType::EXPENSE, AccountType::IMPORT, AccountType::INITIAL_BALANCE, AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::RECONCILIATION]; - $liabilities = [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD]; + $allExceptAssets = [AccountTypeEnum::BENEFICIARY->value, AccountTypeEnum::CASH->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::IMPORT->value, AccountTypeEnum::INITIAL_BALANCE->value, AccountTypeEnum::LIABILITY_CREDIT->value, AccountTypeEnum::RECONCILIATION->value, AccountTypeEnum::REVENUE->value]; + $all = [AccountTypeEnum::ASSET->value, AccountTypeEnum::BENEFICIARY->value, AccountTypeEnum::CASH->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::IMPORT->value, AccountTypeEnum::INITIAL_BALANCE->value, AccountTypeEnum::LIABILITY_CREDIT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::RECONCILIATION->value]; + $liabilities = [AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::CREDITCARD->value]; $transactions = [TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value, TransactionTypeEnum::RECONCILIATION->value]; match ($objects) { @@ -82,9 +82,9 @@ class DestroyController extends Controller 'object_groups' => $this->destroyObjectGroups(), 'not_assets_liabilities' => $this->destroyAccounts($allExceptAssets), 'accounts' => $this->destroyAccounts($all), - 'asset_accounts' => $this->destroyAccounts([AccountType::ASSET, AccountType::DEFAULT]), - 'expense_accounts' => $this->destroyAccounts([AccountType::BENEFICIARY, AccountType::EXPENSE]), - 'revenue_accounts' => $this->destroyAccounts([AccountType::REVENUE]), + 'asset_accounts' => $this->destroyAccounts([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]), + 'expense_accounts' => $this->destroyAccounts([AccountTypeEnum::BENEFICIARY->value, AccountTypeEnum::EXPENSE->value]), + 'revenue_accounts' => $this->destroyAccounts([AccountTypeEnum::REVENUE->value]), 'liabilities' => $this->destroyAccounts($liabilities), 'transactions' => $this->destroyTransactions($transactions), 'withdrawals' => $this->destroyTransactions([TransactionTypeEnum::WITHDRAWAL->value]), diff --git a/app/Api/V1/Controllers/Data/Export/ExportController.php b/app/Api/V1/Controllers/Data/Export/ExportController.php index 8eda16cf8f..cfbe4414b1 100644 --- a/app/Api/V1/Controllers/Data/Export/ExportController.php +++ b/app/Api/V1/Controllers/Data/Export/ExportController.php @@ -88,7 +88,7 @@ class ExportController extends Controller ->header('Expires', '0') ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') ->header('Pragma', 'public') - ->header('Content-Length', (string)strlen($data[$key])) + ->header('Content-Length', (string) strlen($data[$key])) ; return $response; diff --git a/app/Api/V1/Controllers/Data/PurgeController.php b/app/Api/V1/Controllers/Data/PurgeController.php index 8d076b62cf..baba2f622a 100644 --- a/app/Api/V1/Controllers/Data/PurgeController.php +++ b/app/Api/V1/Controllers/Data/PurgeController.php @@ -29,13 +29,13 @@ use FireflyIII\Models\Account; use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; -use FireflyIII\Models\PiggyBank; use FireflyIII\Models\Recurrence; use FireflyIII\Models\Rule; use FireflyIII\Models\RuleGroup; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\User; use Illuminate\Http\JsonResponse; @@ -52,7 +52,7 @@ class PurgeController extends Controller public function purge(): JsonResponse { /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); // some manual code, too lazy to call all repositories. @@ -63,14 +63,17 @@ class PurgeController extends Controller Bill::whereUserId($user->id)->onlyTrashed()->forceDelete(); // piggies - $set = PiggyBank::leftJoin('accounts', 'accounts.id', 'piggy_banks.account_id') - ->where('accounts.user_id', $user->id)->onlyTrashed()->get(['piggy_banks.*']) - ; - - /** @var PiggyBank $piggy */ - foreach ($set as $piggy) { - $piggy->forceDelete(); - } + $repository = app(PiggyBankRepositoryInterface::class); + $repository->setUser($user); + $repository->purgeAll(); + // $set = PiggyBank::leftJoin('accounts', 'accounts.id', 'piggy_banks.account_id') + // ->where('accounts.user_id', $user->id)->onlyTrashed()->get(['piggy_banks.*']) + // ; + // + // /** @var PiggyBank $piggy */ + // foreach ($set as $piggy) { + // $piggy->forceDelete(); + // } // rule group RuleGroup::whereUserId($user->id)->onlyTrashed()->forceDelete(); diff --git a/app/Api/V1/Controllers/Insight/Expense/AccountController.php b/app/Api/V1/Controllers/Insight/Expense/AccountController.php index 6649561c78..323afc1e0f 100644 --- a/app/Api/V1/Controllers/Insight/Expense/AccountController.php +++ b/app/Api/V1/Controllers/Insight/Expense/AccountController.php @@ -79,11 +79,11 @@ class AccountController extends Controller /** @var array $expense */ foreach ($expenses as $expense) { $result[] = [ - 'id' => (string)$expense['id'], + 'id' => (string) $expense['id'], 'name' => $expense['name'], 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // intentional float - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // intentional float + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } @@ -107,11 +107,11 @@ class AccountController extends Controller /** @var array $expense */ foreach ($expenses as $expense) { $result[] = [ - 'id' => (string)$expense['id'], + 'id' => (string) $expense['id'], 'name' => $expense['name'], 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // intentional float - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // intentional float + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } diff --git a/app/Api/V1/Controllers/Insight/Expense/BillController.php b/app/Api/V1/Controllers/Insight/Expense/BillController.php index 2a764e0884..10a89eff60 100644 --- a/app/Api/V1/Controllers/Insight/Expense/BillController.php +++ b/app/Api/V1/Controllers/Insight/Expense/BillController.php @@ -29,7 +29,9 @@ use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Log; /** * Class BillController @@ -63,11 +65,13 @@ class BillController extends Controller */ public function bill(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $bills = $request->getBills(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $bills = $request->getBills(); + $start = $request->getStart(); + $end = $request->getEnd(); + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); + $response = []; // get all bills: if (0 === $bills->count()) { @@ -75,39 +79,42 @@ class BillController extends Controller } // collect all expenses in this period (regardless of type) by the given bills and accounts. - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value])->setRange($start, $end)->setSourceAccounts($accounts); $collector->setBills($bills); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $billId = (int)$journal['bill_id']; - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; - $key = sprintf('%d-%d', $billId, $currencyId); - $foreignKey = sprintf('%d-%d', $billId, $foreignCurrencyId); + $billId = (int) $journal['bill_id']; + $currencyId = (int) $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $field = 'amount'; + + // use the native amount if the user wants to convert to native currency + if ($convertToNative && $currencyId !== $default->id) { + $currencyId = $default->id; + $currencyCode = $default->code; + $field = 'native_amount'; + } + // use foreign amount when the foreign currency IS the default currency. + if ($convertToNative && $journal['currency_id'] !== $default->id && $default->id === $journal['foreign_currency_id']) { + $field = 'foreign_amount'; + } + Log::debug(sprintf('Journal #%d in bill #%d will use %s (%s %s)', $journal['transaction_group_id'], $billId, $field, $currencyCode, $journal[$field] ?? '0')); + + $key = sprintf('%d-%d', $billId, $currencyId); if (0 !== $currencyId) { $response[$key] ??= [ - 'id' => (string)$billId, + 'id' => (string) $billId, 'name' => $journal['bill_name'], 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, ]; - $response[$key]['difference'] = bcadd($response[$key]['difference'], $journal['amount']); - $response[$key]['difference_float'] = (float)$response[$key]['difference']; // intentional float - } - if (0 !== $foreignCurrencyId) { - $response[$foreignKey] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignKey]['difference'] = bcadd($response[$foreignKey]['difference'], $journal['foreign_amount']); - $response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; // intentional float + $response[$key]['difference'] = bcadd($response[$key]['difference'], (string) ($journal[$field] ?? '0')); + $response[$key]['difference_float'] = (float) $response[$key]['difference']; // intentional float } } @@ -122,41 +129,46 @@ class BillController extends Controller */ public function noBill(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); + $response = []; // collect all expenses in this period (regardless of type) by the given bills and accounts. - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value])->setRange($start, $end)->setSourceAccounts($accounts); $collector->withoutBill(); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + $currencyId = (int) $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $field = 'amount'; + + // use the native amount if the user wants to convert to native currency + if ($convertToNative && $currencyId !== $default->id) { + $currencyId = $default->id; + $currencyCode = $default->code; + $field = 'native_amount'; + } + // use foreign amount when the foreign currency IS the default currency. + if ($convertToNative && $journal['currency_id'] !== $default->id && $default->id === $journal['foreign_currency_id']) { + $field = 'foreign_amount'; + } + Log::debug(sprintf('Journal #%d will use %s (%s %s)', $journal['transaction_group_id'], $field, $currencyCode, $journal[$field] ?? '0')); if (0 !== $currencyId) { $response[$currencyId] ??= [ 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, ]; - $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $journal['amount']); - $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; // intentional float - } - if (0 !== $foreignCurrencyId) { - $response[$foreignCurrencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], $journal['foreign_amount']); - $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; // intentional float + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], (string) ($journal[$field] ?? '0')); + $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; // intentional float } } diff --git a/app/Api/V1/Controllers/Insight/Expense/BudgetController.php b/app/Api/V1/Controllers/Insight/Expense/BudgetController.php index 6aab81946a..5795cb114e 100644 --- a/app/Api/V1/Controllers/Insight/Expense/BudgetController.php +++ b/app/Api/V1/Controllers/Insight/Expense/BudgetController.php @@ -85,11 +85,11 @@ class BudgetController extends Controller /** @var array $expense */ foreach ($expenses as $expense) { $result[] = [ - 'id' => (string)$budget->id, + 'id' => (string) $budget->id, 'name' => $budget->name, 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // intentional float - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // intentional float + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } @@ -114,8 +114,8 @@ class BudgetController extends Controller foreach ($expenses as $expense) { $result[] = [ 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // intentional float - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // intentional float + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } diff --git a/app/Api/V1/Controllers/Insight/Expense/CategoryController.php b/app/Api/V1/Controllers/Insight/Expense/CategoryController.php index 5ee6e77378..fd96e47ed7 100644 --- a/app/Api/V1/Controllers/Insight/Expense/CategoryController.php +++ b/app/Api/V1/Controllers/Insight/Expense/CategoryController.php @@ -85,11 +85,11 @@ class CategoryController extends Controller /** @var array $expense */ foreach ($expenses as $expense) { $result[] = [ - 'id' => (string)$category->id, + 'id' => (string) $category->id, 'name' => $category->name, 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // intentional float - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // intentional float + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } @@ -114,8 +114,8 @@ class CategoryController extends Controller foreach ($expenses as $expense) { $result[] = [ 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // intentional float - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // intentional float + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } diff --git a/app/Api/V1/Controllers/Insight/Expense/PeriodController.php b/app/Api/V1/Controllers/Insight/Expense/PeriodController.php index a9d80e935d..b4d2c42450 100644 --- a/app/Api/V1/Controllers/Insight/Expense/PeriodController.php +++ b/app/Api/V1/Controllers/Insight/Expense/PeriodController.php @@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Log; /** * Class PeriodController @@ -41,39 +43,49 @@ class PeriodController extends Controller */ public function total(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); // collect all expenses in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value])->setRange($start, $end)->setSourceAccounts($accounts); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + // same code as many other sumExpense methods. I think this needs some kind of generic method. + $amount = '0'; + $currencyId = (int) $journal['currency_id']; + $currencyCode = $journal['currency_code']; + if ($convertToNative) { + $amount = Amount::getAmountFromJournal($journal); + if ($default->id !== (int) $journal['currency_id'] && $default->id !== (int) $journal['foreign_currency_id']) { + $currencyId = $default->id; + $currencyCode = $default->code; + } + if ($default->id !== (int) $journal['currency_id'] && $default->id === (int) $journal['foreign_currency_id']) { + $currencyId = $journal['foreign_currency_id']; + $currencyCode = $journal['foreign_currency_code']; + } + Log::debug(sprintf('[a] Add amount %s %s', $currencyCode, $amount)); + } + if (!$convertToNative) { + // ignore the amount in foreign currency. + Log::debug(sprintf('[b] Add amount %s %s', $currencyCode, $journal['amount'])); + $amount = $journal['amount']; + } - if (0 !== $currencyId) { - $response[$currencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], - ]; - $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $journal['amount']); - $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; // intentional float - } - if (0 !== $foreignCurrencyId) { - $response[$foreignCurrencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], $journal['foreign_amount']); - $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; // intentional float - } + + $response[$currencyId] ??= [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $amount); + $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; // intentional float } return response()->json(array_values($response)); diff --git a/app/Api/V1/Controllers/Insight/Expense/TagController.php b/app/Api/V1/Controllers/Insight/Expense/TagController.php index 093954ac45..83f43eee37 100644 --- a/app/Api/V1/Controllers/Insight/Expense/TagController.php +++ b/app/Api/V1/Controllers/Insight/Expense/TagController.php @@ -29,7 +29,9 @@ use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Log; /** * Class TagController @@ -62,42 +64,51 @@ class TagController extends Controller */ public function noTag(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); // collect all expenses in this period (regardless of type) by the given bills and accounts. - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value])->setRange($start, $end)->setSourceAccounts($accounts); $collector->withoutTags(); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + // same code as many other sumExpense methods. I think this needs some kind of generic method. + $amount = '0'; + $currencyId = (int) $journal['currency_id']; + $currencyCode = $journal['currency_code']; + if ($convertToNative) { + $amount = Amount::getAmountFromJournal($journal); + if ($default->id !== (int) $journal['currency_id'] && $default->id !== (int) $journal['foreign_currency_id']) { + $currencyId = $default->id; + $currencyCode = $default->code; + } + if ($default->id !== (int) $journal['currency_id'] && $default->id === (int) $journal['foreign_currency_id']) { + $currencyId = $journal['foreign_currency_id']; + $currencyCode = $journal['foreign_currency_code']; + } + Log::debug(sprintf('[a] Add amount %s %s', $currencyCode, $amount)); + } + if (!$convertToNative) { + // ignore the amount in foreign currency. + Log::debug(sprintf('[b] Add amount %s %s', $currencyCode, $journal['amount'])); + $amount = $journal['amount']; + } - if (0 !== $currencyId) { - $response[$currencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], - ]; - $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $journal['amount']); - $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; // float but on purpose. - } - if (0 !== $foreignCurrencyId) { - $response[$foreignCurrencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], $journal['foreign_amount']); - $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; // float but on purpose. - } + $response[$currencyId] ??= [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $amount); + $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; // float but on purpose. } return response()->json(array_values($response)); @@ -130,8 +141,8 @@ class TagController extends Controller /** @var array $journal */ foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + $currencyId = (int) $journal['currency_id']; + $foreignCurrencyId = (int) $journal['foreign_currency_id']; /** @var array $tag */ foreach ($journal['tags'] as $tag) { @@ -142,15 +153,15 @@ class TagController extends Controller // on currency ID if (0 !== $currencyId) { $response[$key] ??= [ - 'id' => (string)$tagId, + 'id' => (string) $tagId, 'name' => $tag['name'], 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_code' => $journal['currency_code'], ]; $response[$key]['difference'] = bcadd($response[$key]['difference'], $journal['amount']); - $response[$key]['difference_float'] = (float)$response[$key]['difference']; // float but on purpose. + $response[$key]['difference_float'] = (float) $response[$key]['difference']; // float but on purpose. } // on foreign ID @@ -158,11 +169,11 @@ class TagController extends Controller $response[$foreignKey] = $journal[$foreignKey] ?? [ 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, + 'currency_id' => (string) $foreignCurrencyId, 'currency_code' => $journal['foreign_currency_code'], ]; $response[$foreignKey]['difference'] = bcadd($response[$foreignKey]['difference'], $journal['foreign_amount']); - $response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; // float but on purpose. + $response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // float but on purpose. } } } diff --git a/app/Api/V1/Controllers/Insight/Income/AccountController.php b/app/Api/V1/Controllers/Insight/Income/AccountController.php index 17e9d34205..87cff7be5c 100644 --- a/app/Api/V1/Controllers/Insight/Income/AccountController.php +++ b/app/Api/V1/Controllers/Insight/Income/AccountController.php @@ -73,17 +73,18 @@ class AccountController extends Controller $start = $request->getStart(); $end = $request->getEnd(); $assetAccounts = $request->getAssetAccounts(); + $income = $this->opsRepository->sumIncomeByDestination($start, $end, $assetAccounts); $result = []; /** @var array $entry */ foreach ($income as $entry) { $result[] = [ - 'id' => (string)$entry['id'], + 'id' => (string) $entry['id'], 'name' => $entry['name'], 'difference' => $entry['sum'], - 'difference_float' => (float)$entry['sum'], // float but on purpose. - 'currency_id' => (string)$entry['currency_id'], + 'difference_float' => (float) $entry['sum'], // float but on purpose. + 'currency_id' => (string) $entry['currency_id'], 'currency_code' => $entry['currency_code'], ]; } @@ -107,11 +108,11 @@ class AccountController extends Controller /** @var array $entry */ foreach ($income as $entry) { $result[] = [ - 'id' => (string)$entry['id'], + 'id' => (string) $entry['id'], 'name' => $entry['name'], 'difference' => $entry['sum'], - 'difference_float' => (float)$entry['sum'], // float but on purpose. - 'currency_id' => (string)$entry['currency_id'], + 'difference_float' => (float) $entry['sum'], // float but on purpose. + 'currency_id' => (string) $entry['currency_id'], 'currency_code' => $entry['currency_code'], ]; } diff --git a/app/Api/V1/Controllers/Insight/Income/CategoryController.php b/app/Api/V1/Controllers/Insight/Income/CategoryController.php index 1f989dfe81..d2a1fa9744 100644 --- a/app/Api/V1/Controllers/Insight/Income/CategoryController.php +++ b/app/Api/V1/Controllers/Insight/Income/CategoryController.php @@ -85,11 +85,11 @@ class CategoryController extends Controller /** @var array $expense */ foreach ($expenses as $expense) { $result[] = [ - 'id' => (string)$category->id, + 'id' => (string) $category->id, 'name' => $category->name, 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // float but on purpose. - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // float but on purpose. + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } @@ -114,8 +114,8 @@ class CategoryController extends Controller foreach ($expenses as $expense) { $result[] = [ 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], // float but on purpose. - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], // float but on purpose. + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } diff --git a/app/Api/V1/Controllers/Insight/Income/PeriodController.php b/app/Api/V1/Controllers/Insight/Income/PeriodController.php index f71c21fa14..89d4c251c5 100644 --- a/app/Api/V1/Controllers/Insight/Income/PeriodController.php +++ b/app/Api/V1/Controllers/Insight/Income/PeriodController.php @@ -28,6 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; /** @@ -41,42 +42,41 @@ class PeriodController extends Controller */ public function total(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); // collect all expenses in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value])->setRange($start, $end)->setDestinationAccounts($accounts); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + // currency + $currencyId = $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $field = $convertToNative && $currencyId !== $default->id ? 'native_amount' : 'amount'; - if (0 !== $currencyId) { - $response[$currencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], - ]; - $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal['amount'])); - $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; // float but on purpose. + // perhaps use default currency instead? + if ($convertToNative && $journal['currency_id'] !== $default->id) { + $currencyId = $default->id; + $currencyCode = $default->code; } - if (0 !== $foreignCurrencyId) { - $response[$foreignCurrencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignCurrencyId]['difference'] = bcadd( - $response[$foreignCurrencyId]['difference'], - app('steam')->positive($journal['foreign_amount']) - ); - $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; // float but on purpose. + // use foreign amount when the foreign currency IS the default currency. + if ($convertToNative && $journal['currency_id'] !== $default->id && $default->id === $journal['foreign_currency_id']) { + $field = 'foreign_amount'; } + + $response[$currencyId] ??= [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal[$field])); + $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; // float but on purpose. } return response()->json(array_values($response)); diff --git a/app/Api/V1/Controllers/Insight/Income/TagController.php b/app/Api/V1/Controllers/Insight/Income/TagController.php index 39dcd4a0f1..e81186582e 100644 --- a/app/Api/V1/Controllers/Insight/Income/TagController.php +++ b/app/Api/V1/Controllers/Insight/Income/TagController.php @@ -29,6 +29,7 @@ use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; /** @@ -63,45 +64,45 @@ class TagController extends Controller */ public function noTag(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); // collect all expenses in this period (regardless of type) by the given bills and accounts. - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value])->setRange($start, $end)->setDestinationAccounts($accounts); $collector->withoutTags(); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + // currency + $currencyId = $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $field = $convertToNative && $currencyId !== $default->id ? 'native_amount' : 'amount'; - if (0 !== $currencyId) { - $response[$currencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], - ]; - $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal['amount'])); - $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; + // perhaps use default currency instead? + if ($convertToNative && $journal['currency_id'] !== $default->id) { + $currencyId = $default->id; + $currencyCode = $default->code; } - if (0 !== $foreignCurrencyId) { - $response[$foreignCurrencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignCurrencyId]['difference'] = bcadd( - $response[$foreignCurrencyId]['difference'], - app('steam')->positive($journal['foreign_amount']) - ); - $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; + // use foreign amount when the foreign currency IS the default currency. + if ($convertToNative && $journal['currency_id'] !== $default->id && $default->id === $journal['foreign_currency_id']) { + $field = 'foreign_amount'; } + + $response[$currencyId] ??= [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal[$field])); + $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; + } return response()->json(array_values($response)); @@ -134,8 +135,8 @@ class TagController extends Controller /** @var array $journal */ foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + $currencyId = (int) $journal['currency_id']; + $foreignCurrencyId = (int) $journal['foreign_currency_id']; /** @var array $tag */ foreach ($journal['tags'] as $tag) { @@ -146,15 +147,15 @@ class TagController extends Controller // on currency ID if (0 !== $currencyId) { $response[$key] ??= [ - 'id' => (string)$tagId, + 'id' => (string) $tagId, 'name' => $tag['name'], 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_code' => $journal['currency_code'], ]; $response[$key]['difference'] = bcadd($response[$key]['difference'], app('steam')->positive($journal['amount'])); - $response[$key]['difference_float'] = (float)$response[$key]['difference']; + $response[$key]['difference_float'] = (float) $response[$key]['difference']; } // on foreign ID @@ -162,14 +163,14 @@ class TagController extends Controller $response[$foreignKey] = $journal[$foreignKey] ?? [ 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, + 'currency_id' => (string) $foreignCurrencyId, 'currency_code' => $journal['foreign_currency_code'], ]; $response[$foreignKey]['difference'] = bcadd( $response[$foreignKey]['difference'], app('steam')->positive($journal['foreign_amount']) ); - $response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; + $response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; } } } diff --git a/app/Api/V1/Controllers/Insight/Transfer/CategoryController.php b/app/Api/V1/Controllers/Insight/Transfer/CategoryController.php index 49782d9fbe..6ad2c269df 100644 --- a/app/Api/V1/Controllers/Insight/Transfer/CategoryController.php +++ b/app/Api/V1/Controllers/Insight/Transfer/CategoryController.php @@ -85,11 +85,11 @@ class CategoryController extends Controller /** @var array $expense */ foreach ($expenses as $expense) { $result[] = [ - 'id' => (string)$category->id, + 'id' => (string) $category->id, 'name' => $category->name, 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } @@ -114,8 +114,8 @@ class CategoryController extends Controller foreach ($expenses as $expense) { $result[] = [ 'difference' => $expense['sum'], - 'difference_float' => (float)$expense['sum'], - 'currency_id' => (string)$expense['currency_id'], + 'difference_float' => (float) $expense['sum'], + 'currency_id' => (string) $expense['currency_id'], 'currency_code' => $expense['currency_code'], ]; } diff --git a/app/Api/V1/Controllers/Insight/Transfer/PeriodController.php b/app/Api/V1/Controllers/Insight/Transfer/PeriodController.php index bda338ae4d..e86a98a748 100644 --- a/app/Api/V1/Controllers/Insight/Transfer/PeriodController.php +++ b/app/Api/V1/Controllers/Insight/Transfer/PeriodController.php @@ -28,6 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; /** @@ -41,42 +42,42 @@ class PeriodController extends Controller */ public function total(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); // collect all expenses in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionType::TRANSFER])->setRange($start, $end)->setDestinationAccounts($accounts); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + // currency + $currencyId = $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $field = $convertToNative && $currencyId !== $default->id ? 'native_amount' : 'amount'; - if (0 !== $currencyId) { - $response[$currencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], - ]; - $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal['amount'])); - $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; + // perhaps use default currency instead? + if ($convertToNative && $journal['currency_id'] !== $default->id) { + $currencyId = $default->id; + $currencyCode = $default->code; } - if (0 !== $foreignCurrencyId) { - $response[$foreignCurrencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignCurrencyId]['difference'] = bcadd( - $response[$foreignCurrencyId]['difference'], - app('steam')->positive($journal['foreign_amount']) - ); - $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; + // use foreign amount when the foreign currency IS the default currency. + if ($convertToNative && $journal['currency_id'] !== $default->id && $default->id === $journal['foreign_currency_id']) { + $field = 'foreign_amount'; } + + $response[$currencyId] ??= [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal[$field])); + $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; + } return response()->json(array_values($response)); diff --git a/app/Api/V1/Controllers/Insight/Transfer/TagController.php b/app/Api/V1/Controllers/Insight/Transfer/TagController.php index 986dc6c2cf..4d3f8f56db 100644 --- a/app/Api/V1/Controllers/Insight/Transfer/TagController.php +++ b/app/Api/V1/Controllers/Insight/Transfer/TagController.php @@ -29,6 +29,7 @@ use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; /** @@ -61,45 +62,46 @@ class TagController extends Controller */ public function noTag(GenericRequest $request): JsonResponse { - $accounts = $request->getAssetAccounts(); - $start = $request->getStart(); - $end = $request->getEnd(); - $response = []; + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); + // collect all expenses in this period (regardless of type) by the given bills and accounts. - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes([TransactionType::TRANSFER])->setRange($start, $end)->setDestinationAccounts($accounts); $collector->withoutTags(); - $genericSet = $collector->getExtractedJournals(); + $genericSet = $collector->getExtractedJournals(); foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + // currency + $currencyId = $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $field = $convertToNative && $currencyId !== $default->id ? 'native_amount' : 'amount'; - if (0 !== $currencyId) { - $response[$currencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$currencyId, - 'currency_code' => $journal['currency_code'], - ]; - $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal['amount'])); - $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; + // perhaps use default currency instead? + if ($convertToNative && $journal['currency_id'] !== $default->id) { + $currencyId = $default->id; + $currencyCode = $default->code; } - if (0 !== $foreignCurrencyId) { - $response[$foreignCurrencyId] ??= [ - 'difference' => '0', - 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, - 'currency_code' => $journal['foreign_currency_code'], - ]; - $response[$foreignCurrencyId]['difference'] = bcadd( - $response[$foreignCurrencyId]['difference'], - app('steam')->positive($journal['foreign_amount']) - ); - $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; + // use foreign amount when the foreign currency IS the default currency. + if ($convertToNative && $journal['currency_id'] !== $default->id && $default->id === $journal['foreign_currency_id']) { + $field = 'foreign_amount'; } + + $response[$currencyId] ??= [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string) $currencyId, + 'currency_code' => $currencyCode, + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal[$field])); + $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; + } return response()->json(array_values($response)); @@ -132,8 +134,8 @@ class TagController extends Controller /** @var array $journal */ foreach ($genericSet as $journal) { - $currencyId = (int)$journal['currency_id']; - $foreignCurrencyId = (int)$journal['foreign_currency_id']; + $currencyId = (int) $journal['currency_id']; + $foreignCurrencyId = (int) $journal['foreign_currency_id']; /** @var array $tag */ foreach ($journal['tags'] as $tag) { @@ -144,15 +146,15 @@ class TagController extends Controller // on currency ID if (0 !== $currencyId) { $response[$key] ??= [ - 'id' => (string)$tagId, + 'id' => (string) $tagId, 'name' => $tag['name'], 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_code' => $journal['currency_code'], ]; $response[$key]['difference'] = bcadd($response[$key]['difference'], app('steam')->positive($journal['amount'])); - $response[$key]['difference_float'] = (float)$response[$key]['difference']; + $response[$key]['difference_float'] = (float) $response[$key]['difference']; } // on foreign ID @@ -160,14 +162,14 @@ class TagController extends Controller $response[$foreignKey] = $journal[$foreignKey] ?? [ 'difference' => '0', 'difference_float' => 0, - 'currency_id' => (string)$foreignCurrencyId, + 'currency_id' => (string) $foreignCurrencyId, 'currency_code' => $journal['foreign_currency_code'], ]; $response[$foreignKey]['difference'] = bcadd( $response[$foreignKey]['difference'], app('steam')->positive($journal['foreign_amount']) ); - $response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; // intentional float + $response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // intentional float } } } diff --git a/app/Api/V1/Controllers/Models/Account/ListController.php b/app/Api/V1/Controllers/Models/Account/ListController.php index c85456f901..b2d4e39d90 100644 --- a/app/Api/V1/Controllers/Models/Account/ListController.php +++ b/app/Api/V1/Controllers/Models/Account/ListController.php @@ -111,7 +111,7 @@ class ListController extends Controller // types to get, page size: $pageSize = $this->parameters->get('limit'); - // get list of budgets. Count it and split it. + // get list of piggy banks. Count it and split it. $collection = $this->repository->getPiggyBanks($account); $count = $collection->count(); $piggyBanks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); diff --git a/app/Api/V1/Controllers/Models/Attachment/ShowController.php b/app/Api/V1/Controllers/Models/Attachment/ShowController.php index c9b0371a0d..bb8e267a11 100644 --- a/app/Api/V1/Controllers/Models/Attachment/ShowController.php +++ b/app/Api/V1/Controllers/Models/Attachment/ShowController.php @@ -105,7 +105,7 @@ class ShowController extends Controller ->header('Expires', '0') ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') ->header('Pragma', 'public') - ->header('Content-Length', (string)strlen($content)) + ->header('Content-Length', (string) strlen($content)) ; return $response; diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php index 6f899b19ce..5da7d3050c 100644 --- a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php +++ b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php @@ -69,6 +69,7 @@ class StoreController extends Controller $data = $request->getAll(); $data['start_date'] = $data['start']; $data['end_date'] = $data['end']; + $data['notes'] = $data['notes']; $data['budget_id'] = $budget->id; $budgetLimit = $this->blRepository->store($data); diff --git a/app/Api/V1/Controllers/Models/PiggyBank/ListController.php b/app/Api/V1/Controllers/Models/PiggyBank/ListController.php index 3c3ac1672b..a0b35ec09a 100644 --- a/app/Api/V1/Controllers/Models/PiggyBank/ListController.php +++ b/app/Api/V1/Controllers/Models/PiggyBank/ListController.php @@ -28,6 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\PiggyBank; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; +use FireflyIII\Transformers\AccountTransformer; use FireflyIII\Transformers\AttachmentTransformer; use FireflyIII\Transformers\PiggyBankEventTransformer; use Illuminate\Http\JsonResponse; @@ -58,6 +59,38 @@ class ListController extends Controller ); } + /** + * This endpoint is documented at: + * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/piggy_banks/listAccountByPiggyBank + * + * List single resource. + * + * @throws FireflyException + */ + public function accounts(PiggyBank $piggyBank): JsonResponse + { + // types to get, page size: + $pageSize = $this->parameters->get('limit'); + $manager = $this->getManager(); + + $collection = $piggyBank->accounts; + $count = $collection->count(); + $events = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + + // make paginator: + $paginator = new LengthAwarePaginator($events, $count, $pageSize, $this->parameters->get('page')); + $paginator->setPath(route('api.v1.piggy-banks.accounts', [$piggyBank->id]).$this->buildParams()); + + /** @var AccountTransformer $transformer */ + $transformer = app(AccountTransformer::class); + $transformer->setParameters($this->parameters); + + $resource = new FractalCollection($events, $transformer, 'accounts'); + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); + } + /** * This endpoint is documented at: * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/piggy_banks/listAttachmentByPiggyBank diff --git a/app/Api/V1/Controllers/Models/PiggyBank/ShowController.php b/app/Api/V1/Controllers/Models/PiggyBank/ShowController.php index 2005bc4de5..442b47cb38 100644 --- a/app/Api/V1/Controllers/Models/PiggyBank/ShowController.php +++ b/app/Api/V1/Controllers/Models/PiggyBank/ShowController.php @@ -72,7 +72,7 @@ class ShowController extends Controller // types to get, page size: $pageSize = $this->parameters->get('limit'); - // get list of budgets. Count it and split it. + // get list of piggy banks. Count it and split it. $collection = $this->repository->getPiggyBanks(); $count = $collection->count(); $piggyBanks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); diff --git a/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php b/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php index 2607c280de..c96e04ff98 100644 --- a/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php +++ b/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php @@ -91,7 +91,7 @@ class ListController extends Controller // filter list on currency preference: $collection = $unfiltered->filter( static function (Account $account) use ($currency, $accountRepository) { - $currencyId = (int)$accountRepository->getMetaValue($account, 'currency_id'); + $currencyId = (int) $accountRepository->getMetaValue($account, 'currency_id'); return $currencyId === $currency->id; } diff --git a/app/Api/V1/Controllers/Models/TransactionCurrency/ShowController.php b/app/Api/V1/Controllers/Models/TransactionCurrency/ShowController.php index 363d599869..48ff9bb724 100644 --- a/app/Api/V1/Controllers/Models/TransactionCurrency/ShowController.php +++ b/app/Api/V1/Controllers/Models/TransactionCurrency/ShowController.php @@ -105,19 +105,18 @@ class ShowController extends Controller public function show(TransactionCurrency $currency): JsonResponse { /** @var User $user */ - $user = auth()->user(); - $manager = $this->getManager(); - $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($user->userGroup); - $this->parameters->set('defaultCurrency', $defaultCurrency); + $user = auth()->user(); + $manager = $this->getManager(); + $this->parameters->set('defaultCurrency', $this->defaultCurrency); // update fields with user info. $currency->refreshForUser($user); /** @var CurrencyTransformer $transformer */ - $transformer = app(CurrencyTransformer::class); + $transformer = app(CurrencyTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($currency, $transformer, 'currencies'); + $resource = new Item($currency, $transformer, 'currencies'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } @@ -135,7 +134,7 @@ class ShowController extends Controller /** @var User $user */ $user = auth()->user(); $manager = $this->getManager(); - $currency = app('amount')->getDefaultCurrencyByUserGroup($user->userGroup); + $currency = $this->defaultCurrency; // update fields with user info. $currency->refreshForUser($user); diff --git a/app/Api/V1/Controllers/Search/AccountController.php b/app/Api/V1/Controllers/Search/AccountController.php index 98204fcef7..74ed89c978 100644 --- a/app/Api/V1/Controllers/Search/AccountController.php +++ b/app/Api/V1/Controllers/Search/AccountController.php @@ -62,14 +62,14 @@ class AccountController extends Controller */ public function search(Request $request): JsonResponse|Response { - app('log')->debug('Now in account search()'); $manager = $this->getManager(); - $query = trim((string)$request->get('query')); - $field = trim((string)$request->get('field')); + $query = trim((string) $request->get('query')); + $field = trim((string) $request->get('field')); $type = $request->get('type') ?? 'all'; if ('' === $query || !in_array($field, $this->validFields, true)) { return response(null, 422); } + app('log')->debug(sprintf('Now in account search("%s", "%s")', $field, $query)); $types = $this->mapAccountTypes($type); /** @var AccountSearch $search */ diff --git a/app/Api/V1/Controllers/Search/TransactionController.php b/app/Api/V1/Controllers/Search/TransactionController.php index 962a454253..b8a5d450d3 100644 --- a/app/Api/V1/Controllers/Search/TransactionController.php +++ b/app/Api/V1/Controllers/Search/TransactionController.php @@ -47,8 +47,8 @@ class TransactionController extends Controller public function search(Request $request, SearchInterface $searcher): JsonResponse { $manager = $this->getManager(); - $fullQuery = (string)$request->get('query'); - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); + $fullQuery = (string) $request->get('query'); + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); $pageSize = $this->parameters->get('limit'); $searcher->parseQuery($fullQuery); $searcher->setPage($page); diff --git a/app/Api/V1/Controllers/Summary/BasicController.php b/app/Api/V1/Controllers/Summary/BasicController.php index f1021ad5ec..cea491fc4d 100644 --- a/app/Api/V1/Controllers/Summary/BasicController.php +++ b/app/Api/V1/Controllers/Summary/BasicController.php @@ -27,19 +27,21 @@ namespace FireflyIII\Api\V1\Controllers\Summary; use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Data\DateRequest; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Report\NetWorthInterface; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use FireflyIII\User; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Log; /** * Class BasicController @@ -101,10 +103,10 @@ class BasicController extends Controller $billData = $this->getBillInformation($start, $end); $spentData = $this->getLeftToSpendInfo($start, $end); $netWorthData = $this->getNetWorthInfo($start, $end); - // $balanceData = []; - // $billData = []; - // $spentData = []; - // $netWorthData = []; + // $balanceData = []; + // $billData = []; + // $spentData = []; + // $netWorthData = []; $total = array_merge($balanceData, $billData, $spentData, $netWorthData); // give new keys @@ -120,48 +122,53 @@ class BasicController extends Controller private function getBalanceInformation(Carbon $start, Carbon $end): array { + // some config settings + $convertToNative = Amount::convertToNative(); + $default = Amount::getDefaultCurrency(); // prep some arrays: - $incomes = []; - $expenses = []; - $sums = []; - $return = []; + $incomes = []; + $expenses = []; + $sums = []; + $return = []; // collect income of user using the new group collector. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->setPage($this->parameters->get('page'))->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $set = $collector->getExtractedJournals(); + $set = $collector->getExtractedJournals(); - /** @var array $transactionJournal */ - foreach ($set as $transactionJournal) { - $currencyId = (int)$transactionJournal['currency_id']; + /** @var array $journal */ + foreach ($set as $journal) { + $currencyId = $convertToNative ? $default->id : (int) $journal['currency_id']; + $amount = Amount::getAmountFromJournal($journal); $incomes[$currencyId] ??= '0'; $incomes[$currencyId] = bcadd( $incomes[$currencyId], - bcmul($transactionJournal['amount'], '-1') + bcmul($amount, '-1') ); $sums[$currencyId] ??= '0'; - $sums[$currencyId] = bcadd($sums[$currencyId], bcmul($transactionJournal['amount'], '-1')); + $sums[$currencyId] = bcadd($sums[$currencyId], bcmul($amount, '-1')); } // collect expenses of user using the new group collector. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->setPage($this->parameters->get('page'))->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $set = $collector->getExtractedJournals(); + $set = $collector->getExtractedJournals(); - /** @var array $transactionJournal */ - foreach ($set as $transactionJournal) { - $currencyId = (int)$transactionJournal['currency_id']; + /** @var array $journal */ + foreach ($set as $journal) { + $currencyId = $convertToNative ? $default->id : (int) $journal['currency_id']; + $amount = Amount::getAmountFromJournal($journal); $expenses[$currencyId] ??= '0'; - $expenses[$currencyId] = bcadd($expenses[$currencyId], $transactionJournal['amount']); + $expenses[$currencyId] = bcadd($expenses[$currencyId], $amount); $sums[$currencyId] ??= '0'; - $sums[$currencyId] = bcadd($sums[$currencyId], $transactionJournal['amount']); + $sums[$currencyId] = bcadd($sums[$currencyId], $amount); } // format amounts: - $keys = array_keys($sums); + $keys = array_keys($sums); foreach ($keys as $currencyId) { $currency = $this->currencyRepos->find($currencyId); if (null === $currency) { @@ -172,7 +179,7 @@ class BasicController extends Controller 'key' => sprintf('balance-in-%s', $currency->code), 'title' => trans('firefly.box_balance_in_currency', ['currency' => $currency->symbol]), 'monetary_value' => $sums[$currencyId] ?? '0', - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, @@ -185,7 +192,7 @@ class BasicController extends Controller 'key' => sprintf('spent-in-%s', $currency->code), 'title' => trans('firefly.box_spent_in_currency', ['currency' => $currency->symbol]), 'monetary_value' => $expenses[$currencyId] ?? '0', - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, @@ -197,7 +204,7 @@ class BasicController extends Controller 'key' => sprintf('earned-in-%s', $currency->code), 'title' => trans('firefly.box_earned_in_currency', ['currency' => $currency->symbol]), 'monetary_value' => $incomes[$currencyId] ?? '0', - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, @@ -231,7 +238,7 @@ class BasicController extends Controller 'key' => sprintf('bills-paid-in-%s', $info['code']), 'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $info['symbol']]), 'monetary_value' => $amount, - 'currency_id' => (string)$info['id'], + 'currency_id' => (string) $info['id'], 'currency_code' => $info['code'], 'currency_symbol' => $info['symbol'], 'currency_decimal_places' => $info['decimal_places'], @@ -250,7 +257,7 @@ class BasicController extends Controller 'key' => sprintf('bills-unpaid-in-%s', $info['code']), 'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $info['symbol']]), 'monetary_value' => $amount, - 'currency_id' => (string)$info['id'], + 'currency_id' => (string) $info['id'], 'currency_code' => $info['code'], 'currency_symbol' => $info['symbol'], 'currency_decimal_places' => $info['decimal_places'], @@ -269,29 +276,32 @@ class BasicController extends Controller */ private function getLeftToSpendInfo(Carbon $start, Carbon $end): array { + Log::debug(sprintf('Now in getLeftToSpendInfo("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); $return = []; $today = today(config('app.timezone')); $available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end); $budgets = $this->budgetRepository->getActiveBudgets(); $spent = $this->opsRepository->sumExpenses($start, $end, null, $budgets); + $days = (int) $today->diffInDays($end, true) + 1; foreach ($spent as $row) { // either an amount was budgeted or 0 is available. - $amount = (string)($available[$row['currency_id']] ?? '0'); + $currencyId = $row['currency_id']; + $amount = (string) ($available[$currencyId] ?? '0'); $spentInCurrency = $row['sum']; $leftToSpend = bcadd($amount, $spentInCurrency); - - $days = (int)$today->diffInDays($end, true) + 1; $perDay = '0'; if (0 !== $days && bccomp($leftToSpend, '0') > -1) { - $perDay = bcdiv($leftToSpend, (string)$days); + $perDay = bcdiv($leftToSpend, (string) $days); } + Log::debug(sprintf('Spent %s %s', $row['currency_code'], $row['sum'])); + $return[] = [ 'key' => sprintf('left-to-spend-in-%s', $row['currency_code']), 'title' => trans('firefly.box_left_to_spend_in_currency', ['currency' => $row['currency_symbol']]), 'monetary_value' => $leftToSpend, - 'currency_id' => (string)$row['currency_id'], + 'currency_id' => (string) $row['currency_id'], 'currency_code' => $row['currency_code'], 'currency_symbol' => $row['currency_symbol'], 'currency_decimal_places' => $row['currency_decimal_places'], @@ -311,9 +321,11 @@ class BasicController extends Controller private function getNetWorthInfo(Carbon $start, Carbon $end): array { + Log::debug('getNetWorthInfo'); + /** @var User $user */ $user = auth()->user(); - $date = today(config('app.timezone'))->startOfDay(); + $date = now(config('app.timezone')); // start and end in the future? use $end if ($this->notInDateRange($date, $start, $end)) { /** @var Carbon $date */ @@ -323,9 +335,7 @@ class BasicController extends Controller /** @var NetWorthInterface $netWorthHelper */ $netWorthHelper = app(NetWorthInterface::class); $netWorthHelper->setUser($user); - $allAccounts = $this->accountRepository->getActiveAccountsByType( - [AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT] - ); + $allAccounts = $this->accountRepository->getActiveAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value]); // filter list on preference of being included. $filtered = $allAccounts->filter( @@ -351,7 +361,7 @@ class BasicController extends Controller 'key' => sprintf('net-worth-in-%s', $data['currency_code']), 'title' => trans('firefly.box_net_worth_in_currency', ['currency' => $data['currency_symbol']]), 'monetary_value' => $amount, - 'currency_id' => (string)$data['currency_id'], + 'currency_id' => (string) $data['currency_id'], 'currency_code' => $data['currency_code'], 'currency_symbol' => $data['currency_symbol'], 'currency_decimal_places' => $data['currency_decimal_places'], @@ -360,6 +370,7 @@ class BasicController extends Controller 'sub_title' => '', ]; } + Log::debug('End of getNetWorthInfo'); return $return; } diff --git a/app/Api/V1/Controllers/System/ConfigurationController.php b/app/Api/V1/Controllers/System/ConfigurationController.php index 2a853e95ae..2eb893b96b 100644 --- a/app/Api/V1/Controllers/System/ConfigurationController.php +++ b/app/Api/V1/Controllers/System/ConfigurationController.php @@ -101,8 +101,8 @@ class ConfigurationController extends Controller return [ 'is_demo_site' => $isDemoSite?->data, - 'permission_update_check' => null === $updateCheck ? null : (int)$updateCheck->data, - 'last_update_check' => null === $lastCheck ? null : (int)$lastCheck->data, + 'permission_update_check' => null === $updateCheck ? null : (int) $updateCheck->data, + 'last_update_check' => null === $lastCheck ? null : (int) $lastCheck->data, 'single_user_mode' => $singleUser?->data, ]; } diff --git a/app/Api/V1/Requests/Data/Bulk/MoveTransactionsRequest.php b/app/Api/V1/Requests/Data/Bulk/MoveTransactionsRequest.php index 4ec6250dd2..f3dacae241 100644 --- a/app/Api/V1/Requests/Data/Bulk/MoveTransactionsRequest.php +++ b/app/Api/V1/Requests/Data/Bulk/MoveTransactionsRequest.php @@ -83,12 +83,12 @@ class MoveTransactionsRequest extends FormRequest $data = $validator->getData(); $repository = app(AccountRepositoryInterface::class); $repository->setUser(auth()->user()); - $original = $repository->find((int)$data['original_account']); - $destination = $repository->find((int)$data['destination_account']); + $original = $repository->find((int) $data['original_account']); + $destination = $repository->find((int) $data['destination_account']); // not the same type: if ($original->accountType->type !== $destination->accountType->type) { - $validator->errors()->add('title', (string)trans('validation.same_account_type')); + $validator->errors()->add('title', (string) trans('validation.same_account_type')); return; } @@ -98,7 +98,7 @@ class MoveTransactionsRequest extends FormRequest // check different scenario's. if (null === $originalCurrency xor null === $destinationCurrency) { - $validator->errors()->add('title', (string)trans('validation.same_account_currency')); + $validator->errors()->add('title', (string) trans('validation.same_account_currency')); return; } @@ -107,7 +107,7 @@ class MoveTransactionsRequest extends FormRequest return; } if ($originalCurrency->code !== $destinationCurrency->code) { - $validator->errors()->add('title', (string)trans('validation.same_account_currency')); + $validator->errors()->add('title', (string) trans('validation.same_account_currency')); } } } diff --git a/app/Api/V1/Requests/Data/Export/ExportRequest.php b/app/Api/V1/Requests/Data/Export/ExportRequest.php index f2cf10ce29..82808d78a4 100644 --- a/app/Api/V1/Requests/Data/Export/ExportRequest.php +++ b/app/Api/V1/Requests/Data/Export/ExportRequest.php @@ -52,7 +52,7 @@ class ExportRequest extends FormRequest $accounts = new Collection(); foreach ($parts as $part) { - $accountId = (int)$part; + $accountId = (int) $part; if (0 !== $accountId) { $account = $repository->find($accountId); if (null !== $account && AccountType::ASSET === $account->accountType->type) { diff --git a/app/Api/V1/Requests/Insight/GenericRequest.php b/app/Api/V1/Requests/Insight/GenericRequest.php index 1bde6efeb7..893789e778 100644 --- a/app/Api/V1/Requests/Insight/GenericRequest.php +++ b/app/Api/V1/Requests/Insight/GenericRequest.php @@ -88,7 +88,7 @@ class GenericRequest extends FormRequest $array = $this->get('accounts'); if (is_array($array)) { foreach ($array as $accountId) { - $accountId = (int)$accountId; + $accountId = (int) $accountId; $account = $repository->find($accountId); if (null !== $account) { $this->accounts->push($account); @@ -114,7 +114,7 @@ class GenericRequest extends FormRequest $array = $this->get('bills'); if (is_array($array)) { foreach ($array as $billId) { - $billId = (int)$billId; + $billId = (int) $billId; $bill = $repository->find($billId); if (null !== $bill) { $this->bills->push($bill); @@ -140,7 +140,7 @@ class GenericRequest extends FormRequest $array = $this->get('budgets'); if (is_array($array)) { foreach ($array as $budgetId) { - $budgetId = (int)$budgetId; + $budgetId = (int) $budgetId; $budget = $repository->find($budgetId); if (null !== $budget) { $this->budgets->push($budget); @@ -166,7 +166,7 @@ class GenericRequest extends FormRequest $array = $this->get('categories'); if (is_array($array)) { foreach ($array as $categoryId) { - $categoryId = (int)$categoryId; + $categoryId = (int) $categoryId; $category = $repository->find($categoryId); if (null !== $category) { $this->categories->push($category); @@ -240,7 +240,7 @@ class GenericRequest extends FormRequest $array = $this->get('tags'); if (is_array($array)) { foreach ($array as $tagId) { - $tagId = (int)$tagId; + $tagId = (int) $tagId; $tag = $repository->find($tagId); if (null !== $tag) { $this->tags->push($tag); diff --git a/app/Api/V1/Requests/Models/AvailableBudget/Request.php b/app/Api/V1/Requests/Models/AvailableBudget/Request.php index 2b8a6d3b7b..613855646f 100644 --- a/app/Api/V1/Requests/Models/AvailableBudget/Request.php +++ b/app/Api/V1/Requests/Models/AvailableBudget/Request.php @@ -84,7 +84,7 @@ class Request extends FormRequest $start = new Carbon($data['start']); $end = new Carbon($data['end']); if ($end->isBefore($start)) { - $validator->errors()->add('end', (string)trans('validation.date_after')); + $validator->errors()->add('end', (string) trans('validation.date_after')); } } } diff --git a/app/Api/V1/Requests/Models/Bill/StoreRequest.php b/app/Api/V1/Requests/Models/Bill/StoreRequest.php index 7884065e97..5966bfbcff 100644 --- a/app/Api/V1/Requests/Models/Bill/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Bill/StoreRequest.php @@ -96,11 +96,11 @@ class StoreRequest extends FormRequest $validator->after( static function (Validator $validator): void { $data = $validator->getData(); - $min = (string)($data['amount_min'] ?? '0'); - $max = (string)($data['amount_max'] ?? '0'); + $min = (string) ($data['amount_min'] ?? '0'); + $max = (string) ($data['amount_max'] ?? '0'); if (1 === bccomp($min, $max)) { - $validator->errors()->add('amount_min', (string)trans('validation.amount_min_over_max')); + $validator->errors()->add('amount_min', (string) trans('validation.amount_min_over_max')); } } ); diff --git a/app/Api/V1/Requests/Models/Bill/UpdateRequest.php b/app/Api/V1/Requests/Models/Bill/UpdateRequest.php index 0df5139965..4b666bac4a 100644 --- a/app/Api/V1/Requests/Models/Bill/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Bill/UpdateRequest.php @@ -104,7 +104,7 @@ class UpdateRequest extends FormRequest $max = $data['amount_max'] ?? '0'; if (1 === bccomp($min, $max)) { - $validator->errors()->add('amount_min', (string)trans('validation.amount_min_over_max')); + $validator->errors()->add('amount_min', (string) trans('validation.amount_min_over_max')); } } } diff --git a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php index 4fc0e8bf5b..48c77cf2a2 100644 --- a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php +++ b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php @@ -48,6 +48,7 @@ class StoreRequest extends FormRequest 'amount' => $this->convertString('amount'), 'currency_id' => $this->convertInteger('currency_id'), 'currency_code' => $this->convertString('currency_code'), + 'notes' => $this->stringWithNewlines('notes'), ]; } @@ -62,6 +63,7 @@ class StoreRequest extends FormRequest 'amount' => ['required', new IsValidPositiveAmount()], 'currency_id' => 'numeric|exists:transaction_currencies,id', 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', + 'notes' => 'nullable|min:0|max:32768', ]; } } diff --git a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php index f93cebb012..16814d24a6 100644 --- a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php @@ -51,7 +51,12 @@ class UpdateRequest extends FormRequest 'amount' => ['amount', 'convertString'], 'currency_id' => ['currency_id', 'convertInteger'], 'currency_code' => ['currency_code', 'convertString'], + 'notes' => ['notes', 'stringWithNewlines'], ]; + if (false === $this->has('notes')) { + // ignore notes, not submitted. + unset($fields['notes']); + } return $this->getAllData($fields); } @@ -67,6 +72,7 @@ class UpdateRequest extends FormRequest 'amount' => ['nullable', new IsValidPositiveAmount()], 'currency_id' => 'numeric|exists:transaction_currencies,id', 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', + 'notes' => 'nullable|min:0|max:32768', ]; } @@ -84,7 +90,7 @@ class UpdateRequest extends FormRequest $start = new Carbon($data['start']); $end = new Carbon($data['end']); if ($end->isBefore($start)) { - $validator->errors()->add('end', (string)trans('validation.date_after')); + $validator->errors()->add('end', (string) trans('validation.date_after')); } } } diff --git a/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php b/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php index 8adc656a65..b4c416f1a3 100644 --- a/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php +++ b/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php @@ -24,10 +24,15 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Models\PiggyBank; -use FireflyIII\Rules\IsValidPositiveAmount; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Rules\IsValidZeroOrMoreAmount; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Log; +use Illuminate\Validation\Validator; /** * Class StoreRequest @@ -42,38 +47,123 @@ class StoreRequest extends FormRequest */ public function getAll(): array { - $fields = [ + $fields = [ 'order' => ['order', 'convertInteger'], ]; - $data = $this->getAllData($fields); - $data['name'] = $this->convertString('name'); - $data['account_id'] = $this->convertInteger('account_id'); - $data['targetamount'] = $this->convertString('target_amount'); - $data['current_amount'] = $this->convertString('current_amount'); - $data['startdate'] = $this->getCarbonDate('start_date'); - $data['targetdate'] = $this->getCarbonDate('target_date'); - $data['notes'] = $this->stringWithNewlines('notes'); - $data['object_group_id'] = $this->convertInteger('object_group_id'); - $data['object_group_title'] = $this->convertString('object_group_title'); + $data = $this->getAllData($fields); + $data['name'] = $this->convertString('name'); + $data['accounts'] = $this->parseAccounts($this->get('accounts')); + $data['target_amount'] = $this->convertString('target_amount'); + $data['start_date'] = $this->getCarbonDate('start_date'); + $data['target_date'] = $this->getCarbonDate('target_date'); + $data['notes'] = $this->stringWithNewlines('notes'); + $data['object_group_id'] = $this->convertInteger('object_group_id'); + $data['transaction_currency_id'] = $this->convertInteger('transaction_currency_id'); + $data['transaction_currency_code'] = $this->convertString('transaction_currency_code'); + $data['object_group_title'] = $this->convertString('object_group_title'); return $data; } + private function parseAccounts(mixed $array): array + { + if (!is_array($array)) { + return []; + } + $return = []; + foreach ($array as $entry) { + if (!is_array($entry)) { + continue; + } + $return[] = [ + 'account_id' => $this->integerFromValue((string) ($entry['account_id'] ?? '0')), + 'current_amount' => $this->clearString((string) ($entry['current_amount'] ?? '0')), + ]; + } + + return $return; + } + /** * The rules that the incoming request must be matched against. */ public function rules(): array { return [ - 'name' => 'required|min:1|max:255|uniquePiggyBankForUser', - 'current_amount' => ['nullable', new IsValidPositiveAmount()], - 'account_id' => 'required|numeric|belongsToUser:accounts,id', - 'object_group_id' => 'numeric|belongsToUser:object_groups,id', - 'object_group_title' => ['min:1', 'max:255'], - 'target_amount' => ['required', new IsValidPositiveAmount()], - 'start_date' => 'date|nullable', - 'target_date' => 'date|nullable|after:start_date', - 'notes' => 'max:65000', + 'name' => 'required|min:1|max:255|uniquePiggyBankForUser', + 'accounts' => 'required', + 'accounts.*' => 'array|required', + 'accounts.*.account_id' => 'required|numeric|belongsToUser:accounts,id', + 'accounts.*.current_amount' => ['numeric', new IsValidZeroOrMoreAmount()], + 'object_group_id' => 'numeric|belongsToUser:object_groups,id', + 'object_group_title' => ['min:1', 'max:255'], + 'target_amount' => ['required', new IsValidZeroOrMoreAmount()], + 'start_date' => 'date|nullable', + 'transaction_currency_id' => 'exists:transaction_currencies,id|required_without:transaction_currency_code', + 'transaction_currency_code' => 'exists:transaction_currencies,code|required_without:transaction_currency_id', + 'target_date' => 'date|nullable|after:start_date', + 'notes' => 'max:65000', ]; } + + /** + * Can only store money on liabilities and asset accounts. + */ + public function withValidator(Validator $validator): void + { + $validator->after( + function (Validator $validator): void { + // validate start before end only if both are there. + $data = $validator->getData(); + $currency = $this->getCurrencyFromData($data); + $targetAmount = (string) ($data['target_amount'] ?? '0'); + $currentAmount = '0'; + if (array_key_exists('accounts', $data) && is_array($data['accounts'])) { + $repository = app(AccountRepositoryInterface::class); + $types = config('firefly.piggy_bank_account_types'); + foreach ($data['accounts'] as $index => $array) { + $accountId = (int) ($array['account_id'] ?? 0); + $account = $repository->find($accountId); + if (null !== $account) { + // check currency here. + $accountCurrency = $repository->getAccountCurrency($account); + $isMultiCurrency = $repository->getMetaValue($account, 'is_multi_currency'); + $currentAmount = bcadd($currentAmount, (string) ($array['current_amount'] ?? '0')); + if ($accountCurrency->id !== $currency->id && 'true' !== $isMultiCurrency) { + $validator->errors()->add(sprintf('accounts.%d', $index), trans('validation.invalid_account_currency')); + } + $type = $account->accountType->type; + if (!in_array($type, $types, true)) { + $validator->errors()->add(sprintf('accounts.%d', $index), trans('validation.invalid_account_type')); + } + } + } + } + if (-1 === bccomp($targetAmount, $currentAmount) && 1 === bccomp($targetAmount, '0')) { + $validator->errors()->add('target_amount', trans('validation.current_amount_too_much')); + } + } + ); + if ($validator->fails()) { + Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); + } + } + + private function getCurrencyFromData(array $data): TransactionCurrency + { + if (array_key_exists('transaction_currency_code', $data) && '' !== (string) $data['transaction_currency_code']) { + $currency = TransactionCurrency::whereCode($data['transaction_currency_code'])->first(); + if (null !== $currency) { + return $currency; + } + } + if (array_key_exists('transaction_currency_id', $data) && '' !== (string) $data['transaction_currency_id']) { + $currency = TransactionCurrency::find((int) $data['transaction_currency_id']); + if (null !== $currency) { + return $currency; + } + } + + throw new FireflyException('Unexpected empty currency.'); + } } diff --git a/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php b/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php index 3a40023f0c..dd2aa5fb82 100644 --- a/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php @@ -121,10 +121,10 @@ class StoreRequest extends FormRequest $current['moment'] = $repetition['moment']; } if (array_key_exists('skip', $repetition)) { - $current['skip'] = (int)$repetition['skip']; + $current['skip'] = (int) $repetition['skip']; } if (array_key_exists('weekend', $repetition)) { - $current['weekend'] = (int)$repetition['weekend']; + $current['weekend'] = (int) $repetition['weekend']; } $return[] = $current; diff --git a/app/Api/V1/Requests/Models/Recurrence/UpdateRequest.php b/app/Api/V1/Requests/Models/Recurrence/UpdateRequest.php index 616db3b0e2..8c7ab24f79 100644 --- a/app/Api/V1/Requests/Models/Recurrence/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Recurrence/UpdateRequest.php @@ -101,15 +101,15 @@ class UpdateRequest extends FormRequest } if (array_key_exists('moment', $repetition)) { - $current['moment'] = (string)$repetition['moment']; + $current['moment'] = (string) $repetition['moment']; } if (array_key_exists('skip', $repetition)) { - $current['skip'] = (int)$repetition['skip']; + $current['skip'] = (int) $repetition['skip']; } if (array_key_exists('weekend', $repetition)) { - $current['weekend'] = (int)$repetition['weekend']; + $current['weekend'] = (int) $repetition['weekend']; } $return[] = $current; } diff --git a/app/Api/V1/Requests/Models/Rule/StoreRequest.php b/app/Api/V1/Requests/Models/Rule/StoreRequest.php index f931324944..fb5b511f4b 100644 --- a/app/Api/V1/Requests/Models/Rule/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Rule/StoreRequest.php @@ -74,9 +74,9 @@ class StoreRequest extends FormRequest $return[] = [ 'type' => $trigger['type'] ?? '', 'value' => $trigger['value'] ?? null, - 'prohibited' => $this->convertBoolean((string)($trigger['prohibited'] ?? 'false')), - 'active' => $this->convertBoolean((string)($trigger['active'] ?? 'true')), - 'stop_processing' => $this->convertBoolean((string)($trigger['stop_processing'] ?? 'false')), + 'prohibited' => $this->convertBoolean((string) ($trigger['prohibited'] ?? 'false')), + 'active' => $this->convertBoolean((string) ($trigger['active'] ?? 'true')), + 'stop_processing' => $this->convertBoolean((string) ($trigger['stop_processing'] ?? 'false')), ]; } } @@ -93,8 +93,8 @@ class StoreRequest extends FormRequest $return[] = [ 'type' => $action['type'], 'value' => $action['value'], - 'active' => $this->convertBoolean((string)($action['active'] ?? 'true')), - 'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')), + 'active' => $this->convertBoolean((string) ($action['active'] ?? 'true')), + 'stop_processing' => $this->convertBoolean((string) ($action['stop_processing'] ?? 'false')), ]; } } @@ -119,7 +119,7 @@ class StoreRequest extends FormRequest 'description' => 'min:1|max:32768|nullable', 'rule_group_id' => 'belongsToUser:rule_groups|required_without:rule_group_title', 'rule_group_title' => 'nullable|min:1|max:255|required_without:rule_group_id|belongsToUser:rule_groups,title', - 'trigger' => 'required|in:store-journal,update-journal', + 'trigger' => 'required|in:store-journal,update-journal,manual-activation', 'triggers.*.type' => 'required|in:'.implode(',', $validTriggers), 'triggers.*.value' => 'required_if:actions.*.type,'.$contextTriggers.'|min:1|ruleTriggerValue|max:1024', 'triggers.*.stop_processing' => [new IsBoolean()], @@ -161,7 +161,7 @@ class StoreRequest extends FormRequest $triggers = $data['triggers'] ?? []; // need at least one trigger if (!is_countable($triggers) || 0 === count($triggers)) { - $validator->errors()->add('title', (string)trans('validation.at_least_one_trigger')); + $validator->errors()->add('title', (string) trans('validation.at_least_one_trigger')); } } @@ -174,7 +174,7 @@ class StoreRequest extends FormRequest $actions = $data['actions'] ?? []; // need at least one trigger if (!is_countable($actions) || 0 === count($actions)) { - $validator->errors()->add('title', (string)trans('validation.at_least_one_action')); + $validator->errors()->add('title', (string) trans('validation.at_least_one_action')); } } @@ -203,7 +203,7 @@ class StoreRequest extends FormRequest } } if (true === $allInactive) { - $validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_trigger')); + $validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_trigger')); } } @@ -232,7 +232,7 @@ class StoreRequest extends FormRequest } } if (true === $allInactive) { - $validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action')); + $validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_action')); } } } diff --git a/app/Api/V1/Requests/Models/Rule/TestRequest.php b/app/Api/V1/Requests/Models/Rule/TestRequest.php index bed4427932..2703365859 100644 --- a/app/Api/V1/Requests/Models/Rule/TestRequest.php +++ b/app/Api/V1/Requests/Models/Rule/TestRequest.php @@ -49,7 +49,7 @@ class TestRequest extends FormRequest private function getPage(): int { - return 0 === (int)$this->query('page') ? 1 : (int)$this->query('page'); + return 0 === (int) $this->query('page') ? 1 : (int) $this->query('page'); } private function getDate(string $field): ?Carbon @@ -58,7 +58,7 @@ class TestRequest extends FormRequest if (is_array($value)) { return null; } - $value = (string)$value; + $value = (string) $value; return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); } diff --git a/app/Api/V1/Requests/Models/Rule/TriggerRequest.php b/app/Api/V1/Requests/Models/Rule/TriggerRequest.php index d1bc942fae..77f9a9e650 100644 --- a/app/Api/V1/Requests/Models/Rule/TriggerRequest.php +++ b/app/Api/V1/Requests/Models/Rule/TriggerRequest.php @@ -52,7 +52,7 @@ class TriggerRequest extends FormRequest if (is_array($value)) { return null; } - $value = (string)$value; + $value = (string) $value; return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); } diff --git a/app/Api/V1/Requests/Models/Rule/UpdateRequest.php b/app/Api/V1/Requests/Models/Rule/UpdateRequest.php index 6a07753b17..d9fecdc314 100644 --- a/app/Api/V1/Requests/Models/Rule/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Rule/UpdateRequest.php @@ -109,8 +109,8 @@ class UpdateRequest extends FormRequest $return[] = [ 'type' => $action['type'], 'value' => $action['value'], - 'active' => $this->convertBoolean((string)($action['active'] ?? 'false')), - 'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')), + 'active' => $this->convertBoolean((string) ($action['active'] ?? 'false')), + 'stop_processing' => $this->convertBoolean((string) ($action['stop_processing'] ?? 'false')), ]; } } @@ -138,7 +138,7 @@ class UpdateRequest extends FormRequest 'description' => 'min:1|max:32768|nullable', 'rule_group_id' => 'belongsToUser:rule_groups', 'rule_group_title' => 'nullable|min:1|max:255|belongsToUser:rule_groups,title', - 'trigger' => 'in:store-journal,update-journal', + 'trigger' => 'in:store-journal,update-journal.manual-activation', 'triggers.*.type' => 'required|in:'.implode(',', $validTriggers), 'triggers.*.value' => 'required_if:actions.*.type,'.$contextTriggers.'|min:1|ruleTriggerValue|max:1024', 'triggers.*.stop_processing' => [new IsBoolean()], @@ -181,7 +181,7 @@ class UpdateRequest extends FormRequest $triggers = $data['triggers'] ?? null; // need at least one trigger if (is_array($triggers) && 0 === count($triggers)) { - $validator->errors()->add('title', (string)trans('validation.at_least_one_trigger')); + $validator->errors()->add('title', (string) trans('validation.at_least_one_trigger')); } } @@ -208,7 +208,7 @@ class UpdateRequest extends FormRequest } } if (true === $allInactive) { - $validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_trigger')); + $validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_trigger')); } } @@ -221,7 +221,7 @@ class UpdateRequest extends FormRequest $actions = $data['actions'] ?? null; // need at least one action if (is_array($actions) && 0 === count($actions)) { - $validator->errors()->add('title', (string)trans('validation.at_least_one_action')); + $validator->errors()->add('title', (string) trans('validation.at_least_one_action')); } } @@ -249,7 +249,7 @@ class UpdateRequest extends FormRequest } } if (true === $allInactive) { - $validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action')); + $validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_action')); } } } diff --git a/app/Api/V1/Requests/Models/RuleGroup/TestRequest.php b/app/Api/V1/Requests/Models/RuleGroup/TestRequest.php index d87b6af6f3..839c6c15ed 100644 --- a/app/Api/V1/Requests/Models/RuleGroup/TestRequest.php +++ b/app/Api/V1/Requests/Models/RuleGroup/TestRequest.php @@ -52,7 +52,7 @@ class TestRequest extends FormRequest if (is_array($value)) { return null; } - $value = (string)$value; + $value = (string) $value; return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); } diff --git a/app/Api/V1/Requests/Models/RuleGroup/TriggerRequest.php b/app/Api/V1/Requests/Models/RuleGroup/TriggerRequest.php index 3e35334e85..fe9d0a002d 100644 --- a/app/Api/V1/Requests/Models/RuleGroup/TriggerRequest.php +++ b/app/Api/V1/Requests/Models/RuleGroup/TriggerRequest.php @@ -52,7 +52,7 @@ class TriggerRequest extends FormRequest if (is_array($value)) { return null; } - $value = (string)$value; + $value = (string) $value; return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); } diff --git a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php index 320629f598..9e4f63a05b 100644 --- a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php @@ -84,73 +84,73 @@ class StoreRequest extends FormRequest $return[] = [ 'type' => $this->clearString($object['type']), 'date' => $this->dateFromValue($object['date']), - 'order' => $this->integerFromValue((string)$object['order']), + 'order' => $this->integerFromValue((string) $object['order']), - 'currency_id' => $this->integerFromValue((string)$object['currency_id']), - 'currency_code' => $this->clearString((string)$object['currency_code']), + 'currency_id' => $this->integerFromValue((string) $object['currency_id']), + 'currency_code' => $this->clearString((string) $object['currency_code']), // foreign currency info: - 'foreign_currency_id' => $this->integerFromValue((string)$object['foreign_currency_id']), - 'foreign_currency_code' => $this->clearString((string)$object['foreign_currency_code']), + 'foreign_currency_id' => $this->integerFromValue((string) $object['foreign_currency_id']), + 'foreign_currency_code' => $this->clearString((string) $object['foreign_currency_code']), // amount and foreign amount. Cannot be 0. - 'amount' => $this->clearString((string)$object['amount']), - 'foreign_amount' => $this->clearString((string)$object['foreign_amount']), + 'amount' => $this->clearString((string) $object['amount']), + 'foreign_amount' => $this->clearString((string) $object['foreign_amount']), // description. 'description' => $this->clearString($object['description']), // source of transaction. If everything is null, assume cash account. - 'source_id' => $this->integerFromValue((string)$object['source_id']), - 'source_name' => $this->clearString((string)$object['source_name']), - 'source_iban' => $this->clearIban((string)$object['source_iban']), - 'source_number' => $this->clearString((string)$object['source_number']), - 'source_bic' => $this->clearString((string)$object['source_bic']), + 'source_id' => $this->integerFromValue((string) $object['source_id']), + 'source_name' => $this->clearString((string) $object['source_name']), + 'source_iban' => $this->clearIban((string) $object['source_iban']), + 'source_number' => $this->clearString((string) $object['source_number']), + 'source_bic' => $this->clearString((string) $object['source_bic']), // destination of transaction. If everything is null, assume cash account. - 'destination_id' => $this->integerFromValue((string)$object['destination_id']), - 'destination_name' => $this->clearString((string)$object['destination_name']), - 'destination_iban' => $this->clearIban((string)$object['destination_iban']), - 'destination_number' => $this->clearString((string)$object['destination_number']), - 'destination_bic' => $this->clearString((string)$object['destination_bic']), + 'destination_id' => $this->integerFromValue((string) $object['destination_id']), + 'destination_name' => $this->clearString((string) $object['destination_name']), + 'destination_iban' => $this->clearIban((string) $object['destination_iban']), + 'destination_number' => $this->clearString((string) $object['destination_number']), + 'destination_bic' => $this->clearString((string) $object['destination_bic']), // budget info - 'budget_id' => $this->integerFromValue((string)$object['budget_id']), - 'budget_name' => $this->clearString((string)$object['budget_name']), + 'budget_id' => $this->integerFromValue((string) $object['budget_id']), + 'budget_name' => $this->clearString((string) $object['budget_name']), // category info - 'category_id' => $this->integerFromValue((string)$object['category_id']), - 'category_name' => $this->clearString((string)$object['category_name']), + 'category_id' => $this->integerFromValue((string) $object['category_id']), + 'category_name' => $this->clearString((string) $object['category_name']), // journal bill reference. Optional. Will only work for withdrawals - 'bill_id' => $this->integerFromValue((string)$object['bill_id']), - 'bill_name' => $this->clearString((string)$object['bill_name']), + 'bill_id' => $this->integerFromValue((string) $object['bill_id']), + 'bill_name' => $this->clearString((string) $object['bill_name']), // piggy bank reference. Optional. Will only work for transfers - 'piggy_bank_id' => $this->integerFromValue((string)$object['piggy_bank_id']), - 'piggy_bank_name' => $this->clearString((string)$object['piggy_bank_name']), + 'piggy_bank_id' => $this->integerFromValue((string) $object['piggy_bank_id']), + 'piggy_bank_name' => $this->clearString((string) $object['piggy_bank_name']), // some other interesting properties - 'reconciled' => $this->convertBoolean((string)$object['reconciled']), - 'notes' => $this->clearStringKeepNewlines((string)$object['notes']), + 'reconciled' => $this->convertBoolean((string) $object['reconciled']), + 'notes' => $this->clearStringKeepNewlines((string) $object['notes']), 'tags' => $this->arrayFromValue($object['tags']), // all custom fields: - 'internal_reference' => $this->clearString((string)$object['internal_reference']), - 'external_id' => $this->clearString((string)$object['external_id']), + 'internal_reference' => $this->clearString((string) $object['internal_reference']), + 'external_id' => $this->clearString((string) $object['external_id']), 'original_source' => sprintf('ff3-v%s', config('firefly.version')), 'recurrence_id' => $this->integerFromValue($object['recurrence_id']), - 'bunq_payment_id' => $this->clearString((string)$object['bunq_payment_id']), - 'external_url' => $this->clearString((string)$object['external_url']), + 'bunq_payment_id' => $this->clearString((string) $object['bunq_payment_id']), + 'external_url' => $this->clearString((string) $object['external_url']), - 'sepa_cc' => $this->clearString((string)$object['sepa_cc']), - 'sepa_ct_op' => $this->clearString((string)$object['sepa_ct_op']), - 'sepa_ct_id' => $this->clearString((string)$object['sepa_ct_id']), - 'sepa_db' => $this->clearString((string)$object['sepa_db']), - 'sepa_country' => $this->clearString((string)$object['sepa_country']), - 'sepa_ep' => $this->clearString((string)$object['sepa_ep']), - 'sepa_ci' => $this->clearString((string)$object['sepa_ci']), - 'sepa_batch_id' => $this->clearString((string)$object['sepa_batch_id']), + 'sepa_cc' => $this->clearString((string) $object['sepa_cc']), + 'sepa_ct_op' => $this->clearString((string) $object['sepa_ct_op']), + 'sepa_ct_id' => $this->clearString((string) $object['sepa_ct_id']), + 'sepa_db' => $this->clearString((string) $object['sepa_db']), + 'sepa_country' => $this->clearString((string) $object['sepa_country']), + 'sepa_ep' => $this->clearString((string) $object['sepa_ep']), + 'sepa_ci' => $this->clearString((string) $object['sepa_ci']), + 'sepa_batch_id' => $this->clearString((string) $object['sepa_batch_id']), // custom date fields. Must be Carbon objects. Presence is optional. 'interest_date' => $this->dateFromValue($object['interest_date']), 'book_date' => $this->dateFromValue($object['book_date']), diff --git a/app/Api/V1/Requests/Models/Transaction/UpdateRequest.php b/app/Api/V1/Requests/Models/Transaction/UpdateRequest.php index b6a3f570b4..2d18b028e3 100644 --- a/app/Api/V1/Requests/Models/Transaction/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Transaction/UpdateRequest.php @@ -137,7 +137,7 @@ class UpdateRequest extends FormRequest { foreach ($this->integerFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->integerFromValue((string)$transaction[$fieldName]); + $current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]); } } @@ -152,7 +152,7 @@ class UpdateRequest extends FormRequest { foreach ($this->stringFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->clearString((string)$transaction[$fieldName]); + $current[$fieldName] = $this->clearString((string) $transaction[$fieldName]); } } @@ -167,7 +167,7 @@ class UpdateRequest extends FormRequest { foreach ($this->textareaFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->clearStringKeepNewlines((string)$transaction[$fieldName]); // keep newlines + $current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines } } @@ -183,8 +183,8 @@ class UpdateRequest extends FormRequest foreach ($this->dateFields as $fieldName) { app('log')->debug(sprintf('Now at date field %s', $fieldName)); if (array_key_exists($fieldName, $transaction)) { - app('log')->debug(sprintf('New value: "%s"', (string)$transaction[$fieldName])); - $current[$fieldName] = $this->dateFromValue((string)$transaction[$fieldName]); + app('log')->debug(sprintf('New value: "%s"', (string) $transaction[$fieldName])); + $current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]); } } @@ -199,7 +199,7 @@ class UpdateRequest extends FormRequest { foreach ($this->booleanFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->convertBoolean((string)$transaction[$fieldName]); + $current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]); } } @@ -234,7 +234,7 @@ class UpdateRequest extends FormRequest $current[$fieldName] = sprintf('%.12f', $value); } if (!is_float($value)) { - $current[$fieldName] = (string)$value; + $current[$fieldName] = (string) $value; } } } diff --git a/app/Api/V1/Requests/Models/TransactionLink/StoreRequest.php b/app/Api/V1/Requests/Models/TransactionLink/StoreRequest.php index 61ed0dd4da..d395da5fd8 100644 --- a/app/Api/V1/Requests/Models/TransactionLink/StoreRequest.php +++ b/app/Api/V1/Requests/Models/TransactionLink/StoreRequest.php @@ -98,8 +98,8 @@ class StoreRequest extends FormRequest $journalRepos->setUser($user); $data = $validator->getData(); - $inwardId = (int)($data['inward_id'] ?? 0); - $outwardId = (int)($data['outward_id'] ?? 0); + $inwardId = (int) ($data['inward_id'] ?? 0); + $outwardId = (int) ($data['outward_id'] ?? 0); $inward = $journalRepos->find($inwardId); $outward = $journalRepos->find($outwardId); diff --git a/app/Api/V1/Requests/Models/TransactionLink/UpdateRequest.php b/app/Api/V1/Requests/Models/TransactionLink/UpdateRequest.php index 0a889b512e..5aaeb892cb 100644 --- a/app/Api/V1/Requests/Models/TransactionLink/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/TransactionLink/UpdateRequest.php @@ -100,8 +100,8 @@ class UpdateRequest extends FormRequest $inwardId = $data['inward_id'] ?? $existing->source_id; $outwardId = $data['outward_id'] ?? $existing->destination_id; - $inward = $journalRepos->find((int)$inwardId); - $outward = $journalRepos->find((int)$outwardId); + $inward = $journalRepos->find((int) $inwardId); + $outward = $journalRepos->find((int) $outwardId); if (null === $inward) { $inward = $existing->source; } diff --git a/app/Api/V1/Requests/Models/Webhook/CreateRequest.php b/app/Api/V1/Requests/Models/Webhook/CreateRequest.php index 8ec63d234c..03720f9350 100644 --- a/app/Api/V1/Requests/Models/Webhook/CreateRequest.php +++ b/app/Api/V1/Requests/Models/Webhook/CreateRequest.php @@ -55,9 +55,9 @@ class CreateRequest extends FormRequest // this is the way. $return = $this->getAllData($fields); - $return['trigger'] = $triggers[$return['trigger']] ?? (int)$return['trigger']; - $return['response'] = $responses[$return['response']] ?? (int)$return['response']; - $return['delivery'] = $deliveries[$return['delivery']] ?? (int)$return['delivery']; + $return['trigger'] = $triggers[$return['trigger']] ?? (int) $return['trigger']; + $return['response'] = $responses[$return['response']] ?? (int) $return['response']; + $return['delivery'] = $deliveries[$return['delivery']] ?? (int) $return['delivery']; return $return; } diff --git a/app/Api/V1/Requests/System/UserUpdateRequest.php b/app/Api/V1/Requests/System/UserUpdateRequest.php index 19cda61d3d..1fc0df7bb3 100644 --- a/app/Api/V1/Requests/System/UserUpdateRequest.php +++ b/app/Api/V1/Requests/System/UserUpdateRequest.php @@ -94,7 +94,7 @@ class UserUpdateRequest extends FormRequest $isAdmin = auth()->user()->hasRole('owner'); // not admin, and not own user? if (auth()->check() && false === $isAdmin && $current?->id !== auth()->user()->id) { - $validator->errors()->add('email', (string)trans('validation.invalid_selection')); + $validator->errors()->add('email', (string) trans('validation.invalid_selection')); } } ); diff --git a/app/Api/V1/Requests/User/PreferenceStoreRequest.php b/app/Api/V1/Requests/User/PreferenceStoreRequest.php index 98b2ae006b..22ee8ad706 100644 --- a/app/Api/V1/Requests/User/PreferenceStoreRequest.php +++ b/app/Api/V1/Requests/User/PreferenceStoreRequest.php @@ -49,7 +49,7 @@ class PreferenceStoreRequest extends FormRequest $array['data'] = false; } if (is_numeric($array['data'])) { - $array['data'] = (float)$array['data']; // intentional float. + $array['data'] = (float) $array['data']; // intentional float. } return $array; diff --git a/app/Api/V1/Requests/User/PreferenceUpdateRequest.php b/app/Api/V1/Requests/User/PreferenceUpdateRequest.php index 3f62c47f87..7489f7e2de 100644 --- a/app/Api/V1/Requests/User/PreferenceUpdateRequest.php +++ b/app/Api/V1/Requests/User/PreferenceUpdateRequest.php @@ -49,7 +49,7 @@ class PreferenceUpdateRequest extends FormRequest $array['data'] = false; } if (is_numeric($array['data'])) { - $array['data'] = (float)$array['data']; // intentional float. + $array['data'] = (float) $array['data']; // intentional float. } return $array; diff --git a/app/Api/V2/Controllers/Autocomplete/AccountController.php b/app/Api/V2/Controllers/Autocomplete/AccountController.php index ffe030b4a3..1531c70ae0 100644 --- a/app/Api/V2/Controllers/Autocomplete/AccountController.php +++ b/app/Api/V2/Controllers/Autocomplete/AccountController.php @@ -40,9 +40,9 @@ use Illuminate\Support\Facades\Log; */ class AccountController extends Controller { - private AccountRepositoryInterface $repository; - private TransactionCurrency $default; private ExchangeRateConverter $converter; + private TransactionCurrency $default; + private AccountRepositoryInterface $repository; /** * AccountController constructor. diff --git a/app/Api/V2/Controllers/Autocomplete/CategoryController.php b/app/Api/V2/Controllers/Autocomplete/CategoryController.php index 686e866e86..be81418f58 100644 --- a/app/Api/V2/Controllers/Autocomplete/CategoryController.php +++ b/app/Api/V2/Controllers/Autocomplete/CategoryController.php @@ -63,7 +63,7 @@ class CategoryController extends Controller $filtered = $result->map( static function (Category $item) { return [ - 'id' => (string)$item->id, + 'id' => (string) $item->id, 'title' => $item->name, 'meta' => [], ]; diff --git a/app/Api/V2/Controllers/Chart/AccountController.php b/app/Api/V2/Controllers/Chart/AccountController.php index 9ba3f72b3b..1a75860b32 100644 --- a/app/Api/V2/Controllers/Chart/AccountController.php +++ b/app/Api/V2/Controllers/Chart/AccountController.php @@ -45,9 +45,9 @@ class AccountController extends Controller use CollectsAccountsFromFilter; use ValidatesUserGroupTrait; - private AccountRepositoryInterface $repository; private ChartData $chartData; private TransactionCurrency $default; + private AccountRepositoryInterface $repository; public function __construct() { @@ -92,11 +92,11 @@ class AccountController extends Controller */ private function renderAccountData(array $params, Account $account): void { - $currency = $this->repository->getAccountCurrency($account); + $currency = $this->repository->getAccountCurrency($account); if (null === $currency) { $currency = $this->default; } - $currentSet = [ + $currentSet = [ 'label' => $account->name, // the currency that belongs to the account. @@ -117,23 +117,22 @@ class AccountController extends Controller 'entries' => [], 'native_entries' => [], ]; - $currentStart = clone $params['start']; - $range = app('steam')->balanceInRange($account, $params['start'], clone $params['end'], $currency); - $rangeConverted = app('steam')->balanceInRangeConverted($account, $params['start'], clone $params['end'], $this->default); + $currentStart = clone $params['start']; + $range = app('steam')->finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToNative); - $previous = array_values($range)[0]; - $previousConverted = array_values($rangeConverted)[0]; + $previous = array_values($range)[0]['balance']; + $previousNative = array_values($range)[0]['native_balance']; while ($currentStart <= $params['end']) { $format = $currentStart->format('Y-m-d'); $label = $currentStart->toAtomString(); - $balance = array_key_exists($format, $range) ? $range[$format] : $previous; - $balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted; + $balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous; + $balanceNative = array_key_exists($format, $range) ? $range[$format]['balance_native'] : $previousNative; $previous = $balance; - $previousConverted = $balanceConverted; + $previousNative = $balanceNative; $currentStart->addDay(); $currentSet['entries'][$label] = $balance; - $currentSet['native_entries'][$label] = $balanceConverted; + $currentSet['native_entries'][$label] = $balanceNative; } $this->chartData->add($currentSet); } diff --git a/app/Api/V2/Controllers/Chart/BalanceController.php b/app/Api/V2/Controllers/Chart/BalanceController.php index 91af9acb40..89a1c9f8cd 100644 --- a/app/Api/V2/Controllers/Chart/BalanceController.php +++ b/app/Api/V2/Controllers/Chart/BalanceController.php @@ -45,9 +45,10 @@ class BalanceController extends Controller use CleansChartData; use CollectsAccountsFromFilter; - private AccountRepositoryInterface $repository; - private GroupCollectorInterface $collector; private ChartData $chartData; + private GroupCollectorInterface $collector; + private AccountRepositoryInterface $repository; + // private TransactionCurrency $default; public function __construct() diff --git a/app/Api/V2/Controllers/Controller.php b/app/Api/V2/Controllers/Controller.php index 02631c38cd..ee8d782e48 100644 --- a/app/Api/V2/Controllers/Controller.php +++ b/app/Api/V2/Controllers/Controller.php @@ -54,9 +54,10 @@ class Controller extends BaseController { use ValidatesUserGroupTrait; - protected const string CONTENT_TYPE = 'application/vnd.api+json'; + protected const string CONTENT_TYPE = 'application/vnd.api+json'; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; protected ParameterBag $parameters; - protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + protected bool $convertToNative = false; public function __construct() { @@ -81,7 +82,7 @@ class Controller extends BaseController $bag->set('limit', 50); try { - $page = (int)request()->get('page'); + $page = (int) request()->get('page'); } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { $page = 1; } @@ -92,8 +93,8 @@ class Controller extends BaseController if ($page < 1) { $page = 1; } - if ($page > (2 ^ 16)) { - $page = (2 ^ 16); + if ($page > 2 ** 16) { + $page = 2 ** 16; } $bag->set('page', $page); @@ -111,10 +112,10 @@ class Controller extends BaseController } if (null !== $date) { try { - $obj = Carbon::parse((string)$date, config('app.timezone')); + $obj = Carbon::parse((string) $date, config('app.timezone')); } catch (InvalidDateException|InvalidFormatException $e) { // don't care - app('log')->warning(sprintf('Ignored invalid date "%s" in API v2 controller parameter check: %s', substr((string)$date, 0, 20), $e->getMessage())); + app('log')->warning(sprintf('Ignored invalid date "%s" in API v2 controller parameter check: %s', substr((string) $date, 0, 20), $e->getMessage())); } // out of range? set to null. if (null !== $obj && ($obj->year <= 1900 || $obj->year > 2099)) { @@ -138,11 +139,11 @@ class Controller extends BaseController $value = null; } if (null !== $value) { - $bag->set($integer, (int)$value); + $bag->set($integer, (int) $value); } if (null === $value && 'limit' === $integer && auth()->check()) { // set default for user: - $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + $pageSize = (int) app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; $bag->set($integer, $pageSize); } } diff --git a/app/Api/V2/Controllers/JsonApi/AccountController.php b/app/Api/V2/Controllers/JsonApi/AccountController.php deleted file mode 100644 index 230e4a92cc..0000000000 --- a/app/Api/V2/Controllers/JsonApi/AccountController.php +++ /dev/null @@ -1,112 +0,0 @@ -repository() - ->queryAll() - ->withRequest($request) - ->get() - ; - - // do something custom... - - return new DataResponse($models); - } - - /** - * Fetch zero to one JSON API resource by id. - * - * @return Responsable|Response - */ - public function show(AccountSchema $schema, AccountSingleQuery $request, Account $account) - { - Log::debug(__METHOD__); - $model = $schema->repository() - ->queryOne($account) - ->withRequest($request) - ->first() - ; - Log::debug(sprintf('%s again!', __METHOD__)); - - // do something custom... - - return new DataResponse($model); - } - - // public function readAccountBalances(AnonymousQuery $query, AccountBalanceSchema $schema, Account $account): Responsable - // { - // $schema = JsonApi::server()->schemas()->schemaFor('account-balances'); - // - // $models = $schema - // ->repository() - // ->queryAll() - // ->withRequest($query) - // ->withAccount($account) - // ->get() - // ; - // - // return DataResponse::make($models); - // } -} diff --git a/app/Api/V2/Controllers/Model/Account/IndexController.php b/app/Api/V2/Controllers/Model/Account/IndexController.php index 972a211ab2..7fa619db0e 100644 --- a/app/Api/V2/Controllers/Model/Account/IndexController.php +++ b/app/Api/V2/Controllers/Model/Account/IndexController.php @@ -36,9 +36,8 @@ use Illuminate\Support\Facades\Log; class IndexController extends Controller { public const string RESOURCE_KEY = 'accounts'; - - private AccountRepositoryInterface $repository; protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS]; + private AccountRepositoryInterface $repository; /** * AccountController constructor. diff --git a/app/Api/V2/Controllers/Model/Account/ShowController.php b/app/Api/V2/Controllers/Model/Account/ShowController.php index f01efc9e72..4e7cbc218b 100644 --- a/app/Api/V2/Controllers/Model/Account/ShowController.php +++ b/app/Api/V2/Controllers/Model/Account/ShowController.php @@ -39,9 +39,8 @@ use Illuminate\Http\JsonResponse; class ShowController extends Controller { public const string RESOURCE_KEY = 'accounts'; - - private AccountRepositoryInterface $repository; protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS]; + private AccountRepositoryInterface $repository; /** * AccountController constructor. diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php b/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php new file mode 100644 index 0000000000..e46cc5375d --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php @@ -0,0 +1,67 @@ +middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse + { + $date = $request->getDate(); + $rate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null === $rate) { + throw new NotFoundHttpException(); + } + $this->repository->deleteRate($rate); + + return response()->json([], 204); + } +} diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php b/app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php new file mode 100644 index 0000000000..c01c5a5e7c --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php @@ -0,0 +1,75 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepositoryInterface; +use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; +use Illuminate\Http\JsonResponse; +use Illuminate\Pagination\LengthAwarePaginator; + +/** + * Class ShowController + */ +class IndexController extends Controller +{ + use ValidatesUserGroupTrait; + + public const string RESOURCE_KEY = 'exchange-rates'; + + private ExchangeRateRepositoryInterface $repository; + + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function index(): JsonResponse + { + $piggies = $this->repository->getAll(); + $pageSize = $this->parameters->get('limit'); + $count = $piggies->count(); + $piggies = $piggies->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + $paginator = new LengthAwarePaginator($piggies, $count, $pageSize, $this->parameters->get('page')); + + var_dump('here we are'); + + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } +} diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php b/app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php new file mode 100644 index 0000000000..5a4f40c545 --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php @@ -0,0 +1,76 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepositoryInterface; +use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; +use FireflyIII\Transformers\V2\ExchangeRateTransformer; +use Illuminate\Http\JsonResponse; +use Illuminate\Pagination\LengthAwarePaginator; + +/** + * Class ShowController + */ +class ShowController extends Controller +{ + use ValidatesUserGroupTrait; + + public const string RESOURCE_KEY = 'exchange-rates'; + + private ExchangeRateRepositoryInterface $repository; + + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function show(TransactionCurrency $from, TransactionCurrency $to): JsonResponse + { + $pageSize = $this->parameters->get('limit'); + $page = $this->parameters->get('page'); + $rates = $this->repository->getRates($from, $to); + $count = $rates->count(); + $rates = $rates->slice(($page - 1) * $pageSize, $pageSize); + $paginator = new LengthAwarePaginator($rates, $count, $pageSize, $page); + + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } +} diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php b/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php new file mode 100644 index 0000000000..73e14892a7 --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php @@ -0,0 +1,81 @@ +middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function store(StoreRequest $request): JsonResponse + { + $date = $request->getDate(); + $rate = $request->getRate(); + $from = $request->getFromCurrency(); + $to = $request->getToCurrency(); + + // already has rate? + $object = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null !== $object) { + // just update it, no matter. + $rate = $this->repository->updateExchangeRate($object, $rate, $date); + } + if (null === $object) { + // store new + $rate = $this->repository->storeExchangeRate($from, $to, $rate, $date); + } + + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } +} diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/UpdateController.php b/app/Api/V2/Controllers/Model/ExchangeRate/UpdateController.php new file mode 100644 index 0000000000..31275864a4 --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/UpdateController.php @@ -0,0 +1,69 @@ +middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function update(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse + { + $date = $request->getDate(); + $rate = $request->getRate(); + $exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date); + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } +} diff --git a/app/Api/V2/Controllers/Model/TransactionCurrency/IndexController.php b/app/Api/V2/Controllers/Model/TransactionCurrency/IndexController.php new file mode 100644 index 0000000000..a19fc67edf --- /dev/null +++ b/app/Api/V2/Controllers/Model/TransactionCurrency/IndexController.php @@ -0,0 +1,83 @@ +middleware( + function ($request, $next) { + $this->repository = app(CurrencyRepositoryInterface::class); + // new way of user group validation + $userGroup = $this->validateUserGroup($request); + $this->repository->setUserGroup($userGroup); + + return $next($request); + } + ); + } + + public function index(IndexRequest $request): JsonResponse + { + $settings = $request->getAll(); + if (true === $settings['enabled']) { + $currencies = $this->repository->get(); + } + if (true !== $settings['enabled']) { + $currencies = $this->repository->getAll(); + } + + $pageSize = $this->parameters->get('limit'); + $count = $currencies->count(); + + // depending on the sort parameters, this list must not be split, because the + // order is calculated in the account transformer and by that time it's too late. + $accounts = $currencies->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + $paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); + $transformer = new CurrencyTransformer(); + + $this->parameters->set('pageSize', $pageSize); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } +} diff --git a/app/Api/V2/Controllers/Model/TransactionCurrency/ShowController.php b/app/Api/V2/Controllers/Model/TransactionCurrency/ShowController.php new file mode 100644 index 0000000000..d33e2b2f4e --- /dev/null +++ b/app/Api/V2/Controllers/Model/TransactionCurrency/ShowController.php @@ -0,0 +1,80 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V2\Controllers\Model\TransactionCurrency; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\UserGroup; +use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; +use FireflyIII\Transformers\V2\CurrencyTransformer; +use Illuminate\Http\JsonResponse; + +/** + * Class ShowController + */ +class ShowController extends Controller +{ + public const string RESOURCE_KEY = 'transaction-currencies'; + + private CurrencyRepositoryInterface $repository; + + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(CurrencyRepositoryInterface::class); + // new way of user group validation + $userGroup = $this->validateUserGroup($request); + $this->repository->setUserGroup($userGroup); + + return $next($request); + } + ); + } + + public function show(TransactionCurrency $currency): JsonResponse + { + $groups = $currency->userGroups()->where('user_groups.id', $this->repository->getUserGroup()->id)->get(); + $enabled = $groups->count() > 0; + $default = false; + + /** @var UserGroup $group */ + foreach ($groups as $group) { + $default = 1 === $group->pivot->group_default; + } + $currency->userGroupEnabled = $enabled; + $currency->userGroupDefault = $default; + + + $transformer = new CurrencyTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $currency, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } +} diff --git a/app/Api/V2/Controllers/Summary/BasicController.php b/app/Api/V2/Controllers/Summary/BasicController.php index 3d84a3f77b..4cab036873 100644 --- a/app/Api/V2/Controllers/Summary/BasicController.php +++ b/app/Api/V2/Controllers/Summary/BasicController.php @@ -179,7 +179,7 @@ class BasicController extends Controller $return[] = [ 'key' => sprintf('bills-paid-in-%s', $info['currency_code']), 'value' => $amount, - 'currency_id' => (string)$info['currency_id'], + 'currency_id' => (string) $info['currency_id'], 'currency_code' => $info['currency_code'], 'currency_symbol' => $info['currency_symbol'], 'currency_decimal_places' => $info['currency_decimal_places'], @@ -187,7 +187,7 @@ class BasicController extends Controller $return[] = [ 'key' => 'bills-paid-in-native', 'value' => $nativeAmount, - 'currency_id' => (string)$info['native_currency_id'], + 'currency_id' => (string) $info['native_currency_id'], 'currency_code' => $info['native_currency_code'], 'currency_symbol' => $info['native_currency_symbol'], 'currency_decimal_places' => $info['native_currency_decimal_places'], @@ -203,7 +203,7 @@ class BasicController extends Controller $return[] = [ 'key' => sprintf('bills-unpaid-in-%s', $info['currency_code']), 'value' => $amount, - 'currency_id' => (string)$info['currency_id'], + 'currency_id' => (string) $info['currency_id'], 'currency_code' => $info['currency_code'], 'currency_symbol' => $info['currency_symbol'], 'currency_decimal_places' => $info['currency_decimal_places'], @@ -211,7 +211,7 @@ class BasicController extends Controller $return[] = [ 'key' => 'bills-unpaid-in-native', 'value' => $nativeAmount, - 'currency_id' => (string)$info['native_currency_id'], + 'currency_id' => (string) $info['native_currency_id'], 'currency_code' => $info['native_currency_code'], 'currency_symbol' => $info['native_currency_symbol'], 'currency_decimal_places' => $info['native_currency_decimal_places'], @@ -241,7 +241,7 @@ class BasicController extends Controller $nativeLeft = [ 'key' => 'left-to-spend-in-native', 'value' => '0', - 'currency_id' => (string)$default->id, + 'currency_id' => (string) $default->id, 'currency_code' => $default->code, 'currency_symbol' => $default->symbol, 'currency_decimal_places' => $default->decimal_places, @@ -249,7 +249,7 @@ class BasicController extends Controller $nativePerDay = [ 'key' => 'left-per-day-to-spend-in-native', 'value' => '0', - 'currency_id' => (string)$default->id, + 'currency_id' => (string) $default->id, 'currency_code' => $default->code, 'currency_symbol' => $default->symbol, 'currency_decimal_places' => $default->decimal_places, @@ -276,7 +276,7 @@ class BasicController extends Controller $currencies[$currencyId] = $currency; $amount = app('steam')->negative($journal['amount']); $amountNative = $converter->convert($default, $currency, $start, $amount); - if ((int)$journal['foreign_currency_id'] === $default->id) { + if ((int) $journal['foreign_currency_id'] === $default->id) { $amountNative = $journal['foreign_amount']; } $spent = bcadd($spent, $amount); @@ -296,24 +296,24 @@ class BasicController extends Controller app('log')->debug(sprintf('Amount left is %s', $left)); // how much left per day? - $days = (int)$today->diffInDays($end, true) + 1; + $days = (int) $today->diffInDays($end, true) + 1; $perDay = '0'; $perDayNative = '0'; if (0 !== $days && bccomp($left, '0') > -1) { - $perDay = bcdiv($left, (string)$days); + $perDay = bcdiv($left, (string) $days); } if (0 !== $days && bccomp($leftNative, '0') > -1) { - $perDayNative = bcdiv($leftNative, (string)$days); + $perDayNative = bcdiv($leftNative, (string) $days); } // left $return[] = [ 'key' => sprintf('left-to-spend-in-%s', $row['currency_code']), 'value' => $left, - 'currency_id' => (string)$row['currency_id'], + 'currency_id' => (string) $row['currency_id'], 'currency_code' => $row['currency_code'], 'currency_symbol' => $row['currency_symbol'], - 'currency_decimal_places' => (int)$row['currency_decimal_places'], + 'currency_decimal_places' => (int) $row['currency_decimal_places'], ]; // left (native) $nativeLeft['value'] = $leftNative; @@ -322,10 +322,10 @@ class BasicController extends Controller $return[] = [ 'key' => sprintf('left-per-day-to-spend-in-%s', $row['currency_code']), 'value' => $perDay, - 'currency_id' => (string)$row['currency_id'], + 'currency_id' => (string) $row['currency_id'], 'currency_code' => $row['currency_code'], 'currency_symbol' => $row['currency_symbol'], - 'currency_decimal_places' => (int)$row['currency_decimal_places'], + 'currency_decimal_places' => (int) $row['currency_decimal_places'], ]; // left per day (native) @@ -371,7 +371,7 @@ class BasicController extends Controller $return[] = [ 'key' => 'net-worth-in-native', 'value' => $netWorthSet['native']['balance'], - 'currency_id' => (string)$netWorthSet['native']['currency_id'], + 'currency_id' => (string) $netWorthSet['native']['currency_id'], 'currency_code' => $netWorthSet['native']['currency_code'], 'currency_symbol' => $netWorthSet['native']['currency_symbol'], 'currency_decimal_places' => $netWorthSet['native']['currency_decimal_places'], @@ -383,7 +383,7 @@ class BasicController extends Controller $return[] = [ 'key' => sprintf('net-worth-in-%s', $data['currency_code']), 'value' => $data['balance'], - 'currency_id' => (string)$data['currency_id'], + 'currency_id' => (string) $data['currency_id'], 'currency_code' => $data['currency_code'], 'currency_symbol' => $data['currency_symbol'], 'currency_decimal_places' => $data['currency_decimal_places'], diff --git a/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php b/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php index 3c933c5b46..958cf846e3 100644 --- a/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php +++ b/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php @@ -73,6 +73,16 @@ class AutocompleteRequest extends FormRequest return $array; } + private function getAccountTypeParameter(array $types): array + { + $return = []; + foreach ($types as $type) { + $return = array_merge($return, $this->mapAccountTypes($type)); + } + + return array_unique($return); + } + public function rules(): array { $valid = array_keys($this->types); @@ -86,14 +96,4 @@ class AutocompleteRequest extends FormRequest 'transaction_types' => 'nullable|in:todo', ]; } - - private function getAccountTypeParameter(array $types): array - { - $return = []; - foreach ($types as $type) { - $return = array_merge($return, $this->mapAccountTypes($type)); - } - - return array_unique($return); - } } diff --git a/app/Api/V2/Request/Chart/BalanceChartRequest.php b/app/Api/V2/Request/Chart/BalanceChartRequest.php index 37322c1ea3..3f60442a89 100644 --- a/app/Api/V2/Request/Chart/BalanceChartRequest.php +++ b/app/Api/V2/Request/Chart/BalanceChartRequest.php @@ -40,7 +40,8 @@ class BalanceChartRequest extends FormRequest use ChecksLogin; use ConvertsDataTypes; use ValidatesUserGroupTrait; - protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; /** * Get all data from the request. diff --git a/app/JsonApi/V2/AccountBalances/AccountBalanceRepository.php b/app/Api/V2/Request/Model/ExchangeRate/DestroyRequest.php similarity index 57% rename from app/JsonApi/V2/AccountBalances/AccountBalanceRepository.php rename to app/Api/V2/Request/Model/ExchangeRate/DestroyRequest.php index 38aa5bb460..d7e0ce45f3 100644 --- a/app/JsonApi/V2/AccountBalances/AccountBalanceRepository.php +++ b/app/Api/V2/Request/Model/ExchangeRate/DestroyRequest.php @@ -1,7 +1,7 @@ getCarbonDate('date'); } - public function queryAll(): Capabilities\AccountBalanceQuery + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array { - return Capabilities\AccountBalanceQuery::make() - ->withServer($this->server) - ->withSchema($this->schema) - ; + return [ + 'date' => 'required|date|after:1900-01-01|before:2099-12-31', + ]; } } diff --git a/app/Api/V2/Request/Model/ExchangeRate/StoreRequest.php b/app/Api/V2/Request/Model/ExchangeRate/StoreRequest.php new file mode 100644 index 0000000000..89f9e8f381 --- /dev/null +++ b/app/Api/V2/Request/Model/ExchangeRate/StoreRequest.php @@ -0,0 +1,70 @@ +getCarbonDate('date'); + } + + public function getRate(): string + { + return (string) $this->get('rate'); + } + + public function getFromCurrency(): TransactionCurrency + { + return TransactionCurrency::where('code', $this->get('from'))->first(); + } + + public function getToCurrency(): TransactionCurrency + { + return TransactionCurrency::where('code', $this->get('to'))->first(); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + 'date' => 'required|date|after:1900-01-01|before:2099-12-31', + 'rate' => 'required|numeric|gt:0', + 'from' => 'required|exists:transaction_currencies,code', + 'to' => 'required|exists:transaction_currencies,code', + ]; + } +} diff --git a/app/JsonApi/V2/AccountBalances/Capabilities/AccountBalanceQuery.php b/app/Api/V2/Request/Model/ExchangeRate/UpdateRequest.php similarity index 56% rename from app/JsonApi/V2/AccountBalances/Capabilities/AccountBalanceQuery.php rename to app/Api/V2/Request/Model/ExchangeRate/UpdateRequest.php index 6f4b6fd581..d55489421f 100644 --- a/app/JsonApi/V2/AccountBalances/Capabilities/AccountBalanceQuery.php +++ b/app/Api/V2/Request/Model/ExchangeRate/UpdateRequest.php @@ -1,7 +1,7 @@ getCarbonDate('date'); + } + + public function getRate(): string + { + return (string) $this->get('rate'); + } /** - * QuerySites constructor. + * The rules that the incoming request must be matched against. */ - public function __construct() - { - parent::__construct(); - } - - public function get(): iterable + public function rules(): array { return [ - AccountBalance::fromArray(), - AccountBalance::fromArray(), - AccountBalance::fromArray(), - AccountBalance::fromArray(), + 'date' => 'date|after:1900-01-01|before:2099-12-31', + 'rate' => 'required|numeric|gt:0', ]; } - - public function withAccount(Account $account): self - { - $this->account = $account; - - return $this; - } } diff --git a/app/Api/V2/Request/Model/Transaction/InfiniteListRequest.php b/app/Api/V2/Request/Model/Transaction/InfiniteListRequest.php index f012ade56a..51c8b0460c 100644 --- a/app/Api/V2/Request/Model/Transaction/InfiniteListRequest.php +++ b/app/Api/V2/Request/Model/Transaction/InfiniteListRequest.php @@ -87,7 +87,7 @@ class InfiniteListRequest extends FormRequest public function getAccountTypes(): array { - $type = (string)$this->get('type', 'default'); + $type = (string) $this->get('type', 'default'); return $this->mapAccountTypes($type); } @@ -101,7 +101,7 @@ class InfiniteListRequest extends FormRequest public function getTransactionTypes(): array { - $type = (string)$this->get('type', 'default'); + $type = (string) $this->get('type', 'default'); return $this->mapTransactionTypes($type); } diff --git a/app/Api/V2/Request/Model/Transaction/ListRequest.php b/app/Api/V2/Request/Model/Transaction/ListRequest.php index 45286d5cbf..3d72d70a00 100644 --- a/app/Api/V2/Request/Model/Transaction/ListRequest.php +++ b/app/Api/V2/Request/Model/Transaction/ListRequest.php @@ -76,7 +76,7 @@ class ListRequest extends FormRequest public function getTransactionTypes(): array { - $type = (string)$this->get('type', 'default'); + $type = (string) $this->get('type', 'default'); return $this->mapTransactionTypes($type); } diff --git a/app/Api/V2/Request/Model/Transaction/StoreRequest.php b/app/Api/V2/Request/Model/Transaction/StoreRequest.php index fcb4d7e206..542fc71f8b 100644 --- a/app/Api/V2/Request/Model/Transaction/StoreRequest.php +++ b/app/Api/V2/Request/Model/Transaction/StoreRequest.php @@ -94,73 +94,73 @@ class StoreRequest extends FormRequest $result = [ 'type' => $this->clearString($object['type']), 'date' => $this->dateFromValue($object['date']), - 'order' => $this->integerFromValue((string)$object['order']), + 'order' => $this->integerFromValue((string) $object['order']), - 'currency_id' => $this->integerFromValue((string)$object['currency_id']), - 'currency_code' => $this->clearString((string)$object['currency_code']), + 'currency_id' => $this->integerFromValue((string) $object['currency_id']), + 'currency_code' => $this->clearString((string) $object['currency_code']), // foreign currency info: - 'foreign_currency_id' => $this->integerFromValue((string)$object['foreign_currency_id']), - 'foreign_currency_code' => $this->clearString((string)$object['foreign_currency_code']), + 'foreign_currency_id' => $this->integerFromValue((string) $object['foreign_currency_id']), + 'foreign_currency_code' => $this->clearString((string) $object['foreign_currency_code']), // amount and foreign amount. Cannot be 0. - 'amount' => $this->clearString((string)$object['amount']), - 'foreign_amount' => $this->clearString((string)$object['foreign_amount']), + 'amount' => $this->clearString((string) $object['amount']), + 'foreign_amount' => $this->clearString((string) $object['foreign_amount']), // description. 'description' => $this->clearString($object['description']), // source of transaction. If everything is null, assume cash account. - 'source_id' => $this->integerFromValue((string)$object['source_id']), - 'source_name' => $this->clearString((string)$object['source_name']), - 'source_iban' => $this->clearString((string)$object['source_iban']), - 'source_number' => $this->clearString((string)$object['source_number']), - 'source_bic' => $this->clearString((string)$object['source_bic']), + 'source_id' => $this->integerFromValue((string) $object['source_id']), + 'source_name' => $this->clearString((string) $object['source_name']), + 'source_iban' => $this->clearString((string) $object['source_iban']), + 'source_number' => $this->clearString((string) $object['source_number']), + 'source_bic' => $this->clearString((string) $object['source_bic']), // destination of transaction. If everything is null, assume cash account. - 'destination_id' => $this->integerFromValue((string)$object['destination_id']), - 'destination_name' => $this->clearString((string)$object['destination_name']), - 'destination_iban' => $this->clearString((string)$object['destination_iban']), - 'destination_number' => $this->clearString((string)$object['destination_number']), - 'destination_bic' => $this->clearString((string)$object['destination_bic']), + 'destination_id' => $this->integerFromValue((string) $object['destination_id']), + 'destination_name' => $this->clearString((string) $object['destination_name']), + 'destination_iban' => $this->clearString((string) $object['destination_iban']), + 'destination_number' => $this->clearString((string) $object['destination_number']), + 'destination_bic' => $this->clearString((string) $object['destination_bic']), // budget info - 'budget_id' => $this->integerFromValue((string)$object['budget_id']), - 'budget_name' => $this->clearString((string)$object['budget_name']), + 'budget_id' => $this->integerFromValue((string) $object['budget_id']), + 'budget_name' => $this->clearString((string) $object['budget_name']), // category info - 'category_id' => $this->integerFromValue((string)$object['category_id']), - 'category_name' => $this->clearString((string)$object['category_name']), + 'category_id' => $this->integerFromValue((string) $object['category_id']), + 'category_name' => $this->clearString((string) $object['category_name']), // journal bill reference. Optional. Will only work for withdrawals - 'bill_id' => $this->integerFromValue((string)$object['bill_id']), - 'bill_name' => $this->clearString((string)$object['bill_name']), + 'bill_id' => $this->integerFromValue((string) $object['bill_id']), + 'bill_name' => $this->clearString((string) $object['bill_name']), // piggy bank reference. Optional. Will only work for transfers - 'piggy_bank_id' => $this->integerFromValue((string)$object['piggy_bank_id']), - 'piggy_bank_name' => $this->clearString((string)$object['piggy_bank_name']), + 'piggy_bank_id' => $this->integerFromValue((string) $object['piggy_bank_id']), + 'piggy_bank_name' => $this->clearString((string) $object['piggy_bank_name']), // some other interesting properties - 'reconciled' => $this->convertBoolean((string)$object['reconciled']), - 'notes' => $this->clearStringKeepNewlines((string)$object['notes']), + 'reconciled' => $this->convertBoolean((string) $object['reconciled']), + 'notes' => $this->clearStringKeepNewlines((string) $object['notes']), 'tags' => $this->arrayFromValue($object['tags']), // all custom fields: - 'internal_reference' => $this->clearString((string)$object['internal_reference']), - 'external_id' => $this->clearString((string)$object['external_id']), + 'internal_reference' => $this->clearString((string) $object['internal_reference']), + 'external_id' => $this->clearString((string) $object['external_id']), 'original_source' => sprintf('ff3-v%s', config('firefly.version')), 'recurrence_id' => $this->integerFromValue($object['recurrence_id']), - 'bunq_payment_id' => $this->clearString((string)$object['bunq_payment_id']), - 'external_url' => $this->clearString((string)$object['external_url']), + 'bunq_payment_id' => $this->clearString((string) $object['bunq_payment_id']), + 'external_url' => $this->clearString((string) $object['external_url']), - 'sepa_cc' => $this->clearString((string)$object['sepa_cc']), - 'sepa_ct_op' => $this->clearString((string)$object['sepa_ct_op']), - 'sepa_ct_id' => $this->clearString((string)$object['sepa_ct_id']), - 'sepa_db' => $this->clearString((string)$object['sepa_db']), - 'sepa_country' => $this->clearString((string)$object['sepa_country']), - 'sepa_ep' => $this->clearString((string)$object['sepa_ep']), - 'sepa_ci' => $this->clearString((string)$object['sepa_ci']), - 'sepa_batch_id' => $this->clearString((string)$object['sepa_batch_id']), + 'sepa_cc' => $this->clearString((string) $object['sepa_cc']), + 'sepa_ct_op' => $this->clearString((string) $object['sepa_ct_op']), + 'sepa_ct_id' => $this->clearString((string) $object['sepa_ct_id']), + 'sepa_db' => $this->clearString((string) $object['sepa_db']), + 'sepa_country' => $this->clearString((string) $object['sepa_country']), + 'sepa_ep' => $this->clearString((string) $object['sepa_ep']), + 'sepa_ci' => $this->clearString((string) $object['sepa_ci']), + 'sepa_batch_id' => $this->clearString((string) $object['sepa_batch_id']), // custom date fields. Must be Carbon objects. Presence is optional. 'interest_date' => $this->dateFromValue($object['interest_date']), 'book_date' => $this->dateFromValue($object['book_date']), diff --git a/app/Api/V2/Request/Model/Transaction/UpdateRequest.php b/app/Api/V2/Request/Model/Transaction/UpdateRequest.php index 98211ca0b4..ce5d6a95d3 100644 --- a/app/Api/V2/Request/Model/Transaction/UpdateRequest.php +++ b/app/Api/V2/Request/Model/Transaction/UpdateRequest.php @@ -139,7 +139,7 @@ class UpdateRequest extends Request { foreach ($this->integerFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->integerFromValue((string)$transaction[$fieldName]); + $current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]); } } @@ -154,7 +154,7 @@ class UpdateRequest extends Request { foreach ($this->stringFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->clearString((string)$transaction[$fieldName]); + $current[$fieldName] = $this->clearString((string) $transaction[$fieldName]); } } @@ -169,7 +169,7 @@ class UpdateRequest extends Request { foreach ($this->textareaFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->clearStringKeepNewlines((string)$transaction[$fieldName]); // keep newlines + $current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines } } @@ -185,8 +185,8 @@ class UpdateRequest extends Request foreach ($this->dateFields as $fieldName) { app('log')->debug(sprintf('Now at date field %s', $fieldName)); if (array_key_exists($fieldName, $transaction)) { - app('log')->debug(sprintf('New value: "%s"', (string)$transaction[$fieldName])); - $current[$fieldName] = $this->dateFromValue((string)$transaction[$fieldName]); + app('log')->debug(sprintf('New value: "%s"', (string) $transaction[$fieldName])); + $current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]); } } @@ -201,7 +201,7 @@ class UpdateRequest extends Request { foreach ($this->booleanFields as $fieldName) { if (array_key_exists($fieldName, $transaction)) { - $current[$fieldName] = $this->convertBoolean((string)$transaction[$fieldName]); + $current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]); } } @@ -236,7 +236,7 @@ class UpdateRequest extends Request $current[$fieldName] = sprintf('%.12f', $value); } if (!is_float($value)) { - $current[$fieldName] = (string)$value; + $current[$fieldName] = (string) $value; } } } diff --git a/app/Api/V2/Request/Model/TransactionCurrency/IndexRequest.php b/app/Api/V2/Request/Model/TransactionCurrency/IndexRequest.php new file mode 100644 index 0000000000..426ccdb415 --- /dev/null +++ b/app/Api/V2/Request/Model/TransactionCurrency/IndexRequest.php @@ -0,0 +1,64 @@ + $this->convertBoolean($this->get('enabled')), + ]; + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + 'enabled' => 'nullable|boolean', + ]; + + } +} diff --git a/app/Api/V2/Response/Sum/AutoSum.php b/app/Api/V2/Response/Sum/AutoSum.php index 6ed1faa73e..c4ca249fbb 100644 --- a/app/Api/V2/Response/Sum/AutoSum.php +++ b/app/Api/V2/Response/Sum/AutoSum.php @@ -52,7 +52,7 @@ class AutoSum $amount = $getSum($object); $return[$currency->id] ??= [ - 'id' => (string)$currency->id, + 'id' => (string) $currency->id, 'name' => $currency->name, 'symbol' => $currency->symbol, 'code' => $currency->code, diff --git a/app/Console/Commands/Integrity/ConvertDatesToUTC.php b/app/Console/Commands/Correction/ConvertsDatesToUTC.php similarity index 76% rename from app/Console/Commands/Integrity/ConvertDatesToUTC.php rename to app/Console/Commands/Correction/ConvertsDatesToUTC.php index 6ca496f8b3..9d5c309f53 100644 --- a/app/Console/Commands/Integrity/ConvertDatesToUTC.php +++ b/app/Console/Commands/Correction/ConvertsDatesToUTC.php @@ -1,5 +1,25 @@ friendlyWarning('Please do not use this command.'); + + return 0; + /** * @var string $model * @var array $fields diff --git a/app/Console/Commands/Correction/CorrectAmounts.php b/app/Console/Commands/Correction/CorrectAmounts.php deleted file mode 100644 index ad30463120..0000000000 --- a/app/Console/Commands/Correction/CorrectAmounts.php +++ /dev/null @@ -1,253 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace FireflyIII\Console\Commands\Correction; - -use FireflyIII\Console\Commands\ShowsFriendlyMessages; -use FireflyIII\Models\AutoBudget; -use FireflyIII\Models\AvailableBudget; -use FireflyIII\Models\Bill; -use FireflyIII\Models\BudgetLimit; -use FireflyIII\Models\CurrencyExchangeRate; -use FireflyIII\Models\PiggyBank; -use FireflyIII\Models\PiggyBankRepetition; -use FireflyIII\Models\RecurrenceTransaction; -use FireflyIII\Models\RuleTrigger; -use Illuminate\Console\Command; - -/** - * Class ReportSkeleton - */ -class CorrectAmounts extends Command -{ - use ShowsFriendlyMessages; - - protected $description = 'This command makes sure positive and negative amounts are recorded correctly.'; - protected $signature = 'firefly-iii:fix-amount-pos-neg'; - - public function handle(): int - { - // auto budgets must be positive - $this->fixAutoBudgets(); - // available budgets must be positive - $this->fixAvailableBudgets(); - // bills must be positive (both amounts) - $this->fixBills(); - // budget limits must be positive - $this->fixBudgetLimits(); - // currency_exchange_rates must be positive - $this->fixExchangeRates(); - // piggy_bank_repetitions must be positive - $this->fixRepetitions(); - // piggy_banks must be positive - $this->fixPiggyBanks(); - // recurrences_transactions amount must be positive - $this->fixRecurrences(); - // rule_triggers must be positive or zero (amount_less, amount_more, amount_is) - $this->fixRuleTriggers(); - - return 0; - } - - private function fixAutoBudgets(): void - { - $set = AutoBudget::where('amount', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All auto budget amounts are positive.'); - - return; - } - - /** @var AutoBudget $item */ - foreach ($set as $item) { - $item->amount = app('steam')->positive($item->amount); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d auto budget amount(s).', $count)); - } - - private function fixAvailableBudgets(): void - { - $set = AvailableBudget::where('amount', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All available budget amounts are positive.'); - - return; - } - - /** @var AvailableBudget $item */ - foreach ($set as $item) { - $item->amount = app('steam')->positive($item->amount); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d available budget amount(s).', $count)); - } - - private function fixBills(): void - { - $set = Bill::where('amount_min', '<', 0)->orWhere('amount_max', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All bill amounts are positive.'); - - return; - } - - /** @var Bill $item */ - foreach ($set as $item) { - $item->amount_min = app('steam')->positive($item->amount_min); - $item->amount_max = app('steam')->positive($item->amount_max); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d bill amount(s).', $count)); - } - - private function fixBudgetLimits(): void - { - $set = BudgetLimit::where('amount', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All budget limit amounts are positive.'); - - return; - } - - /** @var BudgetLimit $item */ - foreach ($set as $item) { - $item->amount = app('steam')->positive($item->amount); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d budget limit amount(s).', $count)); - } - - private function fixExchangeRates(): void - { - $set = CurrencyExchangeRate::where('rate', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All currency exchange rates are positive.'); - - return; - } - - /** @var CurrencyExchangeRate $item */ - foreach ($set as $item) { - $item->rate = app('steam')->positive($item->rate); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d currency exchange rate(s).', $count)); - } - - private function fixRepetitions(): void - { - $set = PiggyBankRepetition::where('currentamount', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All piggy bank repetition amounts are positive.'); - - return; - } - - /** @var PiggyBankRepetition $item */ - foreach ($set as $item) { - $item->currentamount = app('steam')->positive($item->currentamount); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d piggy bank repetition amount(s).', $count)); - } - - private function fixPiggyBanks(): void - { - $set = PiggyBank::where('targetamount', '<', 0)->get(); - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All piggy bank amounts are positive.'); - - return; - } - - /** @var PiggyBank $item */ - foreach ($set as $item) { - $item->targetamount = app('steam')->positive($item->targetamount); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d piggy bank amount(s).', $count)); - } - - private function fixRecurrences(): void - { - $set = RecurrenceTransaction::where('amount', '<', 0) - ->orWhere('foreign_amount', '<', 0) - ->get() - ; - $count = $set->count(); - if (0 === $count) { - $this->friendlyPositive('All recurring transaction amounts are positive.'); - - return; - } - - /** @var RecurrenceTransaction $item */ - foreach ($set as $item) { - $item->amount = app('steam')->positive($item->amount); - $item->foreign_amount = app('steam')->positive($item->foreign_amount); - $item->save(); - } - $this->friendlyInfo(sprintf('Corrected %d recurring transaction amount(s).', $count)); - } - - private function fixRuleTriggers(): void - { - $set = RuleTrigger::whereIn('trigger_type', ['amount_less', 'amount_more', 'amount_is'])->get(); - $fixed = 0; - - /** @var RuleTrigger $item */ - foreach ($set as $item) { - // basic check: - $check = 0; - - try { - $check = bccomp((string)$item->trigger_value, '0'); - } catch (\ValueError $e) { - $this->friendlyError(sprintf('Rule #%d contained invalid %s-trigger "%s". The trigger has been removed, and the rule is disabled.', $item->rule_id, $item->trigger_type, $item->trigger_value)); - $item->rule->active = false; - $item->rule->save(); - $item->forceDelete(); - } - if (-1 === $check) { - ++$fixed; - $item->trigger_value = app('steam')->positive($item->trigger_value); - $item->save(); - } - } - if (0 === $fixed) { - $this->friendlyPositive('All rule trigger amounts are positive.'); - - return; - } - $this->friendlyInfo(sprintf('Corrected %d rule trigger amount(s).', $fixed)); - } -} diff --git a/app/Console/Commands/Correction/CorrectionSkeleton.php.stub b/app/Console/Commands/Correction/CorrectionSkeleton.php.stub index 801dd0d845..400a630f8f 100644 --- a/app/Console/Commands/Correction/CorrectionSkeleton.php.stub +++ b/app/Console/Commands/Correction/CorrectionSkeleton.php.stub @@ -4,12 +4,9 @@ namespace FireflyIII\Console\Commands\Correction; use Illuminate\Console\Command; -/** - * Class CorrectionSkeleton - * TODO DONT FORGET TO ADD THIS TO THE DOCKER BUILD - */ class CorrectionSkeleton extends Command { + use ShowsFriendlyMessages; protected $description = 'DESCRIPTION HERE'; protected $signature = 'firefly-iii:CORR_COMMAND'; diff --git a/app/Console/Commands/Correction/FixAccountOrder.php b/app/Console/Commands/Correction/CorrectsAccountOrder.php similarity index 90% rename from app/Console/Commands/Correction/FixAccountOrder.php rename to app/Console/Commands/Correction/CorrectsAccountOrder.php index 261ba7ad05..af3d8d089f 100644 --- a/app/Console/Commands/Correction/FixAccountOrder.php +++ b/app/Console/Commands/Correction/CorrectsAccountOrder.php @@ -29,15 +29,12 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class FixAccountOrder - */ -class FixAccountOrder extends Command +class CorrectsAccountOrder extends Command { use ShowsFriendlyMessages; protected $description = 'Make sure account order is correct.'; - protected $signature = 'firefly-iii:fix-account-order'; + protected $signature = 'correction:account-order'; private AccountRepositoryInterface $repository; @@ -54,8 +51,6 @@ class FixAccountOrder extends Command $this->repository->resetAccountOrder(); } - $this->friendlyPositive('All accounts are ordered correctly'); - return 0; } diff --git a/app/Console/Commands/Correction/FixAccountTypes.php b/app/Console/Commands/Correction/CorrectsAccountTypes.php similarity index 98% rename from app/Console/Commands/Correction/FixAccountTypes.php rename to app/Console/Commands/Correction/CorrectsAccountTypes.php index 745fdf9819..75be735063 100644 --- a/app/Console/Commands/Correction/FixAccountTypes.php +++ b/app/Console/Commands/Correction/CorrectsAccountTypes.php @@ -36,15 +36,12 @@ use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\JoinClause; -/** - * Class FixAccountTypes - */ -class FixAccountTypes extends Command +class CorrectsAccountTypes extends Command { use ShowsFriendlyMessages; protected $description = 'Make sure all journals have the correct from/to account types.'; - protected $signature = 'firefly-iii:fix-account-types'; + protected $signature = 'correction:account-types'; private int $count; private array $expected; private AccountFactory $factory; @@ -120,9 +117,6 @@ class FixAccountTypes extends Command } } } - if (0 === $this->count) { - $this->friendlyPositive('All account types are OK'); - } if (0 !== $this->count) { app('log')->debug(sprintf('%d journals had to be fixed.', $this->count)); $this->friendlyInfo(sprintf('Acted on %d transaction(s)', $this->count)); diff --git a/app/Console/Commands/Correction/CorrectsAmounts.php b/app/Console/Commands/Correction/CorrectsAmounts.php new file mode 100644 index 0000000000..a1d29d471c --- /dev/null +++ b/app/Console/Commands/Correction/CorrectsAmounts.php @@ -0,0 +1,185 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Console\Commands\ShowsFriendlyMessages; +use FireflyIII\Models\AutoBudget; +use FireflyIII\Models\AvailableBudget; +use FireflyIII\Models\Bill; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\RecurrenceTransaction; +use FireflyIII\Models\RuleTrigger; +use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; + +class CorrectsAmounts extends Command +{ + use ShowsFriendlyMessages; + + protected $description = 'This command makes sure positive and negative amounts are recorded correctly.'; + protected $signature = 'correction:amounts'; + + public function handle(): int + { + // auto budgets must be positive + $this->fixAutoBudgets(); + // available budgets must be positive + $this->fixAvailableBudgets(); + // bills must be positive (both amounts) + $this->fixBills(); + // budget limits must be positive + $this->fixBudgetLimits(); + // currency_exchange_rates must be positive + $this->fixExchangeRates(); + // piggy_banks must be positive + $this->fixPiggyBanks(); + // recurrences_transactions amount must be positive + $this->fixRecurrences(); + // rule_triggers must be positive or zero (amount_less, amount_more, amount_is) + $this->fixRuleTriggers(); + + return 0; + } + + private function fixAutoBudgets(): void + { + $count = AutoBudget::where('amount', '<', 0)->update(['amount' => DB::raw('amount * -1')]); + if (0 === $count) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d auto budget amount(s).', $count)); + } + + private function fixAvailableBudgets(): void + { + $count = AvailableBudget::where('amount', '<', 0)->update(['amount' => DB::raw('amount * -1')]); + if (0 === $count) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d available budget amount(s).', $count)); + } + + private function fixBills(): void + { + $count = 0; + $count += Bill::where('amount_max', '<', 0)->update(['amount_max' => DB::raw('amount_max * -1')]); + $count += Bill::where('amount_min', '<', 0)->update(['amount_min' => DB::raw('amount_min * -1')]); + if (0 === $count) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d bill amount(s).', $count)); + } + + private function fixBudgetLimits(): void + { + $count = BudgetLimit::where('amount', '<', 0)->update(['amount' => DB::raw('amount * -1')]); + if (0 === $count) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d budget limit amount(s).', $count)); + } + + private function fixExchangeRates(): void + { + $count = CurrencyExchangeRate::where('rate', '<', 0)->update(['rate' => DB::raw('rate * -1')]); + if (0 === $count) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d currency exchange rate(s).', $count)); + } + + private function fixPiggyBanks(): void + { + $count = PiggyBank::where('target_amount', '<', 0)->update(['target_amount' => DB::raw('target_amount * -1')]); + if (0 === $count) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d piggy bank amount(s).', $count)); + } + + private function fixRecurrences(): void + { + $count = 0; + $count += RecurrenceTransaction::where('amount', '<', 0)->update(['amount' => DB::raw('amount * -1')]); + $count += RecurrenceTransaction::where('foreign_amount', '<', 0)->update(['foreign_amount' => DB::raw('foreign_amount * -1')]); + if (0 === $count) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d recurring transaction amount(s).', $count)); + } + + /** + * Foreach loop is unavoidable here. + */ + private function fixRuleTriggers(): void + { + $set = RuleTrigger::whereIn('trigger_type', ['amount_less', 'amount_more', 'amount_is'])->get(); + $fixed = 0; + + /** @var RuleTrigger $item */ + foreach ($set as $item) { + $result = $this->fixRuleTrigger($item); + if (true === $result) { + ++$fixed; + } + } + if (0 === $fixed) { + + return; + } + $this->friendlyInfo(sprintf('Corrected %d rule trigger amount(s).', $fixed)); + } + + private function fixRuleTrigger(RuleTrigger $item): bool + { + try { + $check = bccomp((string) $item->trigger_value, '0'); + } catch (\ValueError $e) { + $this->friendlyError(sprintf('Rule #%d contained invalid %s-trigger "%s". The trigger has been removed, and the rule is disabled.', $item->rule_id, $item->trigger_type, $item->trigger_value)); + $item->rule->active = false; + $item->rule->save(); + $item->forceDelete(); + + return false; + } + if (-1 === $check) { + $item->trigger_value = app('steam')->positive($item->trigger_value); + $item->save(); + + return true; + } + + return false; + } +} diff --git a/app/Console/Commands/Correction/EnableCurrencies.php b/app/Console/Commands/Correction/CorrectsCurrencies.php similarity index 93% rename from app/Console/Commands/Correction/EnableCurrencies.php rename to app/Console/Commands/Correction/CorrectsCurrencies.php index 3f7386f625..dc812457ea 100644 --- a/app/Console/Commands/Correction/EnableCurrencies.php +++ b/app/Console/Commands/Correction/CorrectsCurrencies.php @@ -37,15 +37,12 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use Symfony\Component\Console\Command\Command as CommandAlias; -/** - * Class EnableCurrencies - */ -class EnableCurrencies extends Command +class CorrectsCurrencies extends Command { use ShowsFriendlyMessages; protected $description = 'Enables all currencies in use.'; - protected $signature = 'firefly-iii:enable-currencies'; + protected $signature = 'correction:currencies'; /** * Execute the console command. @@ -77,7 +74,7 @@ class EnableCurrencies extends Command ->where('account_meta.name', 'currency_id')->groupBy('data')->get(['data']) ; foreach ($meta as $entry) { - $found[] = (int)$entry->data; + $found[] = (int) $entry->data; } // get all from journals: @@ -85,7 +82,7 @@ class EnableCurrencies extends Command ->groupBy('transaction_currency_id')->get(['transaction_currency_id']) ; foreach ($journals as $entry) { - $found[] = (int)$entry->transaction_currency_id; + $found[] = (int) $entry->transaction_currency_id; } // get all from transactions @@ -95,8 +92,8 @@ class EnableCurrencies extends Command ->get(['transactions.transaction_currency_id', 'transactions.foreign_currency_id']) ; foreach ($transactions as $entry) { - $found[] = (int)$entry->transaction_currency_id; - $found[] = (int)$entry->foreign_currency_id; + $found[] = (int) $entry->transaction_currency_id; + $found[] = (int) $entry->foreign_currency_id; } // get all from budget limits diff --git a/app/Console/Commands/Correction/CorrectDatabase.php b/app/Console/Commands/Correction/CorrectsDatabase.php similarity index 52% rename from app/Console/Commands/Correction/CorrectDatabase.php rename to app/Console/Commands/Correction/CorrectsDatabase.php index 35a6a16dea..1d9ea62112 100644 --- a/app/Console/Commands/Correction/CorrectDatabase.php +++ b/app/Console/Commands/Correction/CorrectsDatabase.php @@ -27,14 +27,11 @@ namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; -/** - * Class CorrectDatabase - */ -class CorrectDatabase extends Command +class CorrectsDatabase extends Command { use ShowsFriendlyMessages; - protected $description = 'Will correct the integrity of your database, if necessary.'; + protected $description = 'Will validate and correct the integrity of your database, if necessary.'; protected $signature = 'firefly-iii:correct-database'; /** @@ -49,32 +46,36 @@ class CorrectDatabase extends Command return 1; } $commands = [ - 'firefly-iii:fix-piggies', - 'firefly-iii:create-link-types', - 'firefly-iii:create-access-tokens', - 'firefly-iii:remove-bills', - 'firefly-iii:fix-amount-pos-neg', - 'firefly-iii:enable-currencies', - 'firefly-iii:fix-transfer-budgets', - 'firefly-iii:fix-uneven-amount', - 'firefly-iii:delete-zero-amount', - 'firefly-iii:delete-orphaned-transactions', - 'firefly-iii:delete-empty-journals', - 'firefly-iii:delete-empty-groups', - 'firefly-iii:fix-account-types', - 'firefly-iii:fix-ibans', - 'firefly-iii:fix-account-order', - 'firefly-iii:rename-meta-fields', - 'firefly-iii:fix-ob-currencies', - 'firefly-iii:fix-long-descriptions', - 'firefly-iii:fix-recurring-transactions', - 'firefly-iii:upgrade-group-information', - 'firefly-iii:fix-transaction-types', - 'firefly-iii:fix-frontpage-accounts', - // new! - 'firefly-iii:unify-group-accounts', - 'firefly-iii:trigger-credit-recalculation', - 'firefly-iii:migrate-preferences', + 'correction:restore-oauth-keys', + 'correction:timezones', + 'correction:create-group-memberships', + 'correction:group-information', + 'correction:piggy-banks', + 'correction:link-types', + 'correction:access-tokens', + 'correction:bills', + 'correction:amounts', + 'correction:currencies', + 'correction:transfer-budgets', + 'correction:uneven-amounts', + 'correction:zero-amounts', + 'correction:orphaned-transactions', + 'correction:empty-journals', + 'correction:empty-groups', + 'correction:account-types', + 'correction:ibans', + 'correction:account-order', + 'correction:meta-fields', + 'correction:opening-balance-currencies', + 'correction:long-descriptions', + 'correction:recurring-transactions', + 'correction:frontpage-accounts', + 'correction:group-accounts', + 'correction:recalculates-liabilities', + 'correction:preferences', + // 'correction:transaction-types', // resource heavy, disabled. + 'correction:recalculate-native-amounts', // not necessary, disabled. + 'firefly-iii:report-integrity', ]; foreach ($commands as $command) { $this->friendlyLine(sprintf('Now executing command "%s"', $command)); diff --git a/app/Console/Commands/Correction/FixFrontpageAccounts.php b/app/Console/Commands/Correction/CorrectsFrontpageAccounts.php similarity index 91% rename from app/Console/Commands/Correction/FixFrontpageAccounts.php rename to app/Console/Commands/Correction/CorrectsFrontpageAccounts.php index 883520e397..2d3021cdf1 100644 --- a/app/Console/Commands/Correction/FixFrontpageAccounts.php +++ b/app/Console/Commands/Correction/CorrectsFrontpageAccounts.php @@ -31,15 +31,12 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class FixFrontpageAccounts - */ -class FixFrontpageAccounts extends Command +class CorrectsFrontpageAccounts extends Command { use ShowsFriendlyMessages; protected $description = 'Fixes a preference that may include deleted accounts or accounts of another type.'; - protected $signature = 'firefly-iii:fix-frontpage-accounts'; + protected $signature = 'correction:frontpage-accounts'; /** * Execute the console command. @@ -55,7 +52,6 @@ class FixFrontpageAccounts extends Command $this->fixPreference($preference); } } - $this->friendlyPositive('Account preferences are OK'); return 0; } @@ -74,7 +70,7 @@ class FixFrontpageAccounts extends Command if (is_array($data)) { /** @var string $accountId */ foreach ($data as $accountId) { - $accountIdInt = (int)$accountId; + $accountIdInt = (int) $accountId; $account = $repository->find($accountIdInt); if (null !== $account && in_array($account->accountType->type, [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], true) diff --git a/app/Console/Commands/Correction/FixGroupAccounts.php b/app/Console/Commands/Correction/CorrectsGroupAccounts.php similarity index 85% rename from app/Console/Commands/Correction/FixGroupAccounts.php rename to app/Console/Commands/Correction/CorrectsGroupAccounts.php index 51847ce2c0..584e2aad84 100644 --- a/app/Console/Commands/Correction/FixGroupAccounts.php +++ b/app/Console/Commands/Correction/CorrectsGroupAccounts.php @@ -31,15 +31,12 @@ use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; -/** - * Class FixGroupAccounts - */ -class FixGroupAccounts extends Command +class CorrectsGroupAccounts extends Command { use ShowsFriendlyMessages; protected $description = 'Unify the source / destination accounts of split groups.'; - protected $signature = 'firefly-iii:unify-group-accounts'; + protected $signature = 'correction:group-accounts'; /** * Execute the console command. @@ -53,8 +50,8 @@ class FixGroupAccounts extends Command /** @var TransactionJournal $journal */ foreach ($res as $journal) { - if ((int)$journal->the_count > 1) { - $groups[] = (int)$journal->transaction_group_id; + if ((int) $journal->the_count > 1) { + $groups[] = (int) $journal->transaction_group_id; } } $handler = new UpdatedGroupEventHandler(); @@ -64,8 +61,6 @@ class FixGroupAccounts extends Command $handler->unifyAccounts($event); } - $this->friendlyPositive('Updated possible inconsistent transaction groups.'); - return 0; } } diff --git a/app/Console/Commands/Integrity/UpdateGroupInformation.php b/app/Console/Commands/Correction/CorrectsGroupInformation.php similarity index 90% rename from app/Console/Commands/Integrity/UpdateGroupInformation.php rename to app/Console/Commands/Correction/CorrectsGroupInformation.php index 57df0f52a6..1a187d2da5 100644 --- a/app/Console/Commands/Integrity/UpdateGroupInformation.php +++ b/app/Console/Commands/Correction/CorrectsGroupInformation.php @@ -1,8 +1,8 @@ . + * along with this program. If not, see https://www.gnu.org/licenses/. */ declare(strict_types=1); -namespace FireflyIII\Console\Commands\Integrity; +namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Models\Account; @@ -45,15 +45,12 @@ use FireflyIII\User; use Illuminate\Console\Command; use Illuminate\Database\QueryException; -/** - * Class UpdateGroupInformation - */ -class UpdateGroupInformation extends Command +class CorrectsGroupInformation extends Command { use ShowsFriendlyMessages; protected $description = 'Makes sure that every object is linked to a group'; - protected $signature = 'firefly-iii:upgrade-group-information'; + protected $signature = 'correction:group-information'; /** * Execute the console command. @@ -79,7 +76,7 @@ class UpdateGroupInformation extends Command { $group = $user->userGroup; if (null === $group) { - $this->friendlyWarning(sprintf('User "%s" has no group.', $user->email)); + $this->friendlyWarning(sprintf('User "%s" has no group. Please run "php artisan firefly-iii:create-group-memberships"', $user->email)); return; } diff --git a/app/Console/Commands/Correction/FixIbans.php b/app/Console/Commands/Correction/CorrectsIbans.php similarity index 94% rename from app/Console/Commands/Correction/FixIbans.php rename to app/Console/Commands/Correction/CorrectsIbans.php index c20c21be02..672fce3281 100644 --- a/app/Console/Commands/Correction/FixIbans.php +++ b/app/Console/Commands/Correction/CorrectsIbans.php @@ -30,15 +30,12 @@ use FireflyIII\Models\AccountType; use Illuminate\Console\Command; use Illuminate\Support\Collection; -/** - * Class FixIbans - */ -class FixIbans extends Command +class CorrectsIbans extends Command { use ShowsFriendlyMessages; protected $description = 'Removes spaces from IBANs'; - protected $signature = 'firefly-iii:fix-ibans'; + protected $signature = 'correction:ibans'; private int $count = 0; /** @@ -49,9 +46,6 @@ class FixIbans extends Command $accounts = Account::whereNotNull('iban')->get(); $this->filterIbans($accounts); $this->countAndCorrectIbans($accounts); - if (0 === $this->count) { - $this->friendlyPositive('All IBANs are valid.'); - } return 0; } diff --git a/app/Console/Commands/Correction/FixLongDescriptions.php b/app/Console/Commands/Correction/CorrectsLongDescriptions.php similarity index 81% rename from app/Console/Commands/Correction/FixLongDescriptions.php rename to app/Console/Commands/Correction/CorrectsLongDescriptions.php index 6830dd40e9..36dae29fc3 100644 --- a/app/Console/Commands/Correction/FixLongDescriptions.php +++ b/app/Console/Commands/Correction/CorrectsLongDescriptions.php @@ -28,24 +28,22 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; -/** - * Class FixLongDescriptions - */ -class FixLongDescriptions extends Command +class CorrectsLongDescriptions extends Command { use ShowsFriendlyMessages; private const int MAX_LENGTH = 1000; protected $description = 'Fixes long descriptions in journals and groups.'; - protected $signature = 'firefly-iii:fix-long-descriptions'; + protected $signature = 'correction:long-descriptions'; /** * Execute the console command. */ public function handle(): int { - $journals = TransactionJournal::get(['id', 'description']); + $journals = TransactionJournal::where(DB::raw('LENGTH(description)'), '>', self::MAX_LENGTH)->get(['id', 'description']); $count = 0; /** @var TransactionJournal $journal */ @@ -58,20 +56,17 @@ class FixLongDescriptions extends Command } } - $groups = TransactionGroup::get(['id', 'title']); + $groups = TransactionGroup::where(DB::raw('LENGTH(title)'), '>', self::MAX_LENGTH)->get(['id', 'title']); /** @var TransactionGroup $group */ foreach ($groups as $group) { - if (strlen((string)$group->title) > self::MAX_LENGTH) { + if (strlen((string) $group->title) > self::MAX_LENGTH) { $group->title = substr($group->title, 0, self::MAX_LENGTH); $group->save(); $this->friendlyWarning(sprintf('Truncated description of transaction group #%d', $group->id)); ++$count; } } - if (0 === $count) { - $this->friendlyPositive('All transaction group and journal title lengths are within bounds.'); - } return 0; } diff --git a/app/Console/Commands/Correction/RenameMetaFields.php b/app/Console/Commands/Correction/CorrectsMetaDataFields.php similarity index 88% rename from app/Console/Commands/Correction/RenameMetaFields.php rename to app/Console/Commands/Correction/CorrectsMetaDataFields.php index 9278c74c23..731c5f49f7 100644 --- a/app/Console/Commands/Correction/RenameMetaFields.php +++ b/app/Console/Commands/Correction/CorrectsMetaDataFields.php @@ -26,16 +26,14 @@ namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; -/** - * Class RenameMetaFields - */ -class RenameMetaFields extends Command +class CorrectsMetaDataFields extends Command { use ShowsFriendlyMessages; protected $description = 'Rename changed meta fields.'; - protected $signature = 'firefly-iii:rename-meta-fields'; + protected $signature = 'correction:meta-fields'; private int $count = 0; @@ -61,9 +59,6 @@ class RenameMetaFields extends Command foreach ($changes as $original => $update) { $this->rename($original, $update); } - if (0 === $this->count) { - $this->friendlyPositive('All meta fields are correct.'); - } if (0 !== $this->count) { $this->friendlyInfo(sprintf('Renamed %d meta field(s).', $this->count)); } @@ -73,7 +68,7 @@ class RenameMetaFields extends Command private function rename(string $original, string $update): void { - $total = \DB::table('journal_meta') + $total = DB::table('journal_meta') ->where('name', '=', $original) ->update(['name' => $update]) ; diff --git a/app/Console/Commands/Correction/CorrectsNativeAmounts.php b/app/Console/Commands/Correction/CorrectsNativeAmounts.php new file mode 100644 index 0000000000..7cbab491fc --- /dev/null +++ b/app/Console/Commands/Correction/CorrectsNativeAmounts.php @@ -0,0 +1,243 @@ +friendlyInfo('This command will not run because currency exchange rates are disabled.'); + + return 0; + } + Log::debug('Will update all native amounts. This may take some time.'); + $this->friendlyWarning('Recalculating native amounts for all objects. This may take some time!'); + + /** @var UserGroupRepositoryInterface $repository */ + $repository = app(UserGroupRepositoryInterface::class); + + /** @var UserGroup $userGroup */ + foreach ($repository->getAll() as $userGroup) { + $this->recalculateForGroup($userGroup); + } + $this->friendlyInfo('Recalculated all native amounts.'); + + return 0; + } + + private function recalculateForGroup(UserGroup $userGroup): void + { + Log::debug(sprintf('Now recalculating for user group #%d', $userGroup->id)); + $this->recalculateAccounts($userGroup); + + // do a check with the group's currency so we can skip some stuff. + Preferences::mark(); + $currency = app('amount')->getDefaultCurrencyByUserGroup($userGroup); + + $this->recalculatePiggyBanks($userGroup, $currency); + $this->recalculateBudgets($userGroup, $currency); + $this->recalculateAvailableBudgets($userGroup, $currency); + $this->recalculateBills($userGroup, $currency); + $this->calculateTransactions($userGroup, $currency); + + } + + private function recalculateAccounts(UserGroup $userGroup): void + { + $set = $userGroup->accounts()->where(function (EloquentBuilder $q): void { + $q->whereNotNull('virtual_balance'); + $q->orWhere('virtual_balance', '!=', ''); + })->get(); + + /** @var Account $account */ + foreach ($set as $account) { + $account->touch(); + } + Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id)); + } + + private function recalculatePiggyBanks(UserGroup $userGroup, TransactionCurrency $currency): void + { + $converter = new ExchangeRateConverter(); + $converter->setUserGroup($userGroup); + $converter->setIgnoreSettings(true); + $repository = app(PiggyBankRepositoryInterface::class); + $repository->setUserGroup($userGroup); + $set = $repository->getPiggyBanks(); + $set = $set->filter( + static function (PiggyBank $piggyBank) use ($currency) { + return $currency->id !== $piggyBank->transaction_currency_id; + } + ); + foreach ($set as $piggyBank) { + $piggyBank->encrypted = false; + $piggyBank->save(); + + foreach ($piggyBank->accounts as $account) { + $account->pivot->native_current_amount = null; + if (0 !== bccomp((string) $account->pivot->current_amount, '0')) { + $account->pivot->native_current_amount = $converter->convert($piggyBank->transactionCurrency, $currency, today(), (string) $account->pivot->current_amount); + } + $account->pivot->save(); + } + $this->recalculatePiggyBankEvents($piggyBank); + } + Log::debug(sprintf('Recalculated %d piggy banks for user group #%d.', $set->count(), $userGroup->id)); + + } + + private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void + { + $set = $piggyBank->piggyBankEvents()->get(); + $set->each( + static function (PiggyBankEvent $event): void { + $event->touch(); + } + ); + Log::debug(sprintf('Recalculated %d piggy bank events.', $set->count())); + } + + private function recalculateBudgets(UserGroup $userGroup, TransactionCurrency $currency): void + { + $set = $userGroup->budgets()->get(); + + /** @var Budget $budget */ + foreach ($set as $budget) { + $this->recalculateBudgetLimits($budget, $currency); + $this->recalculateAutoBudgets($budget, $currency); + } + Log::debug(sprintf('Recalculated %d budgets.', $set->count())); + } + + private function recalculateBudgetLimits(Budget $budget, TransactionCurrency $currency): void + { + $set = $budget->budgetlimits()->where('transaction_currency_id', '!=', $currency->id)->get(); + + /** @var BudgetLimit $limit */ + foreach ($set as $limit) { + Log::debug(sprintf('Will now touch BL #%d', $limit->id)); + $limit->touch(); + Log::debug(sprintf('Done with touch BL #%d', $limit->id)); + } + Log::debug(sprintf('Recalculated %d budget limits for budget #%d.', $set->count(), $budget->id)); + } + + private function recalculateAutoBudgets(Budget $budget, TransactionCurrency $currency): void + { + $set = $budget->autoBudgets()->where('transaction_currency_id', '!=', $currency->id)->get(); + + /** @var AutoBudget $autoBudget */ + foreach ($set as $autoBudget) { + $autoBudget->touch(); + } + Log::debug(sprintf('Recalculated %d auto budgets for budget #%d.', $set->count(), $budget->id)); + } + + private function recalculateAvailableBudgets(UserGroup $userGroup, TransactionCurrency $currency): void + { + Log::debug('Start with available budgets.'); + $set = $userGroup->availableBudgets()->where('transaction_currency_id', '!=', $currency->id)->get(); + + /** @var AvailableBudget $budget */ + foreach ($set as $budget) { + $budget->touch(); + } + Log::debug(sprintf('Recalculated %d available budgets.', $set->count())); + } + + private function recalculateBills(UserGroup $userGroup, TransactionCurrency $currency): void + { + $set = $userGroup->bills()->where('transaction_currency_id', '!=', $currency->id)->get(); + + /** @var Bill $bill */ + foreach ($set as $bill) { + $bill->touch(); + } + Log::debug(sprintf('Recalculated %d bills.', $set->count())); + } + + private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void + { + // custom query because of the potential size of this update. + $set = DB::table('transactions') + ->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.user_group_id', $userGroup->id) + + ->where(function (DatabaseBuilder $q1) use ($currency): void { + $q1->where(function (DatabaseBuilder $q2) use ($currency): void { + $q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id'); + })->orWhere(function (DatabaseBuilder $q3) use ($currency): void { + $q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id); + }); + }) +// ->where(static function (DatabaseBuilder $q) use ($currency): void { +// $q->whereNot('transactions.transaction_currency_id', $currency->id) +// ->whereNot('transactions.foreign_currency_id', $currency->id) +// ; +// }) + ->get(['transactions.id']) + ; + TransactionObserver::$recalculate = false; + foreach ($set as $item) { + // here we are. + $transaction = Transaction::find($item->id); + $transaction->touch(); + } + TransactionObserver::$recalculate = true; + Log::debug(sprintf('Recalculated %d transactions.', $set->count())); + } +} diff --git a/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php b/app/Console/Commands/Correction/CorrectsOpeningBalanceCurrencies.php similarity index 91% rename from app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php rename to app/Console/Commands/Correction/CorrectsOpeningBalanceCurrencies.php index a1dcf6c476..16a3d196ce 100644 --- a/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php +++ b/app/Console/Commands/Correction/CorrectsOpeningBalanceCurrencies.php @@ -35,15 +35,12 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use Illuminate\Console\Command; use Illuminate\Support\Collection; -/** - * Class CorrectOpeningBalanceCurrencies - */ -class CorrectOpeningBalanceCurrencies extends Command +class CorrectsOpeningBalanceCurrencies extends Command { use ShowsFriendlyMessages; protected $description = 'Will make sure that opening balance transaction currencies match the account they\'re for.'; - protected $signature = 'firefly-iii:fix-ob-currencies'; + protected $signature = 'correction:opening-balance-currencies'; /** * Execute the console command. @@ -62,10 +59,6 @@ class CorrectOpeningBalanceCurrencies extends Command $message = sprintf('Corrected %d opening balance transaction(s).', $count); $this->friendlyInfo($message); } - if (0 === $count) { - $message = 'There was nothing to fix in the opening balance transactions.'; - $this->friendlyPositive($message); - } return 0; } @@ -115,7 +108,7 @@ class CorrectOpeningBalanceCurrencies extends Command { $currency = $this->getCurrency($account); $count = 0; - if ((int)$journal->transaction_currency_id !== $currency->id) { + if ((int) $journal->transaction_currency_id !== $currency->id) { $journal->transaction_currency_id = $currency->id; $journal->save(); $count = 1; diff --git a/app/Console/Commands/Correction/FixPiggies.php b/app/Console/Commands/Correction/CorrectsPiggyBanks.php similarity index 88% rename from app/Console/Commands/Correction/FixPiggies.php rename to app/Console/Commands/Correction/CorrectsPiggyBanks.php index 7dff784e1b..8922629960 100644 --- a/app/Console/Commands/Correction/FixPiggies.php +++ b/app/Console/Commands/Correction/CorrectsPiggyBanks.php @@ -29,17 +29,12 @@ use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; -/** - * Report (and fix) piggy banks. - * - * Class FixPiggies - */ -class FixPiggies extends Command +class CorrectsPiggyBanks extends Command { use ShowsFriendlyMessages; protected $description = 'Fixes common issues with piggy banks.'; - protected $signature = 'firefly-iii:fix-piggies'; + protected $signature = 'correction:piggy-banks'; /** * Execute the console command. @@ -66,9 +61,6 @@ class FixPiggies extends Command continue; } } - if (0 === $count) { - $this->friendlyPositive('All piggy bank events are OK.'); - } if (0 !== $count) { $this->friendlyInfo(sprintf('Fixed %d piggy bank event(s).', $count)); } diff --git a/app/Console/Commands/Correction/MigratePreferences.php b/app/Console/Commands/Correction/CorrectsPreferences.php similarity index 92% rename from app/Console/Commands/Correction/MigratePreferences.php rename to app/Console/Commands/Correction/CorrectsPreferences.php index a42e1668f7..e8e4b04a6b 100644 --- a/app/Console/Commands/Correction/MigratePreferences.php +++ b/app/Console/Commands/Correction/CorrectsPreferences.php @@ -28,11 +28,11 @@ use FireflyIII\User; use Illuminate\Console\Command; use Symfony\Component\Console\Command\Command as CommandAlias; -class MigratePreferences extends Command +class CorrectsPreferences extends Command { protected $description = 'Give Firefly III preferences a user group ID so they can be made administration specific.'; - protected $signature = 'firefly-iii:migrate-preferences'; + protected $signature = 'correction:preferences'; /** * Execute the console command. @@ -50,7 +50,7 @@ class MigratePreferences extends Command if (null === $preference) { continue; } - if (null !== $preference->user_group_id) { + if (null === $preference->user_group_id) { $preference->user_group_id = $user->user_group_id; $preference->save(); ++$count; diff --git a/app/Console/Commands/Correction/FixRecurringTransactions.php b/app/Console/Commands/Correction/CorrectsRecurringTransactions.php similarity index 93% rename from app/Console/Commands/Correction/FixRecurringTransactions.php rename to app/Console/Commands/Correction/CorrectsRecurringTransactions.php index a7c3e70bb3..e7d302dff2 100644 --- a/app/Console/Commands/Correction/FixRecurringTransactions.php +++ b/app/Console/Commands/Correction/CorrectsRecurringTransactions.php @@ -33,15 +33,12 @@ use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class FixRecurringTransactions - */ -class FixRecurringTransactions extends Command +class CorrectsRecurringTransactions extends Command { use ShowsFriendlyMessages; protected $description = 'Fixes recurring transactions with the wrong transaction type.'; - protected $signature = 'firefly-iii:fix-recurring-transactions'; + protected $signature = 'correction:recurring-transactions'; private int $count = 0; private RecurringRepositoryInterface $recurringRepos; private UserRepositoryInterface $userRepos; @@ -53,9 +50,6 @@ class FixRecurringTransactions extends Command { $this->stupidLaravel(); $this->correctTransactions(); - if (0 === $this->count) { - $this->friendlyPositive('All recurring transactions are OK.'); - } return 0; } diff --git a/app/Console/Commands/Integrity/AddTimezonesToDates.php b/app/Console/Commands/Correction/CorrectsTimezoneInformation.php similarity index 60% rename from app/Console/Commands/Integrity/AddTimezonesToDates.php rename to app/Console/Commands/Correction/CorrectsTimezoneInformation.php index 7dafa505c4..5b3b110bc6 100644 --- a/app/Console/Commands/Integrity/AddTimezonesToDates.php +++ b/app/Console/Commands/Correction/CorrectsTimezoneInformation.php @@ -1,5 +1,25 @@ ['date'], // done + AvailableBudget::class => ['start_date', 'end_date'], // done + Bill::class => ['date', 'end_date', 'extension_date'], // done + BudgetLimit::class => ['start_date', 'end_date'], // done + CurrencyExchangeRate::class => ['date'], // done + InvitedUser::class => ['expires'], + PiggyBankEvent::class => ['date'], + PiggyBankRepetition::class => ['start_date', 'target_date'], + PiggyBank::class => ['start_date', 'target_date'], // done + Recurrence::class => ['first_date', 'repeat_until', 'latest_date'], + Tag::class => ['date'], + TransactionJournal::class => ['date'], + ]; /** * The console command description. * * @var string */ - protected $description = 'Make sure all dates have a timezone.'; + protected $description = 'Make sure all dates have a timezone.'; - public static array $models = [ - AccountBalance::class => ['date'], // done - AvailableBudget::class => ['start_date', 'end_date'], // done - Bill::class => ['date', 'end_date', 'extension_date'], // done - BudgetLimit::class => ['start_date', 'end_date'], // done - CurrencyExchangeRate::class => ['date'], // done - InvitedUser::class => ['expires'], - PiggyBankEvent::class => ['date'], - PiggyBankRepetition::class => ['startdate', 'targetdate'], - PiggyBank::class => ['startdate', 'targetdate'], // done - Recurrence::class => ['first_date', 'repeat_until', 'latest_date'], - Tag::class => ['date'], - TransactionJournal::class => ['date'], - ]; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'correction:timezones'; /** * Execute the console command. @@ -106,8 +127,6 @@ class AddTimezonesToDates extends Command Log::error($e->getMessage()); } if (0 === $count) { - $this->friendlyPositive(sprintf('Timezone information is present in field "%s" of model "%s".', $field, $shortModel)); - return; } $this->friendlyInfo(sprintf('Adding timezone information to field "%s" of model "%s".', $field, $shortModel)); diff --git a/app/Console/Commands/Correction/FixTransactionTypes.php b/app/Console/Commands/Correction/CorrectsTransactionTypes.php similarity index 93% rename from app/Console/Commands/Correction/FixTransactionTypes.php rename to app/Console/Commands/Correction/CorrectsTransactionTypes.php index a5bcfb9e15..bb185d5d87 100644 --- a/app/Console/Commands/Correction/FixTransactionTypes.php +++ b/app/Console/Commands/Correction/CorrectsTransactionTypes.php @@ -32,16 +32,14 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use Illuminate\Console\Command; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; -/** - * Class FixTransactionTypes - */ -class FixTransactionTypes extends Command +class CorrectsTransactionTypes extends Command { use ShowsFriendlyMessages; protected $description = 'Make sure all transactions are of the correct type, based on source + dest.'; - protected $signature = 'firefly-iii:fix-transaction-types'; + protected $signature = 'correction:transaction-types'; /** * Execute the console command. @@ -50,6 +48,7 @@ class FixTransactionTypes extends Command { $count = 0; $journals = $this->collectJournals(); + Log::debug(sprintf('In FixTransactionTypes, found %d journals.', $journals->count())); /** @var TransactionJournal $journal */ foreach ($journals as $journal) { @@ -90,7 +89,7 @@ class FixTransactionTypes extends Command return false; } - $expectedType = (string)config(sprintf('firefly.account_to_transaction.%s.%s', $source->accountType->type, $destination->accountType->type)); + $expectedType = (string) config(sprintf('firefly.account_to_transaction.%s.%s', $source->accountType->type, $destination->accountType->type)); if ($expectedType !== $type) { $this->friendlyWarning( sprintf( diff --git a/app/Console/Commands/Correction/TransferBudgets.php b/app/Console/Commands/Correction/CorrectsTransferBudgets.php similarity index 89% rename from app/Console/Commands/Correction/TransferBudgets.php rename to app/Console/Commands/Correction/CorrectsTransferBudgets.php index 61549320b6..7d33e95a8d 100644 --- a/app/Console/Commands/Correction/TransferBudgets.php +++ b/app/Console/Commands/Correction/CorrectsTransferBudgets.php @@ -29,15 +29,12 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use Illuminate\Console\Command; -/** - * Class TransferBudgets - */ -class TransferBudgets extends Command +class CorrectsTransferBudgets extends Command { use ShowsFriendlyMessages; protected $description = 'Removes budgets from transfers.'; - protected $signature = 'firefly-iii:fix-transfer-budgets'; + protected $signature = 'correction:transfer-budgets'; /** * Execute the console command. @@ -60,10 +57,6 @@ class TransferBudgets extends Command $entry->budgets()->sync([]); ++$count; } - if (0 === $count) { - $message = 'No invalid budget/journal entries.'; - $this->friendlyPositive($message); - } if (0 !== $count) { $message = sprintf('Corrected %d invalid budget/journal entries (entry).', $count); app('log')->debug($message); diff --git a/app/Console/Commands/Correction/FixUnevenAmount.php b/app/Console/Commands/Correction/CorrectsUnevenAmount.php similarity index 95% rename from app/Console/Commands/Correction/FixUnevenAmount.php rename to app/Console/Commands/Correction/CorrectsUnevenAmount.php index e3b39c4ca3..c97d384462 100644 --- a/app/Console/Commands/Correction/FixUnevenAmount.php +++ b/app/Console/Commands/Correction/CorrectsUnevenAmount.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; @@ -32,15 +33,12 @@ use FireflyIII\Support\Models\AccountBalanceCalculator; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; -/** - * Class FixUnevenAmount - */ -class FixUnevenAmount extends Command +class CorrectsUnevenAmount extends Command { use ShowsFriendlyMessages; protected $description = 'Fix journals with uneven amounts.'; - protected $signature = 'firefly-iii:fix-uneven-amount'; + protected $signature = 'correction:uneven-amounts'; private int $count; /** @@ -54,13 +52,112 @@ class FixUnevenAmount extends Command $this->matchCurrencies(); if (config('firefly.feature_flags.running_balance_column')) { $this->friendlyInfo('Will recalculate transaction running balance columns. This may take a LONG time. Please be patient.'); - AccountBalanceCalculator::recalculateAll(true); + AccountBalanceCalculator::recalculateAll(false); $this->friendlyInfo('Done recalculating transaction running balance columns.'); } return 0; } + private function convertOldStyleTransfers(): void + { + Log::debug('convertOldStyleTransfers()'); + // select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different. + $transactions = Transaction::distinct() + ->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id') + ->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id') + ->where('transaction_types.type', TransactionTypeEnum::TRANSFER->value) + ->whereNotNull('foreign_currency_id') + ->whereNotNull('foreign_amount')->get(['transactions.transaction_journal_id']) + ; + $count = 0; + + Log::debug(sprintf('Found %d potential journal(s)', $transactions->count())); + + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + /** @var null|TransactionJournal $journal */ + $journal = TransactionJournal::find($transaction->transaction_journal_id); + if (null === $journal) { + Log::debug('Found no journal, continue.'); + + continue; + } + // needs to be a transfer. + if (TransactionType::TRANSFER !== $journal->transactionType->type) { + Log::debug('Must be a transfer, continue.'); + + continue; + } + + /** @var null|Transaction $destination */ + $destination = $journal->transactions()->where('amount', '>', 0)->first(); + + /** @var null|Transaction $source */ + $source = $journal->transactions()->where('amount', '<', 0)->first(); + if (null === $destination || null === $source) { + Log::debug('Source or destination transaction is NULL, continue.'); + + // will be picked up later. + continue; + } + if ($source->transaction_currency_id === $destination->transaction_currency_id) { + Log::debug('Ready to swap data between transactions.'); + $destination->foreign_currency_id = $source->transaction_currency_id; + $destination->foreign_amount = app('steam')->positive($source->amount); + $destination->transaction_currency_id = $source->foreign_currency_id; + $destination->amount = app('steam')->positive($source->foreign_amount); + $destination->balance_dirty = true; + $source->balance_dirty = true; + $destination->save(); + $source->save(); + $this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id)); + ++$count; + } + } + } + + private function fixUnevenAmounts(): void + { + $journals = \DB::table('transactions') + ->groupBy('transaction_journal_id') + ->whereNull('deleted_at') + ->get(['transaction_journal_id', \DB::raw('SUM(amount) AS the_sum')]) + ; + + /** @var \stdClass $entry */ + foreach ($journals as $entry) { + $sum = (string) $entry->the_sum; + if (!is_numeric($sum) + || '' === $sum // @phpstan-ignore-line + || str_contains($sum, 'e') + || str_contains($sum, ',')) { + $message = sprintf( + 'Journal #%d has an invalid sum ("%s"). No sure what to do.', + $entry->transaction_journal_id, + $entry->the_sum + ); + $this->friendlyWarning($message); + app('log')->warning($message); + ++$this->count; + + continue; + } + $res = -1; + + try { + $res = bccomp($sum, '0'); + } catch (\ValueError $e) { + $this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum)); + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + } + if (0 !== $res) { + $this->fixJournal($entry->transaction_journal_id); + } + } + } + private function fixJournal(int $param): void { // one of the transactions is bad. @@ -129,78 +226,6 @@ class FixUnevenAmount extends Command ++$this->count; } - private function fixUnevenAmounts(): void - { - $journals = \DB::table('transactions') - ->groupBy('transaction_journal_id') - ->whereNull('deleted_at') - ->get(['transaction_journal_id', \DB::raw('SUM(amount) AS the_sum')]) - ; - - /** @var \stdClass $entry */ - foreach ($journals as $entry) { - $sum = (string) $entry->the_sum; - if (!is_numeric($sum) - || '' === $sum // @phpstan-ignore-line - || str_contains($sum, 'e') - || str_contains($sum, ',')) { - $message = sprintf( - 'Journal #%d has an invalid sum ("%s"). No sure what to do.', - $entry->transaction_journal_id, - $entry->the_sum - ); - $this->friendlyWarning($message); - app('log')->warning($message); - ++$this->count; - - continue; - } - $res = -1; - - try { - $res = bccomp($sum, '0'); - } catch (\ValueError $e) { - $this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum)); - Log::error($e->getMessage()); - Log::error($e->getTraceAsString()); - } - if (0 !== $res) { - $this->fixJournal($entry->transaction_journal_id); - } - } - if (0 === $this->count) { - $this->friendlyPositive('Database amount integrity is OK'); - } - } - - private function matchCurrencies(): void - { - $journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id') - ->where('transactions.transaction_currency_id', '!=', \DB::raw('transaction_journals.transaction_currency_id')) - ->get(['transaction_journals.*']) - ; - - $count = 0; - - /** @var TransactionJournal $journal */ - foreach ($journals as $journal) { - if (!$this->isForeignCurrencyTransfer($journal)) { - Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]); - ++$count; - - continue; - } - Log::debug(sprintf('Can skip foreign currency transfer #%d.', $journal->id)); - } - if (0 === $count) { - $this->friendlyPositive('Journal currency integrity is OK'); - - return; - } - - $this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count())); - } - private function isForeignCurrencyTransfer(TransactionJournal $journal): bool { if (TransactionType::TRANSFER !== $journal->transactionType->type) { @@ -235,63 +260,29 @@ class FixUnevenAmount extends Command return false; } - private function convertOldStyleTransfers(): void + private function matchCurrencies(): void { - Log::debug('convertOldStyleTransfers()'); - // select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different. - $transactions = Transaction::distinct() - ->whereNotNull('foreign_currency_id') - ->whereNotNull('foreign_amount')->get(['transactions.transaction_journal_id']) + $journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id') + ->where('transactions.transaction_currency_id', '!=', \DB::raw('transaction_journals.transaction_currency_id')) + ->get(['transaction_journals.*']) ; - $count = 0; - Log::debug(sprintf('Found %d potential journal(s)', $transactions->count())); + $count = 0; - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - /** @var null|TransactionJournal $journal */ - $journal = TransactionJournal::find($transaction->transaction_journal_id); - if (null === $journal) { - Log::debug('Found no journal, continue.'); - - continue; - } - // needs to be a transfer. - if (TransactionType::TRANSFER !== $journal->transactionType->type) { - Log::debug('Must be a transfer, continue.'); - - continue; - } - - /** @var null|Transaction $destination */ - $destination = $journal->transactions()->where('amount', '>', 0)->first(); - - /** @var null|Transaction $source */ - $source = $journal->transactions()->where('amount', '<', 0)->first(); - if (null === $destination || null === $source) { - Log::debug('Source or destination transaction is NULL, continue.'); - - // will be picked up later. - continue; - } - if ($source->transaction_currency_id === $destination->transaction_currency_id) { - Log::debug('Ready to swap data between transactions.'); - $destination->foreign_currency_id = $source->transaction_currency_id; - $destination->foreign_amount = app('steam')->positive($source->amount); - $destination->transaction_currency_id = $source->foreign_currency_id; - $destination->amount = app('steam')->positive($source->foreign_amount); - $destination->balance_dirty = true; - $source->balance_dirty = true; - $destination->save(); - $source->save(); - $this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id)); + /** @var TransactionJournal $journal */ + foreach ($journals as $journal) { + if (!$this->isForeignCurrencyTransfer($journal)) { + Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]); ++$count; + + continue; } + Log::debug(sprintf('Can skip foreign currency transfer #%d.', $journal->id)); } if (0 === $count) { - $this->friendlyPositive('No "old style" foreign currency transfers.'); - return; } + + $this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count())); } } diff --git a/app/Console/Commands/Correction/CreateAccessTokens.php b/app/Console/Commands/Correction/CreatesAccessTokens.php similarity index 89% rename from app/Console/Commands/Correction/CreateAccessTokens.php rename to app/Console/Commands/Correction/CreatesAccessTokens.php index ba17f25e1e..2dcfcb7ffd 100644 --- a/app/Console/Commands/Correction/CreateAccessTokens.php +++ b/app/Console/Commands/Correction/CreatesAccessTokens.php @@ -29,16 +29,13 @@ use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class CreateAccessTokens - */ -class CreateAccessTokens extends Command +class CreatesAccessTokens extends Command { use ShowsFriendlyMessages; protected $description = 'Creates user access tokens which are used for command line access to personal data.'; - protected $signature = 'firefly-iii:create-access-tokens'; + protected $signature = 'correction:access-tokens'; /** * Execute the console command. @@ -64,9 +61,6 @@ class CreateAccessTokens extends Command ++$count; } } - if (0 === $count) { - $this->friendlyPositive('Verified access tokens.'); - } return 0; } diff --git a/app/Console/Commands/Integrity/CreateGroupMemberships.php b/app/Console/Commands/Correction/CreatesGroupMemberships.php similarity index 87% rename from app/Console/Commands/Integrity/CreateGroupMemberships.php rename to app/Console/Commands/Correction/CreatesGroupMemberships.php index 0927f1b7cb..92709fdad0 100644 --- a/app/Console/Commands/Integrity/CreateGroupMemberships.php +++ b/app/Console/Commands/Correction/CreatesGroupMemberships.php @@ -1,8 +1,8 @@ . + * along with this program. If not, see https://www.gnu.org/licenses/. */ declare(strict_types=1); -namespace FireflyIII\Console\Commands\Integrity; +namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Enums\UserRoleEnum; @@ -33,16 +33,13 @@ use FireflyIII\Models\UserRole; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class CreateGroupMemberships - */ -class CreateGroupMemberships extends Command +class CreatesGroupMemberships extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '560_create_group_memberships'; protected $description = 'Update group memberships'; - protected $signature = 'firefly-iii:create-group-memberships'; + protected $signature = 'correction:create-group-memberships'; /** * Execute the console command. @@ -52,7 +49,6 @@ class CreateGroupMemberships extends Command public function handle(): int { $this->createGroupMemberships(); - $this->friendlyPositive('Validated group memberships'); return 0; } diff --git a/app/Console/Commands/Correction/CreateLinkTypes.php b/app/Console/Commands/Correction/CreatesLinkTypes.php similarity index 88% rename from app/Console/Commands/Correction/CreateLinkTypes.php rename to app/Console/Commands/Correction/CreatesLinkTypes.php index 88b13ec3a9..19592b03cf 100644 --- a/app/Console/Commands/Correction/CreateLinkTypes.php +++ b/app/Console/Commands/Correction/CreatesLinkTypes.php @@ -28,16 +28,13 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Models\LinkType; use Illuminate\Console\Command; -/** - * Class CreateLinkTypes. Created all link types in case a migration hasn't fired. - */ -class CreateLinkTypes extends Command +class CreatesLinkTypes extends Command { use ShowsFriendlyMessages; protected $description = 'Creates all link types.'; - protected $signature = 'firefly-iii:create-link-types'; + protected $signature = 'correction:link-types'; /** * Execute the console command. @@ -66,9 +63,6 @@ class CreateLinkTypes extends Command $link->editable = false; $link->save(); } - if (0 === $count) { - $this->friendlyPositive('All link types are OK'); - } return 0; } diff --git a/app/Console/Commands/Correction/RemoveBills.php b/app/Console/Commands/Correction/RemovesBills.php similarity index 91% rename from app/Console/Commands/Correction/RemoveBills.php rename to app/Console/Commands/Correction/RemovesBills.php index 997f46af26..e327e37b5e 100644 --- a/app/Console/Commands/Correction/RemoveBills.php +++ b/app/Console/Commands/Correction/RemovesBills.php @@ -29,15 +29,12 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use Illuminate\Console\Command; -/** - * Class RemoveBills - */ -class RemoveBills extends Command +class RemovesBills extends Command { use ShowsFriendlyMessages; protected $description = 'Remove bills from transactions that shouldn\'t have one.'; - protected $signature = 'firefly-iii:remove-bills'; + protected $signature = 'correction:bills'; /** * Execute the console command. @@ -60,7 +57,6 @@ class RemoveBills extends Command if ($journals->count() > 0) { $this->friendlyInfo('Fixed all transaction journals so they have correct bill information.'); } - $this->friendlyPositive('All bills and journals are OK'); return 0; } diff --git a/app/Console/Commands/Correction/DeleteEmptyGroups.php b/app/Console/Commands/Correction/RemovesEmptyGroups.php similarity index 88% rename from app/Console/Commands/Correction/DeleteEmptyGroups.php rename to app/Console/Commands/Correction/RemovesEmptyGroups.php index b5f3363556..0d68e7f1a2 100644 --- a/app/Console/Commands/Correction/DeleteEmptyGroups.php +++ b/app/Console/Commands/Correction/RemovesEmptyGroups.php @@ -29,15 +29,12 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Models\TransactionGroup; use Illuminate\Console\Command; -/** - * Class DeleteEmptyGroups - */ -class DeleteEmptyGroups extends Command +class RemovesEmptyGroups extends Command { use ShowsFriendlyMessages; protected $description = 'Delete empty transaction groups.'; - protected $signature = 'firefly-iii:delete-empty-groups'; + protected $signature = 'correction:empty-groups'; /** * Execute the console command. @@ -61,9 +58,6 @@ class DeleteEmptyGroups extends Command TransactionGroup::whereNull('deleted_at')->whereIn('id', $chunk)->delete(); } } - if (0 === $total) { - $this->friendlyInfo('Verified there are no empty groups.'); - } return 0; } diff --git a/app/Console/Commands/Correction/DeleteEmptyJournals.php b/app/Console/Commands/Correction/RemovesEmptyJournals.php similarity index 89% rename from app/Console/Commands/Correction/DeleteEmptyJournals.php rename to app/Console/Commands/Correction/RemovesEmptyJournals.php index 3df105fdf1..cbb6c6c3e7 100644 --- a/app/Console/Commands/Correction/DeleteEmptyJournals.php +++ b/app/Console/Commands/Correction/RemovesEmptyJournals.php @@ -30,16 +30,13 @@ use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; use Illuminate\Database\QueryException; -/** - * Class DeleteEmptyJournals - */ -class DeleteEmptyJournals extends Command +class RemovesEmptyJournals extends Command { use ShowsFriendlyMessages; protected $description = 'Delete empty and uneven transaction journals.'; - protected $signature = 'firefly-iii:delete-empty-journals'; + protected $signature = 'correction:empty-journals'; /** * Execute the console command. @@ -65,7 +62,7 @@ class DeleteEmptyJournals extends Command /** @var Transaction $row */ foreach ($set as $row) { - $count = (int)$row->the_count; + $count = (int) $row->the_count; if (1 === $count % 2) { // uneven number, delete journal and transactions: try { @@ -82,9 +79,6 @@ class DeleteEmptyJournals extends Command ++$total; } } - if (0 === $total) { - $this->friendlyPositive('No uneven transaction journals.'); - } } private function deleteEmptyJournals(): void @@ -107,8 +101,5 @@ class DeleteEmptyJournals extends Command $this->friendlyInfo(sprintf('Deleted empty transaction journal #%d', $entry->id)); ++$count; } - if (0 === $count) { - $this->friendlyPositive('No empty transaction journals.'); - } } } diff --git a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php b/app/Console/Commands/Correction/RemovesOrphanedTransactions.php similarity index 91% rename from app/Console/Commands/Correction/DeleteOrphanedTransactions.php rename to app/Console/Commands/Correction/RemovesOrphanedTransactions.php index f0c64e4bf0..700ca259cf 100644 --- a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php +++ b/app/Console/Commands/Correction/RemovesOrphanedTransactions.php @@ -32,13 +32,13 @@ use Illuminate\Console\Command; /** * Deletes transactions where the journal has been deleted. */ -class DeleteOrphanedTransactions extends Command +class RemovesOrphanedTransactions extends Command { use ShowsFriendlyMessages; protected $description = 'Deletes orphaned transactions.'; - protected $signature = 'firefly-iii:delete-orphaned-transactions'; + protected $signature = 'correction:orphaned-transactions'; /** * Execute the console command. @@ -63,7 +63,7 @@ class DeleteOrphanedTransactions extends Command ; $count = $set->count(); if (0 === $count) { - $this->friendlyPositive('No orphaned journals.'); + // $this->friendlyPositive('No orphaned journals.'); return; } @@ -103,7 +103,7 @@ class DeleteOrphanedTransactions extends Command /** @var \stdClass $entry */ foreach ($set as $entry) { - $transaction = Transaction::find((int)$entry->transaction_id); + $transaction = Transaction::find((int) $entry->transaction_id); if (null !== $transaction) { $transaction->delete(); $this->friendlyWarning( @@ -116,9 +116,6 @@ class DeleteOrphanedTransactions extends Command ++$count; } } - if (0 === $count) { - $this->friendlyPositive('No orphaned transactions.'); - } } private function deleteFromOrphanedAccounts(): void @@ -147,8 +144,5 @@ class DeleteOrphanedTransactions extends Command ); ++$count; } - if (0 === $count) { - $this->friendlyPositive('No orphaned accounts.'); - } } } diff --git a/app/Console/Commands/Correction/DeleteZeroAmount.php b/app/Console/Commands/Correction/RemovesZeroAmount.php similarity index 87% rename from app/Console/Commands/Correction/DeleteZeroAmount.php rename to app/Console/Commands/Correction/RemovesZeroAmount.php index dc0f7cd954..fd9d1c344e 100644 --- a/app/Console/Commands/Correction/DeleteZeroAmount.php +++ b/app/Console/Commands/Correction/RemovesZeroAmount.php @@ -29,16 +29,13 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; -/** - * Class DeleteZeroAmount - */ -class DeleteZeroAmount extends Command +class RemovesZeroAmount extends Command { use ShowsFriendlyMessages; protected $description = 'Delete transactions with zero amount.'; - protected $signature = 'firefly-iii:delete-zero-amount'; + protected $signature = 'correction:zero-amounts'; /** * Execute the console command. @@ -56,9 +53,6 @@ class DeleteZeroAmount extends Command Transaction::where('transaction_journal_id', $journal->id)->delete(); } - if (0 === $journals->count()) { - $this->friendlyPositive('No zero-amount transaction journals.'); - } return 0; } diff --git a/app/Console/Commands/Integrity/RestoreOAuthKeys.php b/app/Console/Commands/Correction/RestoresOAuthKeys.php similarity index 86% rename from app/Console/Commands/Integrity/RestoreOAuthKeys.php rename to app/Console/Commands/Correction/RestoresOAuthKeys.php index b84a7eec14..8eaf3c79a7 100644 --- a/app/Console/Commands/Integrity/RestoreOAuthKeys.php +++ b/app/Console/Commands/Correction/RestoresOAuthKeys.php @@ -1,8 +1,8 @@ . + * along with this program. If not, see https://www.gnu.org/licenses/. */ declare(strict_types=1); -namespace FireflyIII\Console\Commands\Integrity; +namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Support\System\OAuthKeys; use Illuminate\Console\Command; -/** - * Class RestoreOAuthKeys - */ -class RestoreOAuthKeys extends Command +class RestoresOAuthKeys extends Command { use ShowsFriendlyMessages; protected $description = 'Will restore the OAuth keys generated for the system.'; - protected $signature = 'firefly-iii:restore-oauth-keys'; + protected $signature = 'correction:restore-oauth-keys'; /** * Execute the console command. @@ -76,7 +73,6 @@ class RestoreOAuthKeys extends Command return; } - $this->friendlyPositive('OAuth keys are OK'); } private function keysInDatabase(): bool diff --git a/app/Console/Commands/Correction/TriggerCreditCalculation.php b/app/Console/Commands/Correction/TriggersCreditCalculation.php similarity index 92% rename from app/Console/Commands/Correction/TriggerCreditCalculation.php rename to app/Console/Commands/Correction/TriggersCreditCalculation.php index 1cefe73a3a..57918d9976 100644 --- a/app/Console/Commands/Correction/TriggerCreditCalculation.php +++ b/app/Console/Commands/Correction/TriggersCreditCalculation.php @@ -28,13 +28,10 @@ use FireflyIII\Models\Account; use FireflyIII\Services\Internal\Support\CreditRecalculateService; use Illuminate\Console\Command; -/** - * Class CorrectionSkeleton - */ -class TriggerCreditCalculation extends Command +class TriggersCreditCalculation extends Command { protected $description = 'Triggers the credit recalculation service for liabilities.'; - protected $signature = 'firefly-iii:trigger-credit-recalculation'; + protected $signature = 'correction:recalculates-liabilities'; /** * Execute the console command. diff --git a/app/Console/Commands/Export/ExportData.php b/app/Console/Commands/Export/ExportsData.php similarity index 98% rename from app/Console/Commands/Export/ExportData.php rename to app/Console/Commands/Export/ExportsData.php index 0cd55a9804..97f528f7cd 100644 --- a/app/Console/Commands/Export/ExportData.php +++ b/app/Console/Commands/Export/ExportsData.php @@ -36,10 +36,7 @@ use FireflyIII\Support\Export\ExportDataGenerator; use Illuminate\Console\Command; use Illuminate\Support\Collection; -/** - * Class ExportData - */ -class ExportData extends Command +class ExportsData extends Command { use ShowsFriendlyMessages; use VerifiesAccessToken; @@ -231,7 +228,7 @@ class ExportData extends Command { $final = new Collection(); $accounts = new Collection(); - $accountList = (string)$this->option('accounts'); + $accountList = (string) $this->option('accounts'); $types = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]; if ('' !== $accountList) { $accountIds = explode(',', $accountList); @@ -260,7 +257,7 @@ class ExportData extends Command */ private function getExportDirectory(): string { - $directory = (string)$this->option('export_directory'); + $directory = (string) $this->option('export_directory'); if ('' === $directory) { $directory = './'; } diff --git a/app/Console/Commands/Integrity/ReportSkeleton.php.stub b/app/Console/Commands/Integrity/ReportSkeleton.php.stub index fa771b24c9..5efa56a32a 100644 --- a/app/Console/Commands/Integrity/ReportSkeleton.php.stub +++ b/app/Console/Commands/Integrity/ReportSkeleton.php.stub @@ -4,10 +4,6 @@ namespace FireflyIII\Console\Commands\Integrity; use Illuminate\Console\Command; -/** - * Class ReportSkeleton - * TODO DONT FORGET TO ADD THIS TO THE DOCKER BUILD - */ class ReportSkeleton extends Command { diff --git a/app/Console/Commands/Integrity/ReportEmptyObjects.php b/app/Console/Commands/Integrity/ReportsEmptyObjects.php similarity index 97% rename from app/Console/Commands/Integrity/ReportEmptyObjects.php rename to app/Console/Commands/Integrity/ReportsEmptyObjects.php index 811398d673..06c9d99d0c 100644 --- a/app/Console/Commands/Integrity/ReportEmptyObjects.php +++ b/app/Console/Commands/Integrity/ReportsEmptyObjects.php @@ -31,16 +31,13 @@ use FireflyIII\Models\Category; use FireflyIII\Models\Tag; use Illuminate\Console\Command; -/** - * Class ReportEmptyObjects - */ -class ReportEmptyObjects extends Command +class ReportsEmptyObjects extends Command { use ShowsFriendlyMessages; protected $description = 'Reports on empty database objects.'; - protected $signature = 'firefly-iii:report-empty-objects'; + protected $signature = 'integrity:empty-objects'; /** * Execute the console command. diff --git a/app/Console/Commands/Integrity/ReportIntegrity.php b/app/Console/Commands/Integrity/ReportsIntegrity.php similarity index 83% rename from app/Console/Commands/Integrity/ReportIntegrity.php rename to app/Console/Commands/Integrity/ReportsIntegrity.php index 3dc698a2d3..2ac856b24b 100644 --- a/app/Console/Commands/Integrity/ReportIntegrity.php +++ b/app/Console/Commands/Integrity/ReportsIntegrity.php @@ -27,10 +27,7 @@ namespace FireflyIII\Console\Commands\Integrity; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; -/** - * Class ReportIntegrity - */ -class ReportIntegrity extends Command +class ReportsIntegrity extends Command { use ShowsFriendlyMessages; @@ -48,11 +45,8 @@ class ReportIntegrity extends Command return 1; } $commands = [ - 'firefly-iii:add-timezones-to-dates', - 'firefly-iii:create-group-memberships', - 'firefly-iii:report-empty-objects', - 'firefly-iii:report-sum', - 'firefly-iii:upgrade-group-information', + 'integrity:empty-objects', + 'integrity:total-sums', ]; foreach ($commands as $command) { $this->friendlyLine(sprintf('Now executing %s', $command)); diff --git a/app/Console/Commands/Integrity/ReportSum.php b/app/Console/Commands/Integrity/ReportsSums.php similarity index 76% rename from app/Console/Commands/Integrity/ReportSum.php rename to app/Console/Commands/Integrity/ReportsSums.php index 62ab39afbb..f08c942d3a 100644 --- a/app/Console/Commands/Integrity/ReportSum.php +++ b/app/Console/Commands/Integrity/ReportsSums.php @@ -29,15 +29,12 @@ use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class ReportSkeleton - */ -class ReportSum extends Command +class ReportsSums extends Command { use ShowsFriendlyMessages; protected $description = 'Report on the total sum of transactions. Must be 0.'; - protected $signature = 'firefly-iii:report-sum'; + protected $signature = 'integrity:total-sums'; /** * Execute the console command. @@ -59,18 +56,22 @@ class ReportSum extends Command /** @var User $user */ foreach ($userRepository->all() as $user) { - $sum = (string)$user->transactions()->selectRaw('SUM(amount) + SUM(foreign_amount) as total')->value('total'); - if (!is_numeric($sum)) { - $message = sprintf('Error: Transactions for user #%d (%s) have an invalid sum ("%s").', $user->id, $user->email, $sum); + $sum = (string) $user->transactions()->selectRaw('SUM(amount) as total')->value('total'); + $foreign = (string) $user->transactions()->selectRaw('SUM(foreign_amount) as total')->value('total'); + $sum = '' === $sum ? '0' : $sum; + $foreign = '' === $foreign ? '0' : $foreign; + $total = bcadd($sum, $foreign); + if (!is_numeric($total)) { + $message = sprintf('Error: Transactions for user #%d (%s) have an invalid sum ("%s").', $user->id, $user->email, $total); $this->friendlyError($message); continue; } - if (0 !== bccomp($sum, '0')) { - $message = sprintf('Error: Transactions for user #%d (%s) are off by %s!', $user->id, $user->email, $sum); + if (0 !== bccomp($total, '0')) { + $message = sprintf('Error: Transactions for user #%d (%s) are off by %s!', $user->id, $user->email, $total); $this->friendlyError($message); } - if (0 === bccomp($sum, '0')) { + if (0 === bccomp($total, '0')) { $this->friendlyPositive(sprintf('Amount integrity OK for user #%d', $user->id)); } } diff --git a/app/Console/Commands/System/LaravelPassportKeys.php b/app/Console/Commands/System/CallsLaravelPassportKeys.php similarity index 97% rename from app/Console/Commands/System/LaravelPassportKeys.php rename to app/Console/Commands/System/CallsLaravelPassportKeys.php index 68003d0914..859a6817cd 100644 --- a/app/Console/Commands/System/LaravelPassportKeys.php +++ b/app/Console/Commands/System/CallsLaravelPassportKeys.php @@ -28,7 +28,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan; use Symfony\Component\Console\Command\Command as CommandAlias; -class LaravelPassportKeys extends Command +class CallsLaravelPassportKeys extends Command { use ShowsFriendlyMessages; diff --git a/app/Console/Commands/System/CreateDatabase.php b/app/Console/Commands/System/CreatesDatabase.php similarity index 98% rename from app/Console/Commands/System/CreateDatabase.php rename to app/Console/Commands/System/CreatesDatabase.php index 64b1c65895..bb7f4bb23f 100644 --- a/app/Console/Commands/System/CreateDatabase.php +++ b/app/Console/Commands/System/CreatesDatabase.php @@ -28,10 +28,7 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; use PDO; -/** - * Class CreateDatabase - */ -class CreateDatabase extends Command +class CreatesDatabase extends Command { use ShowsFriendlyMessages; diff --git a/app/Console/Commands/System/CreateFirstUser.php b/app/Console/Commands/System/CreatesFirstUser.php similarity index 95% rename from app/Console/Commands/System/CreateFirstUser.php rename to app/Console/Commands/System/CreatesFirstUser.php index 8a4d5dc882..3df76aa989 100644 --- a/app/Console/Commands/System/CreateFirstUser.php +++ b/app/Console/Commands/System/CreatesFirstUser.php @@ -29,16 +29,13 @@ use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Console\Command; use Illuminate\Support\Facades\Hash; -/** - * Class CreateFirstUser - */ -class CreateFirstUser extends Command +class CreatesFirstUser extends Command { use ShowsFriendlyMessages; protected $description = 'Creates a new user and gives admin rights. Outputs the password on the command line. Strictly for testing.'; - protected $signature = 'firefly-iii:create-first-user {email}'; + protected $signature = 'system:create-first-user {email}'; private UserRepositoryInterface $repository; /** diff --git a/app/Console/Commands/System/ForceDecimalSize.php b/app/Console/Commands/System/ForcesDecimalSize.php similarity index 93% rename from app/Console/Commands/System/ForceDecimalSize.php rename to app/Console/Commands/System/ForcesDecimalSize.php index 426e7fbcbd..66c97cdfdd 100644 --- a/app/Console/Commands/System/ForceDecimalSize.php +++ b/app/Console/Commands/System/ForcesDecimalSize.php @@ -44,13 +44,11 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; /** - * Class ForceDecimalSize - * * This command was inspired by https://github.com/elliot-gh. It will check all amount fields * and their values and correct them to the correct number of decimal places. This fixes issues where * Firefly III would store 0.01 as 0.01000000000000000020816681711721685132943093776702880859375. */ -class ForceDecimalSize extends Command +class ForcesDecimalSize extends Command { use ShowsFriendlyMessages; @@ -83,8 +81,8 @@ class ForceDecimalSize extends Command 'currency_exchange_rates' => ['rate', 'user_rate'], 'limit_repetitions' => ['amount'], 'piggy_bank_events' => ['amount'], - 'piggy_bank_repetitions' => ['currentamount'], - 'piggy_banks' => ['targetamount'], + 'piggy_bank_repetitions' => ['current_amount'], + 'piggy_banks' => ['target_amount'], 'recurrences_transactions' => ['amount', 'foreign_amount'], 'transactions' => ['amount', 'foreign_amount'], ]; @@ -130,17 +128,17 @@ class ForceDecimalSize extends Command private function correctAmounts(): void { // if sqlite, add function? - if ('sqlite' === (string)config('database.default')) { + if ('sqlite' === (string) config('database.default')) { DB::connection()->getPdo()->sqliteCreateFunction('REGEXP', static function ($pattern, $value) { mb_regex_encoding('UTF-8'); $pattern = trim($pattern, '"'); - return (false !== mb_ereg($pattern, (string)$value)) ? 1 : 0; + return (false !== mb_ereg($pattern, (string) $value)) ? 1 : 0; }); } - if (!in_array((string)config('database.default'), ['mysql', 'pgsql', 'sqlite'], true)) { - $this->friendlyWarning(sprintf('Skip correcting amounts, does not support "%s"...', (string)config('database.default'))); + if (!in_array((string) config('database.default'), ['mysql', 'pgsql', 'sqlite'], true)) { + $this->friendlyWarning(sprintf('Skip correcting amounts, does not support "%s"...', (string) config('database.default'))); return; } @@ -236,7 +234,7 @@ class ForceDecimalSize extends Command /** @var Builder $query */ $query = Account::leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') ->where('account_meta.name', 'currency_id') - ->where('account_meta.data', json_encode((string)$currency->id)) + ->where('account_meta.data', json_encode((string) $currency->id)) ; $query->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression): void { foreach ($fields as $field) { @@ -264,7 +262,7 @@ class ForceDecimalSize extends Command } // fix $field by rounding it down correctly. $pow = 10 ** $currency->decimal_places; - $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $correct = bcdiv((string) round($value * $pow), (string) $pow, 12); $this->friendlyInfo(sprintf('Account #%d has %s with value "%s", this has been corrected to "%s".', $account->id, $field, $value, $correct)); Account::find($account->id)->update([$field => $correct]); } @@ -313,7 +311,7 @@ class ForceDecimalSize extends Command } // fix $field by rounding it down correctly. $pow = 10 ** $currency->decimal_places; - $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $correct = bcdiv((string) round($value * $pow), (string) $pow, 12); $this->friendlyWarning(sprintf('%s #%d has %s with value "%s", this has been corrected to "%s".', $table, $item->id, $field, $value, $correct)); $class::find($item->id)->update([$field => $correct]); } @@ -334,7 +332,7 @@ class ForceDecimalSize extends Command ->leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id') ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') ->where('account_meta.name', 'currency_id') - ->where('account_meta.data', json_encode((string)$currency->id)) + ->where('account_meta.data', json_encode((string) $currency->id)) ->where(static function (Builder $q) use ($fields, $currency, $cast, $operator, $regularExpression): void { foreach ($fields as $field) { $q->orWhere( @@ -363,7 +361,7 @@ class ForceDecimalSize extends Command } // fix $field by rounding it down correctly. $pow = 10 ** $currency->decimal_places; - $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $correct = bcdiv((string) round($value * $pow), (string) $pow, 12); $this->friendlyWarning( sprintf('Piggy bank event #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct) ); @@ -387,7 +385,7 @@ class ForceDecimalSize extends Command ->leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id') ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') ->where('account_meta.name', 'currency_id') - ->where('account_meta.data', json_encode((string)$currency->id)) + ->where('account_meta.data', json_encode((string) $currency->id)) ->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression): void { foreach ($fields as $field) { $q->orWhere( @@ -416,7 +414,7 @@ class ForceDecimalSize extends Command } // fix $field by rounding it down correctly. $pow = 10 ** $currency->decimal_places; - $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $correct = bcdiv((string) round($value * $pow), (string) $pow, 12); $this->friendlyWarning( sprintf('Piggy bank repetition #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct) ); @@ -438,7 +436,7 @@ class ForceDecimalSize extends Command $query = PiggyBank::leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id') ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') ->where('account_meta.name', 'currency_id') - ->where('account_meta.data', json_encode((string)$currency->id)) + ->where('account_meta.data', json_encode((string) $currency->id)) ->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression): void { foreach ($fields as $field) { $q->orWhere( @@ -467,7 +465,7 @@ class ForceDecimalSize extends Command } // fix $field by rounding it down correctly. $pow = 10 ** $currency->decimal_places; - $correct = bcdiv((string)round($value * $pow), (string)$pow, 12); + $correct = bcdiv((string) round($value * $pow), (string) $pow, 12); $this->friendlyWarning(sprintf('Piggy bank #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct)); PiggyBank::find($item->id)->update([$field => $correct]); } @@ -499,8 +497,8 @@ class ForceDecimalSize extends Command continue; } // fix $field by rounding it down correctly. - $pow = (float)10 ** $currency->decimal_places; - $correct = bcdiv((string)round((float)$value * $pow), (string)$pow, 12); + $pow = (float) 10 ** $currency->decimal_places; + $correct = bcdiv((string) round((float) $value * $pow), (string) $pow, 12); $this->friendlyWarning(sprintf('Transaction #%d has amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct)); Transaction::find($item->id)->update(['amount' => $correct]); } @@ -527,8 +525,8 @@ class ForceDecimalSize extends Command continue; } // fix $field by rounding it down correctly. - $pow = (float)10 ** $currency->decimal_places; - $correct = bcdiv((string)round((float)$value * $pow), (string)$pow, 12); + $pow = (float) 10 ** $currency->decimal_places; + $correct = bcdiv((string) round((float) $value * $pow), (string) $pow, 12); $this->friendlyWarning( sprintf('Transaction #%d has foreign amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct) ); @@ -539,7 +537,7 @@ class ForceDecimalSize extends Command private function updateDecimals(): void { $this->friendlyInfo('Going to force the size of DECIMAL columns. Please hold.'); - $type = (string)config('database.default'); + $type = (string) config('database.default'); /** * @var string $name diff --git a/app/Console/Commands/System/ForceMigration.php b/app/Console/Commands/System/ForcesMigrations.php similarity index 97% rename from app/Console/Commands/System/ForceMigration.php rename to app/Console/Commands/System/ForcesMigrations.php index 2857c8157d..479031c62e 100644 --- a/app/Console/Commands/System/ForceMigration.php +++ b/app/Console/Commands/System/ForcesMigrations.php @@ -33,10 +33,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; -/** - * Class ForceMigration - */ -class ForceMigration extends Command +class ForcesMigrations extends Command { use ShowsFriendlyMessages; use VerifiesAccessToken; diff --git a/app/Console/Commands/System/UpgradeFireflyInstructions.php b/app/Console/Commands/System/OutputsInstructions.php similarity index 86% rename from app/Console/Commands/System/UpgradeFireflyInstructions.php rename to app/Console/Commands/System/OutputsInstructions.php index 98816cbdca..7842cf1ad6 100644 --- a/app/Console/Commands/System/UpgradeFireflyInstructions.php +++ b/app/Console/Commands/System/OutputsInstructions.php @@ -27,16 +27,13 @@ namespace FireflyIII\Console\Commands\System; use FireflyIII\Support\System\GeneratesInstallationId; use Illuminate\Console\Command; -/** - * Class UpgradeFireflyInstructions. - */ -class UpgradeFireflyInstructions extends Command +class OutputsInstructions extends Command { use GeneratesInstallationId; protected $description = 'Instructions in case of upgrade trouble.'; - protected $signature = 'firefly:instructions {task}'; + protected $signature = 'firefly-iii:instructions {task=install}'; /** * Execute the console command. @@ -59,7 +56,7 @@ class UpgradeFireflyInstructions extends Command */ private function updateInstructions(): void { - $version = (string)config('firefly.version'); + $version = (string) config('firefly.version'); /** @var array $config */ $config = config('upgrade.text.upgrade'); @@ -69,17 +66,17 @@ class UpgradeFireflyInstructions extends Command foreach (array_keys($config) as $compare) { // if string starts with: if (str_starts_with($version, $compare)) { - $text = (string)$config[$compare]; + $text = (string) $config[$compare]; } } // validate some settings. - if ('' === $text && 'local' === (string)config('app.env')) { + if ('' === $text && 'local' === (string) config('app.env')) { $text = 'Please set APP_ENV=production for a safer environment.'; } $prefix = 'v'; - if (str_starts_with($version, 'develop')) { + if (str_starts_with($version, 'develop') || str_starts_with($version, 'branch')) { $prefix = ''; } @@ -94,6 +91,8 @@ class UpgradeFireflyInstructions extends Command $this->boxedInfo('There are no extra upgrade instructions.'); $this->boxed('Firefly III should be ready for use.'); $this->boxed(''); + $this->donationText(); + $this->boxed(''); $this->showLine(); return; @@ -102,6 +101,8 @@ class UpgradeFireflyInstructions extends Command $this->boxed(sprintf('Thank you for updating to Firefly III, %s%s!', $prefix, $version)); $this->boxedInfo($text); $this->boxed(''); + $this->donationText(); + $this->boxed(''); $this->showLine(); } @@ -179,7 +180,7 @@ class UpgradeFireflyInstructions extends Command */ private function installInstructions(): void { - $version = (string)config('firefly.version'); + $version = (string) config('firefly.version'); /** @var array $config */ $config = config('upgrade.text.install'); @@ -189,12 +190,12 @@ class UpgradeFireflyInstructions extends Command foreach (array_keys($config) as $compare) { // if string starts with: if (str_starts_with($version, $compare)) { - $text = (string)$config[$compare]; + $text = (string) $config[$compare]; } } // validate some settings. - if ('' === $text && 'local' === (string)config('app.env')) { + if ('' === $text && 'local' === (string) config('app.env')) { $text = 'Please set APP_ENV=production for a safer environment.'; } @@ -213,6 +214,8 @@ class UpgradeFireflyInstructions extends Command $this->boxedInfo('There are no extra installation instructions.'); $this->boxed('Firefly III should be ready for use.'); $this->boxed(''); + $this->donationText(); + $this->boxed(''); $this->showLine(); return; @@ -221,6 +224,15 @@ class UpgradeFireflyInstructions extends Command $this->boxed(sprintf('Thank you for installing Firefly III, %s%s!', $prefix, $version)); $this->boxedInfo($text); $this->boxed(''); + $this->donationText(); + $this->boxed(''); $this->showLine(); } + + private function donationText(): void + { + $this->boxed('Did you know you can support the development of Firefly III?'); + $this->boxed('You can donate in many ways, like GitHub Sponsors or Patreon.'); + $this->boxed('For more information, please visit https://bit.ly/donate-to-Firefly-III'); + } } diff --git a/app/Console/Commands/System/OutputVersion.php b/app/Console/Commands/System/OutputsVersion.php similarity index 94% rename from app/Console/Commands/System/OutputVersion.php rename to app/Console/Commands/System/OutputsVersion.php index 88835900b8..56050f3cd4 100644 --- a/app/Console/Commands/System/OutputVersion.php +++ b/app/Console/Commands/System/OutputsVersion.php @@ -26,10 +26,7 @@ namespace FireflyIII\Console\Commands\System; use Illuminate\Console\Command; -/** - * Class OutputVersion - */ -class OutputVersion extends Command +class OutputsVersion extends Command { protected $description = 'Outputs the Firefly III version'; diff --git a/app/Console/Commands/System/ScanAttachments.php b/app/Console/Commands/System/ScansAttachments.php similarity index 92% rename from app/Console/Commands/System/ScanAttachments.php rename to app/Console/Commands/System/ScansAttachments.php index 9eb11560da..eac9f9b774 100644 --- a/app/Console/Commands/System/ScanAttachments.php +++ b/app/Console/Commands/System/ScansAttachments.php @@ -29,10 +29,7 @@ use FireflyIII\Models\Attachment; use Illuminate\Console\Command; use Illuminate\Contracts\Encryption\DecryptException; -/** - * Class ScanAttachments. - */ -class ScanAttachments extends Command +class ScansAttachments extends Command { use ShowsFriendlyMessages; @@ -71,8 +68,8 @@ class ScanAttachments extends Command exit(1); } file_put_contents($tempFileName, $decryptedContent); - $attachment->md5 = (string)md5_file($tempFileName); - $attachment->mime = (string)mime_content_type($tempFileName); + $attachment->md5 = (string) md5_file($tempFileName); + $attachment->mime = (string) mime_content_type($tempFileName); $attachment->save(); $this->friendlyInfo(sprintf('Fixed attachment #%d', $attachment->id)); } diff --git a/app/Console/Commands/System/SetLatestVersion.php b/app/Console/Commands/System/SetsLatestVersion.php similarity index 95% rename from app/Console/Commands/System/SetLatestVersion.php rename to app/Console/Commands/System/SetsLatestVersion.php index 71889bd0b0..f6207ee15a 100644 --- a/app/Console/Commands/System/SetLatestVersion.php +++ b/app/Console/Commands/System/SetsLatestVersion.php @@ -27,10 +27,7 @@ namespace FireflyIII\Console\Commands\System; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; -/** - * Class SetLatestVersion - */ -class SetLatestVersion extends Command +class SetsLatestVersion extends Command { use ShowsFriendlyMessages; diff --git a/app/Console/Commands/System/VerifySecurityAlerts.php b/app/Console/Commands/System/VerifySecurityAlerts.php index aa60146808..ce05afa16b 100644 --- a/app/Console/Commands/System/VerifySecurityAlerts.php +++ b/app/Console/Commands/System/VerifySecurityAlerts.php @@ -29,9 +29,6 @@ use Illuminate\Console\Command; use Illuminate\Database\QueryException; use League\Flysystem\FilesystemException; -/** - * Class VerifySecurityAlerts - */ class VerifySecurityAlerts extends Command { use ShowsFriendlyMessages; diff --git a/app/Console/Commands/Tools/ApplyRules.php b/app/Console/Commands/Tools/ApplyRules.php index 5e6e64552f..10cf1d37f0 100644 --- a/app/Console/Commands/Tools/ApplyRules.php +++ b/app/Console/Commands/Tools/ApplyRules.php @@ -40,9 +40,6 @@ use Illuminate\Console\Command; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -/** - * Class ApplyRules - */ class ApplyRules extends Command { use ShowsFriendlyMessages; @@ -128,7 +125,7 @@ class ApplyRules extends Command $ruleEngine->addOperator(['type' => 'account_id', 'value' => $list]); // add the date as a filter: - $ruleEngine->addOperator(['type' => 'date_after', 'value' => $this->startDate->format('Y-m-d')]); + $ruleEngine->addOperator(['type' => 'date_after', 'value' => $this->start_date->format('Y-m-d')]); $ruleEngine->addOperator(['type' => 'date_before', 'value' => $this->endDate->format('Y-m-d')]); // start running rules. @@ -201,7 +198,7 @@ class ApplyRules extends Command $accountRepository = app(AccountRepositoryInterface::class); $accountRepository->setUser($this->getUser()); foreach ($accountList as $accountId) { - $accountId = (int)$accountId; + $accountId = (int) $accountId; $account = $accountRepository->find($accountId); if (null !== $account && in_array($account->accountType->type, $this->acceptedAccounts, true)) { $finalList->push($account); @@ -228,7 +225,7 @@ class ApplyRules extends Command $ruleGroupList = explode(',', $ruleGroupString); foreach ($ruleGroupList as $ruleGroupId) { - $ruleGroup = $this->ruleGroupRepository->find((int)$ruleGroupId); + $ruleGroup = $this->ruleGroupRepository->find((int) $ruleGroupId); if ($ruleGroup->active) { $this->ruleGroupSelection[] = $ruleGroup->id; } @@ -250,7 +247,7 @@ class ApplyRules extends Command $ruleList = explode(',', $ruleString); foreach ($ruleList as $ruleId) { - $rule = $this->ruleRepository->find((int)$ruleId); + $rule = $this->ruleRepository->find((int) $ruleId); if (null !== $rule && $rule->active) { $this->ruleSelection[] = $rule->id; } @@ -265,8 +262,8 @@ class ApplyRules extends Command private function verifyInputDates(): void { // parse start date. - $inputStart = today(config('app.timezone'))->startOfMonth(); - $startString = $this->option('start_date'); + $inputStart = today(config('app.timezone'))->startOfMonth(); + $startString = $this->option('start_date'); if (null === $startString) { /** @var JournalRepositoryInterface $repository */ $repository = app(JournalRepositoryInterface::class); @@ -281,8 +278,8 @@ class ApplyRules extends Command } // parse end date - $inputEnd = today(config('app.timezone')); - $endString = $this->option('end_date'); + $inputEnd = today(config('app.timezone')); + $endString = $this->option('end_date'); if (null !== $endString && '' !== $endString) { $inputEnd = Carbon::createFromFormat('Y-m-d', $endString); } @@ -296,8 +293,8 @@ class ApplyRules extends Command [$inputEnd, $inputStart] = [$inputStart, $inputEnd]; } - $this->startDate = $inputStart; - $this->endDate = $inputEnd; + $this->start_date = $inputStart; + $this->endDate = $inputEnd; } private function grabAllRules(): void diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index 8ee4800de8..a86f187133 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -34,9 +34,6 @@ use FireflyIII\Support\Cronjobs\RecurringCronjob; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; -/** - * Class Cron - */ class Cron extends Command { use ShowsFriendlyMessages; @@ -62,7 +59,7 @@ class Cron extends Command } catch (\InvalidArgumentException $e) { $this->friendlyError(sprintf('"%s" is not a valid date', $this->option('date'))); } - $force = (bool)$this->option('force'); // @phpstan-ignore-line + $force = (bool) $this->option('force'); // @phpstan-ignore-line // Fire exchange rates cron job. if (true === config('cer.download_enabled') && ($doAll || $this->option('download-cer'))) { diff --git a/app/Console/Commands/Upgrade/TransactionIdentifier.php b/app/Console/Commands/Upgrade/AddsTransactionIdentifiers.php similarity index 94% rename from app/Console/Commands/Upgrade/TransactionIdentifier.php rename to app/Console/Commands/Upgrade/AddsTransactionIdentifiers.php index 1aca9a86f7..281078dee2 100644 --- a/app/Console/Commands/Upgrade/TransactionIdentifier.php +++ b/app/Console/Commands/Upgrade/AddsTransactionIdentifiers.php @@ -32,16 +32,13 @@ use FireflyIII\Repositories\Journal\JournalCLIRepositoryInterface; use Illuminate\Console\Command; use Illuminate\Database\QueryException; -/** - * Class TransactionIdentifier - */ -class TransactionIdentifier extends Command +class AddsTransactionIdentifiers extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '480_transaction_identifier'; protected $description = 'Fixes transaction identifiers.'; - protected $signature = 'firefly-iii:transaction-identifiers {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-transaction-identifiers {--F|force : Force the execution of this command.}'; private JournalCLIRepositoryInterface $cliRepository; private int $count; @@ -79,9 +76,6 @@ class TransactionIdentifier extends Command $this->updateJournalIdentifiers($journal); } - if (0 === $this->count) { - $this->friendlyPositive('All split journal transaction identifiers are OK.'); - } if (0 !== $this->count) { $this->friendlyInfo(sprintf('Fixed %d split journal transaction identifier(s).', $this->count)); } @@ -106,7 +100,7 @@ class TransactionIdentifier extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/DecryptDatabase.php b/app/Console/Commands/Upgrade/RemovesDatabaseDecryption.php similarity index 94% rename from app/Console/Commands/Upgrade/DecryptDatabase.php rename to app/Console/Commands/Upgrade/RemovesDatabaseDecryption.php index e4332d5fd5..29f12ff34a 100644 --- a/app/Console/Commands/Upgrade/DecryptDatabase.php +++ b/app/Console/Commands/Upgrade/RemovesDatabaseDecryption.php @@ -30,15 +30,12 @@ use FireflyIII\Models\Preference; use Illuminate\Console\Command; use Illuminate\Contracts\Encryption\DecryptException; -/** - * Class DecryptDatabase - */ -class DecryptDatabase extends Command +class RemovesDatabaseDecryption extends Command { use ShowsFriendlyMessages; protected $description = 'Decrypts the database.'; - protected $signature = 'firefly-iii:decrypt-all'; + protected $signature = 'upgrade:480-decrypt-all'; /** * Execute the console command. @@ -73,7 +70,6 @@ class DecryptDatabase extends Command private function decryptTable(string $table, array $fields): void { if ($this->isDecrypted($table)) { - $this->friendlyInfo(sprintf('No decryption required for table "%s".', $table)); return; } @@ -97,7 +93,7 @@ class DecryptDatabase extends Command app('log')->error($e->getMessage()); } if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; @@ -119,7 +115,7 @@ class DecryptDatabase extends Command if (null === $original) { return; } - $id = (int)$row->id; + $id = (int) $row->id; $value = ''; try { diff --git a/app/Console/Commands/Upgrade/CorrectAccountBalance.php b/app/Console/Commands/Upgrade/RepairsAccountBalances.php similarity index 91% rename from app/Console/Commands/Upgrade/CorrectAccountBalance.php rename to app/Console/Commands/Upgrade/RepairsAccountBalances.php index b377ae0405..670b20995c 100644 --- a/app/Console/Commands/Upgrade/CorrectAccountBalance.php +++ b/app/Console/Commands/Upgrade/RepairsAccountBalances.php @@ -28,16 +28,13 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Support\Models\AccountBalanceCalculator; use Illuminate\Console\Command; -/** - * Class CorrectionSkeleton - */ -class CorrectAccountBalance extends Command +class RepairsAccountBalances extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '610_correct_balances'; protected $description = 'Recalculate all account balance amounts'; - protected $signature = 'firefly-iii:correct-account-balance {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:610-account-balances {--F|force : Force the execution of this command.}'; public function handle(): int { @@ -59,12 +56,6 @@ class CorrectAccountBalance extends Command return 0; } - private function correctBalanceAmounts(): void - { - return; - AccountBalanceCalculator::recalculateAll(true); - } - private function isExecuted(): bool { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); @@ -76,4 +67,10 @@ class CorrectAccountBalance extends Command { app('fireflyconfig')->set(self::CONFIG_NAME, true); } + + private function correctBalanceAmounts(): void + { + return; + AccountBalanceCalculator::recalculateAll(true); + } } diff --git a/app/Console/Commands/Upgrade/FixPostgresSequences.php b/app/Console/Commands/Upgrade/RepairsPostgresSequences.php similarity index 96% rename from app/Console/Commands/Upgrade/FixPostgresSequences.php rename to app/Console/Commands/Upgrade/RepairsPostgresSequences.php index 01f667f490..7d64be8637 100644 --- a/app/Console/Commands/Upgrade/FixPostgresSequences.php +++ b/app/Console/Commands/Upgrade/RepairsPostgresSequences.php @@ -27,16 +27,13 @@ namespace FireflyIII\Console\Commands\Upgrade; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; -/** - * Class FixPostgresSequences - */ -class FixPostgresSequences extends Command +class RepairsPostgresSequences extends Command { use ShowsFriendlyMessages; protected $description = 'Fixes issues with PostgreSQL sequences.'; - protected $signature = 'firefly-iii:fix-pgsql-sequences'; + protected $signature = 'upgrade:600-pgsql-sequences'; /** * Execute the console command. diff --git a/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub b/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub index d2f5b627c0..6a9c3897df 100644 --- a/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub +++ b/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub @@ -4,10 +4,6 @@ namespace FireflyIII\Console\Commands\Upgrade; use Illuminate\Console\Command; -/** - * Class UpgradeSkeleton. - * TODO DONT FORGET TO ADD THIS TO THE DOCKER BUILD - */ class UpgradeSkeleton extends Command { use ShowsFriendlyMessages; @@ -15,7 +11,7 @@ class UpgradeSkeleton extends Command protected $description = 'SOME DESCRIPTION'; - protected $signature = 'firefly-iii:UPGRSKELETON {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:UPGRSKELETON {--F|force : Force the execution of this command.}'; /** * Execute the console command. diff --git a/app/Console/Commands/Upgrade/AccountCurrencies.php b/app/Console/Commands/Upgrade/UpgradesAccountCurrencies.php similarity index 92% rename from app/Console/Commands/Upgrade/AccountCurrencies.php rename to app/Console/Commands/Upgrade/UpgradesAccountCurrencies.php index 3afe9ec47d..89ef7e5b9b 100644 --- a/app/Console/Commands/Upgrade/AccountCurrencies.php +++ b/app/Console/Commands/Upgrade/UpgradesAccountCurrencies.php @@ -36,17 +36,14 @@ use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class AccountCurrencies - */ -class AccountCurrencies extends Command +class UpgradesAccountCurrencies extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '480_account_currencies'; protected $description = 'Give all accounts proper currency info.'; - protected $signature = 'firefly-iii:account-currencies {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-account-currencies {--F|force : Force the execution of this command.}'; private AccountRepositoryInterface $accountRepos; private int $count; private UserRepositoryInterface $userRepos; @@ -65,9 +62,6 @@ class AccountCurrencies extends Command } $this->updateAccountCurrencies(); - if (0 === $this->count) { - $this->friendlyPositive('All account currencies are OK.'); - } if (0 !== $this->count) { $this->friendlyInfo(sprintf('Corrected %d account(s).', $this->count)); } @@ -93,7 +87,7 @@ class AccountCurrencies extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - return (bool)$configVar?->data; + return (bool) $configVar?->data; } private function updateAccountCurrencies(): void @@ -124,9 +118,9 @@ class AccountCurrencies extends Command private function updateAccount(Account $account, TransactionCurrency $currency): void { $this->accountRepos->setUser($account->user); - $accountCurrency = (int)$this->accountRepos->getMetaValue($account, 'currency_id'); + $accountCurrency = (int) $this->accountRepos->getMetaValue($account, 'currency_id'); $openingBalance = $this->accountRepos->getOpeningBalance($account); - $obCurrency = (int)$openingBalance?->transaction_currency_id; + $obCurrency = (int) $openingBalance?->transaction_currency_id; // both 0? set to default currency: if (0 === $accountCurrency && 0 === $obCurrency) { diff --git a/app/Console/Commands/Upgrade/RenameAccountMeta.php b/app/Console/Commands/Upgrade/UpgradesAccountMetaData.php similarity index 88% rename from app/Console/Commands/Upgrade/RenameAccountMeta.php rename to app/Console/Commands/Upgrade/UpgradesAccountMetaData.php index 6602cee886..d181e41e22 100644 --- a/app/Console/Commands/Upgrade/RenameAccountMeta.php +++ b/app/Console/Commands/Upgrade/UpgradesAccountMetaData.php @@ -29,10 +29,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\AccountMeta; use Illuminate\Console\Command; -/** - * Class RenameAccountMeta - */ -class RenameAccountMeta extends Command +class UpgradesAccountMetaData extends Command { use ShowsFriendlyMessages; @@ -40,7 +37,7 @@ class RenameAccountMeta extends Command protected $description = 'Rename account meta-data to new format.'; - protected $signature = 'firefly-iii:rename-account-meta {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-account-meta {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -76,9 +73,6 @@ class RenameAccountMeta extends Command $this->markAsExecuted(); - if (0 === $count) { - $this->friendlyPositive('All account meta is OK.'); - } if (0 !== $count) { $this->friendlyInfo(sprintf('Renamed %d account meta entries (entry).', $count)); } @@ -90,7 +84,7 @@ class RenameAccountMeta extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/MigrateAttachments.php b/app/Console/Commands/Upgrade/UpgradesAttachments.php similarity index 88% rename from app/Console/Commands/Upgrade/MigrateAttachments.php rename to app/Console/Commands/Upgrade/UpgradesAttachments.php index ec0af6a5f8..c3e0d32eb9 100644 --- a/app/Console/Commands/Upgrade/MigrateAttachments.php +++ b/app/Console/Commands/Upgrade/UpgradesAttachments.php @@ -30,10 +30,7 @@ use FireflyIII\Models\Attachment; use FireflyIII\Models\Note; use Illuminate\Console\Command; -/** - * Class MigrateAttachments - */ -class MigrateAttachments extends Command +class UpgradesAttachments extends Command { use ShowsFriendlyMessages; @@ -41,7 +38,7 @@ class MigrateAttachments extends Command protected $description = 'Migrates attachment meta-data.'; - protected $signature = 'firefly-iii:migrate-attachments {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-attachments {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -63,7 +60,7 @@ class MigrateAttachments extends Command /** @var Attachment $att */ foreach ($attachments as $att) { // move description: - $attDescription = (string)$att->description; + $attDescription = (string) $att->description; if ('' !== $attDescription) { // find or create note: $note = $att->notes()->first(); @@ -82,9 +79,6 @@ class MigrateAttachments extends Command ++$count; } } - if (0 === $count) { - $this->friendlyPositive('All attachments are OK.'); - } if (0 !== $count) { $this->friendlyInfo(sprintf('Updated %d attachment(s).', $count)); } @@ -99,7 +93,7 @@ class MigrateAttachments extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/MigrateToRules.php b/app/Console/Commands/Upgrade/UpgradesBillsToRules.php similarity index 88% rename from app/Console/Commands/Upgrade/MigrateToRules.php rename to app/Console/Commands/Upgrade/UpgradesBillsToRules.php index e07100dade..1e2398c2be 100644 --- a/app/Console/Commands/Upgrade/MigrateToRules.php +++ b/app/Console/Commands/Upgrade/UpgradesBillsToRules.php @@ -36,10 +36,7 @@ use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class MigrateToRules - */ -class MigrateToRules extends Command +class UpgradesBillsToRules extends Command { use ShowsFriendlyMessages; @@ -47,7 +44,7 @@ class MigrateToRules extends Command protected $description = 'Migrate bills to rules.'; - protected $signature = 'firefly-iii:bills-to-rules {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-bills-to-rules {--F|force : Force the execution of this command.}'; private BillRepositoryInterface $billRepository; private int $count; private RuleGroupRepositoryInterface $ruleGroupRepository; @@ -76,9 +73,6 @@ class MigrateToRules extends Command $this->migrateUser($user); } - if (0 === $this->count) { - $this->friendlyPositive('All bills are OK.'); - } if (0 !== $this->count) { $this->friendlyInfo(sprintf('Verified and fixed %d bill(s).', $this->count)); } @@ -106,7 +100,7 @@ class MigrateToRules extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; @@ -125,15 +119,15 @@ class MigrateToRules extends Command /** @var Preference $lang */ $lang = app('preferences')->getForUser($user, 'language', 'en_US'); - $language = null !== $lang->data && !is_array($lang->data) ? (string)$lang->data : 'en_US'; - $groupTitle = (string)trans('firefly.rulegroup_for_bills_title', [], $language); + $language = null !== $lang->data && !is_array($lang->data) ? (string) $lang->data : 'en_US'; + $groupTitle = (string) trans('firefly.rulegroup_for_bills_title', [], $language); $ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle); if (null === $ruleGroup) { $ruleGroup = $this->ruleGroupRepository->store( [ - 'title' => (string)trans('firefly.rulegroup_for_bills_title', [], $language), - 'description' => (string)trans('firefly.rulegroup_for_bills_description', [], $language), + 'title' => (string) trans('firefly.rulegroup_for_bills_title', [], $language), + 'description' => (string) trans('firefly.rulegroup_for_bills_description', [], $language), 'active' => true, ] ); @@ -151,7 +145,7 @@ class MigrateToRules extends Command if ('MIGRATED_TO_RULES' === $bill->match) { return; } - $languageString = null !== $language->data && !is_array($language->data) ? (string)$language->data : 'en_US'; + $languageString = null !== $language->data && !is_array($language->data) ? (string) $language->data : 'en_US'; // get match thing: $match = implode(' ', explode(',', $bill->match)); @@ -160,8 +154,8 @@ class MigrateToRules extends Command 'active' => true, 'strict' => false, 'stop_processing' => false, // field is no longer used. - 'title' => (string)trans('firefly.rule_for_bill_title', ['name' => $bill->name], $languageString), - 'description' => (string)trans('firefly.rule_for_bill_description', ['name' => $bill->name], $languageString), + 'title' => (string) trans('firefly.rule_for_bill_title', ['name' => $bill->name], $languageString), + 'description' => (string) trans('firefly.rule_for_bill_description', ['name' => $bill->name], $languageString), 'trigger' => 'store-journal', 'triggers' => [ [ diff --git a/app/Console/Commands/Upgrade/AppendBudgetLimitPeriods.php b/app/Console/Commands/Upgrade/UpgradesBudgetLimitPeriods.php similarity index 90% rename from app/Console/Commands/Upgrade/AppendBudgetLimitPeriods.php rename to app/Console/Commands/Upgrade/UpgradesBudgetLimitPeriods.php index 17d7aa221f..a43f05682d 100644 --- a/app/Console/Commands/Upgrade/AppendBudgetLimitPeriods.php +++ b/app/Console/Commands/Upgrade/UpgradesBudgetLimitPeriods.php @@ -28,10 +28,7 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Models\BudgetLimit; use Illuminate\Console\Command; -/** - * Class AppendBudgetLimitPeriods - */ -class AppendBudgetLimitPeriods extends Command +class UpgradesBudgetLimitPeriods extends Command { use ShowsFriendlyMessages; @@ -39,7 +36,7 @@ class AppendBudgetLimitPeriods extends Command protected $description = 'Append budget limits with their (estimated) timeframe.'; - protected $signature = 'firefly-iii:budget-limit-periods {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:550-budget-limit-periods {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -62,7 +59,7 @@ class AppendBudgetLimitPeriods extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - return (bool)$configVar->data; + return (bool) $configVar->data; } private function theresNoLimit(): void @@ -111,7 +108,7 @@ class AppendBudgetLimitPeriods extends Command return 'daily'; } // is weekly - if ('1' === $limit->start_date->format('N') && '7' === $limit->end_date->format('N') && 6 === (int)$limit->end_date->diffInDays($limit->start_date, true)) { + if ('1' === $limit->start_date->format('N') && '7' === $limit->end_date->format('N') && 6 === (int) $limit->end_date->diffInDays($limit->start_date, true)) { return 'weekly'; } @@ -130,7 +127,7 @@ class AppendBudgetLimitPeriods extends Command if ( in_array($limit->start_date->format('j-n'), $start, true) // start of quarter && in_array($limit->end_date->format('j-n'), $end, true) // end of quarter - && 2 === (int)$limit->start_date->diffInMonths($limit->end_date, true) + && 2 === (int) $limit->start_date->diffInMonths($limit->end_date, true) ) { return 'quarterly'; } @@ -140,7 +137,7 @@ class AppendBudgetLimitPeriods extends Command if ( in_array($limit->start_date->format('j-n'), $start, true) // start of quarter && in_array($limit->end_date->format('j-n'), $end, true) // end of quarter - && 5 === (int)$limit->start_date->diffInMonths($limit->end_date, true) + && 5 === (int) $limit->start_date->diffInMonths($limit->end_date, true) ) { return 'half_year'; } diff --git a/app/Console/Commands/Upgrade/BudgetLimitCurrency.php b/app/Console/Commands/Upgrade/UpgradesBudgetLimits.php similarity index 89% rename from app/Console/Commands/Upgrade/BudgetLimitCurrency.php rename to app/Console/Commands/Upgrade/UpgradesBudgetLimits.php index de9de12029..c94fe0d7dd 100644 --- a/app/Console/Commands/Upgrade/BudgetLimitCurrency.php +++ b/app/Console/Commands/Upgrade/UpgradesBudgetLimits.php @@ -31,10 +31,7 @@ use FireflyIII\Models\BudgetLimit; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class BudgetLimitCurrency - */ -class BudgetLimitCurrency extends Command +class UpgradesBudgetLimits extends Command { use ShowsFriendlyMessages; @@ -42,7 +39,7 @@ class BudgetLimitCurrency extends Command protected $description = 'Give budget limits a currency'; - protected $signature = 'firefly-iii:bl-currency {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-budget-limit-currencies {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -80,9 +77,6 @@ class BudgetLimitCurrency extends Command } } } - if (0 === $count) { - $this->friendlyPositive('All budget limits are OK.'); - } $this->markAsExecuted(); return 0; @@ -92,7 +86,7 @@ class BudgetLimitCurrency extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/CCLiabilities.php b/app/Console/Commands/Upgrade/UpgradesCreditCardLiabilities.php similarity index 88% rename from app/Console/Commands/Upgrade/CCLiabilities.php rename to app/Console/Commands/Upgrade/UpgradesCreditCardLiabilities.php index 201453e66a..f8db7c06a9 100644 --- a/app/Console/Commands/Upgrade/CCLiabilities.php +++ b/app/Console/Commands/Upgrade/UpgradesCreditCardLiabilities.php @@ -31,16 +31,13 @@ use FireflyIII\Models\AccountType; use Illuminate\Console\Command; use Illuminate\Support\Collection; -/** - * Class CCLiabilities - */ -class CCLiabilities extends Command +class UpgradesCreditCardLiabilities extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '480_cc_liabilities'; protected $description = 'Convert old credit card liabilities.'; - protected $signature = 'firefly-iii:cc-liabilities {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-cc-liabilities {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -58,7 +55,6 @@ class CCLiabilities extends Command $ccType = AccountType::where('type', AccountType::CREDITCARD)->first(); $debtType = AccountType::where('type', AccountType::DEBT)->first(); if (null === $ccType || null === $debtType) { - $this->friendlyPositive('No incorrectly stored credit card liabilities.'); $this->markAsExecuted(); return 0; @@ -76,9 +72,6 @@ class CCLiabilities extends Command 'Credit card liability types are no longer supported and have been converted to generic debts. See: https://bit.ly/FF3-credit-cards' ); } - if (0 === $accounts->count()) { - $this->friendlyPositive('No incorrectly stored credit card liabilities.'); - } $this->markAsExecuted(); return 0; @@ -88,7 +81,7 @@ class CCLiabilities extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - return (bool)$configVar?->data; + return (bool) $configVar?->data; } private function markAsExecuted(): void diff --git a/app/Console/Commands/Upgrade/UpgradeCurrencyPreferences.php b/app/Console/Commands/Upgrade/UpgradesCurrencyPreferences.php similarity index 93% rename from app/Console/Commands/Upgrade/UpgradeCurrencyPreferences.php rename to app/Console/Commands/Upgrade/UpgradesCurrencyPreferences.php index ff59b5169c..50ed8b6ff3 100644 --- a/app/Console/Commands/Upgrade/UpgradeCurrencyPreferences.php +++ b/app/Console/Commands/Upgrade/UpgradesCurrencyPreferences.php @@ -32,10 +32,7 @@ use FireflyIII\User; use Illuminate\Console\Command; use Illuminate\Support\Collection; -/** - * Class UpgradeCurrencyPreferences - */ -class UpgradeCurrencyPreferences extends Command +class UpgradesCurrencyPreferences extends Command { use ShowsFriendlyMessages; @@ -43,7 +40,7 @@ class UpgradeCurrencyPreferences extends Command protected $description = 'Upgrade user currency preferences'; - protected $signature = 'firefly-iii:upgrade-currency-preferences {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:610-currency-preferences {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -68,7 +65,7 @@ class UpgradeCurrencyPreferences extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; @@ -138,7 +135,7 @@ class UpgradeCurrencyPreferences extends Command } if (null !== $preference->data && !is_array($preference->data)) { - return (string)$preference->data; + return (string) $preference->data; } return 'EUR'; diff --git a/app/Console/Commands/Upgrade/UpgradeDatabase.php b/app/Console/Commands/Upgrade/UpgradesDatabase.php similarity index 55% rename from app/Console/Commands/Upgrade/UpgradeDatabase.php rename to app/Console/Commands/Upgrade/UpgradesDatabase.php index 5aea98ad90..b09c15f657 100644 --- a/app/Console/Commands/Upgrade/UpgradeDatabase.php +++ b/app/Console/Commands/Upgrade/UpgradesDatabase.php @@ -29,10 +29,7 @@ set_time_limit(0); use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; -/** - * Class UpgradeDatabase - */ -class UpgradeDatabase extends Command +class UpgradesDatabase extends Command { use ShowsFriendlyMessages; @@ -46,32 +43,29 @@ class UpgradeDatabase extends Command { $this->callInitialCommands(); $commands = [ - 'firefly-iii:transaction-identifiers', - 'firefly-iii:migrate-to-groups', - 'firefly-iii:account-currencies', - 'firefly-iii:transfer-currencies', - 'firefly-iii:other-currencies', - 'firefly-iii:migrate-notes', - 'firefly-iii:migrate-attachments', - 'firefly-iii:bills-to-rules', - 'firefly-iii:bl-currency', - 'firefly-iii:cc-liabilities', - 'firefly-iii:back-to-journals', - 'firefly-iii:rename-account-meta', - 'firefly-iii:migrate-recurrence-meta', - 'firefly-iii:migrate-tag-locations', - 'firefly-iii:migrate-recurrence-type', - 'firefly-iii:upgrade-liabilities', - 'firefly-iii:liabilities-600', - 'firefly-iii:budget-limit-periods', - 'firefly-iii:migrate-rule-actions', - 'firefly-iii:restore-oauth-keys', - 'firefly-iii:correct-account-balance', - // also just in case, some integrity commands: - 'firefly-iii:add-timezones-to-dates', - 'firefly-iii:create-group-memberships', - 'firefly-iii:upgrade-group-information', - 'firefly-iii:upgrade-currency-preferences', + 'upgrade:480-transaction-identifiers', + 'upgrade:480-migrate-to-groups', + 'upgrade:480-account-currencies', + 'upgrade:480-transfer-currencies', + 'upgrade:480-currency-information', + 'upgrade:480-notes', + 'upgrade:480-attachments', + 'upgrade:480-bills-to-rules', + 'upgrade:480-budget-limit-currencies', + 'upgrade:480-cc-liabilities', + 'upgrade:480-journal-meta-data', + 'upgrade:480-account-meta', + 'upgrade:481-recurrence-meta', + 'upgrade:500-tag-locations', + 'upgrade:560-liabilities', + 'upgrade:600-liabilities', + 'upgrade:550-budget-limit-periods', + 'upgrade:600-rule-actions', + 'upgrade:610-account-balance', + 'upgrade:610-currency-preferences', + 'upgrade:610-currency-preferences', + 'upgrade:620-piggy-banks', + 'upgrade:620-native-amounts', 'firefly-iii:correct-database', ]; $args = []; @@ -83,9 +77,9 @@ class UpgradeDatabase extends Command $this->call($command, $args); } // set new DB version. - app('fireflyconfig')->set('db_version', (int)config('firefly.db_version')); + app('fireflyconfig')->set('db_version', (int) config('firefly.db_version')); // index will set FF3 version. - app('fireflyconfig')->set('ff3_version', (string)config('firefly.version')); + app('fireflyconfig')->set('ff3_version', (string) config('firefly.version')); return 0; } @@ -93,7 +87,7 @@ class UpgradeDatabase extends Command private function callInitialCommands(): void { $this->call('migrate', ['--seed' => true, '--force' => true, '--no-interaction' => true]); - $this->call('firefly-iii:fix-pgsql-sequences'); - $this->call('firefly-iii:decrypt-all'); + $this->call('upgrade:600-pgsql-sequences'); + $this->call('upgrade:480-decrypt-all'); } } diff --git a/app/Console/Commands/Upgrade/BackToJournals.php b/app/Console/Commands/Upgrade/UpgradesJournalMetaData.php similarity index 95% rename from app/Console/Commands/Upgrade/BackToJournals.php rename to app/Console/Commands/Upgrade/UpgradesJournalMetaData.php index 961c24040d..e6145dc023 100644 --- a/app/Console/Commands/Upgrade/BackToJournals.php +++ b/app/Console/Commands/Upgrade/UpgradesJournalMetaData.php @@ -32,10 +32,7 @@ use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; use Illuminate\Support\Collection; -/** - * Class BackToJournals - */ -class BackToJournals extends Command +class UpgradesJournalMetaData extends Command { use ShowsFriendlyMessages; @@ -43,7 +40,7 @@ class BackToJournals extends Command protected $description = 'Move meta data back to journals, not individual transactions.'; - protected $signature = 'firefly-iii:back-to-journals {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-journal-meta-data {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -71,16 +68,16 @@ class BackToJournals extends Command private function isMigrated(): bool { - $configVar = app('fireflyconfig')->get(MigrateToGroups::CONFIG_NAME, false); + $configVar = app('fireflyconfig')->get(UpgradesToGroups::CONFIG_NAME, false); - return (bool)$configVar->data; + return (bool) $configVar->data; } private function isExecuted(): bool { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); - return (bool)$configVar->data; + return (bool) $configVar->data; } private function migrateAll(): void diff --git a/app/Console/Commands/Upgrade/MigrateJournalNotes.php b/app/Console/Commands/Upgrade/UpgradesJournalNotes.php similarity index 89% rename from app/Console/Commands/Upgrade/MigrateJournalNotes.php rename to app/Console/Commands/Upgrade/UpgradesJournalNotes.php index 4f472070f1..788cf1f6c1 100644 --- a/app/Console/Commands/Upgrade/MigrateJournalNotes.php +++ b/app/Console/Commands/Upgrade/UpgradesJournalNotes.php @@ -29,10 +29,7 @@ use FireflyIII\Models\Note; use FireflyIII\Models\TransactionJournalMeta; use Illuminate\Console\Command; -/** - * Class MigrateJournalNotes - */ -class MigrateJournalNotes extends Command +class UpgradesJournalNotes extends Command { use ShowsFriendlyMessages; @@ -40,7 +37,7 @@ class MigrateJournalNotes extends Command protected $description = 'Migrate notes for transaction journals.'; - protected $signature = 'firefly-iii:migrate-notes {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-notes {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -75,9 +72,6 @@ class MigrateJournalNotes extends Command ++$count; } - if (0 === $count) { - $this->friendlyPositive('No notes to migrate.'); - } if (0 !== $count) { $this->friendlyInfo(sprintf('Migrated %d note(s).', $count)); } @@ -93,7 +87,7 @@ class MigrateJournalNotes extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilities.php b/app/Console/Commands/Upgrade/UpgradesLiabilities.php similarity index 95% rename from app/Console/Commands/Upgrade/UpgradeLiabilities.php rename to app/Console/Commands/Upgrade/UpgradesLiabilities.php index 0f36506a8e..c75ccc6a99 100644 --- a/app/Console/Commands/Upgrade/UpgradeLiabilities.php +++ b/app/Console/Commands/Upgrade/UpgradesLiabilities.php @@ -34,16 +34,13 @@ use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class UpgradeLiabilities - */ -class UpgradeLiabilities extends Command +class UpgradesLiabilities extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '560_upgrade_liabilities'; protected $description = 'Upgrade liabilities to new 5.6.0 structure.'; - protected $signature = 'firefly-iii:upgrade-liabilities {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:560-liabilities {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -66,7 +63,7 @@ class UpgradeLiabilities extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php b/app/Console/Commands/Upgrade/UpgradesLiabilitiesEight.php similarity index 84% rename from app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php rename to app/Console/Commands/Upgrade/UpgradesLiabilitiesEight.php index 0fb2c9bb84..a28fb86324 100644 --- a/app/Console/Commands/Upgrade/UpgradeLiabilitiesEight.php +++ b/app/Console/Commands/Upgrade/UpgradesLiabilitiesEight.php @@ -35,16 +35,13 @@ use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\User; use Illuminate\Console\Command; -/** - * Class UpgradeLiabilitiesEight - */ -class UpgradeLiabilitiesEight extends Command +class UpgradesLiabilitiesEight extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '600_upgrade_liabilities'; protected $description = 'Upgrade liabilities to new 6.0.0 structure.'; - protected $signature = 'firefly-iii:liabilities-600 {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:600-liabilities {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -66,7 +63,7 @@ class UpgradeLiabilitiesEight extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; @@ -201,32 +198,9 @@ class UpgradeLiabilitiesEight extends Command /** @var TransactionJournal $journal */ foreach ($journals as $journal) { - // $delete = false; - // /** @var Transaction $source */ - // $source = $journal->transactions()->where('amount', '<', 0)->first(); - // /** @var Transaction $dest */ - // $dest = $journal->transactions()->where('amount', '>', 0)->first(); - - /** - * // if source is this liability and destination is expense, remove transaction. - * // if source is revenue and destination is liability, remove transaction. - * if ($source->account_id === $account->id && $dest->account->accountType->type === AccountType::EXPENSE) { - * $delete = true; - * } - * if ($dest->account_id === $account->id && $source->account->accountType->type === AccountType::REVENUE) { - * $delete = true; - * } - * - * // overruled. No transaction will be deleted, ever. - * // code is kept in place, so I can revisit my reasoning. - * $delete = false; - */ - - // if ($delete) { $service = app(TransactionGroupDestroyService::class); $service->destroy($journal->transactionGroup); ++$count; - // } } return $count; diff --git a/app/Console/Commands/Upgrade/UpgradesMultiPiggyBanks.php b/app/Console/Commands/Upgrade/UpgradesMultiPiggyBanks.php new file mode 100644 index 0000000000..9284ca913a --- /dev/null +++ b/app/Console/Commands/Upgrade/UpgradesMultiPiggyBanks.php @@ -0,0 +1,111 @@ +isExecuted() && true !== $this->option('force')) { + $this->friendlyInfo('This command has already been executed.'); + + return 0; + } + $this->upgradePiggyBanks(); + $this->friendlyInfo('Upgraded all piggy banks.'); + + $this->markAsExecuted(); + + return 0; + } + + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool) $configVar->data; + } + + return false; + } + + private function upgradePiggyBanks(): void + { + $this->repository = app(PiggyBankRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + $set = PiggyBank::whereNotNull('account_id')->get(); + Log::debug(sprintf('Will update %d piggy banks(s).', $set->count())); + + /** @var PiggyBank $piggyBank */ + foreach ($set as $piggyBank) { + $this->upgradePiggyBank($piggyBank); + } + } + + private function upgradePiggyBank(PiggyBank $piggyBank): void + { + $this->repository->setUser($piggyBank->account->user); + $this->accountRepository->setUser($piggyBank->account->user); + $repetition = $this->repository->getRepetition($piggyBank, true); + $currency = $this->accountRepository->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrencyByUserGroup($piggyBank->account->user->userGroup); + + // update piggy bank to have a currency. + $piggyBank->transaction_currency_id = $currency->id; + $piggyBank->saveQuietly(); + + // store current amount in account association. + $piggyBank->accounts()->sync([$piggyBank->account->id => ['current_amount' => $repetition->current_amount]]); + $piggyBank->account_id = null; + $piggyBank->saveQuietly(); + + // remove all repetitions (no longer used) + $piggyBank->piggyBankRepetitions()->delete(); + + } + + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } +} diff --git a/app/Console/Commands/Upgrade/MigrateRecurrenceType.php b/app/Console/Commands/Upgrade/UpgradesNativeAmounts.php similarity index 68% rename from app/Console/Commands/Upgrade/MigrateRecurrenceType.php rename to app/Console/Commands/Upgrade/UpgradesNativeAmounts.php index 7aec519d9c..b532d32adb 100644 --- a/app/Console/Commands/Upgrade/MigrateRecurrenceType.php +++ b/app/Console/Commands/Upgrade/UpgradesNativeAmounts.php @@ -1,8 +1,9 @@ . + * along with this program. If not, see https://www.gnu.org/licenses/. */ -declare(strict_types=1); - namespace FireflyIII\Console\Commands\Upgrade; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Artisan; -/** - * Class MigrateRecurrenceType - */ -class MigrateRecurrenceType extends Command +class UpgradesNativeAmounts extends Command { use ShowsFriendlyMessages; + public const string CONFIG_NAME = '620_native_amounts'; - public const string CONFIG_NAME = '550_migrate_recurrence_type'; + protected $description = 'Runs the native amounts calculations.'; - protected $description = 'Migrate transaction type of recurring transaction.'; - - protected $signature = 'firefly-iii:migrate-recurrence-type {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:620-native-amounts {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -50,17 +46,23 @@ class MigrateRecurrenceType extends Command return 0; } - $this->friendlyWarning('This command has been disabled.'); + + Artisan::call('correction:recalculate-native-amounts'); + $this->markAsExecuted(); + return 0; } private function isExecuted(): bool { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } - return (bool)$configVar?->data; + return false; } private function markAsExecuted(): void diff --git a/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php b/app/Console/Commands/Upgrade/UpgradesRecurrenceMetaData.php similarity index 90% rename from app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php rename to app/Console/Commands/Upgrade/UpgradesRecurrenceMetaData.php index 207e45d1fa..d20f763945 100644 --- a/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php +++ b/app/Console/Commands/Upgrade/UpgradesRecurrenceMetaData.php @@ -30,10 +30,7 @@ use FireflyIII\Models\RecurrenceMeta; use FireflyIII\Models\RecurrenceTransactionMeta; use Illuminate\Console\Command; -/** - * Class MigrateRecurrenceMeta - */ -class MigrateRecurrenceMeta extends Command +class UpgradesRecurrenceMetaData extends Command { use ShowsFriendlyMessages; @@ -41,7 +38,7 @@ class MigrateRecurrenceMeta extends Command protected $description = 'Migrate recurrence meta data'; - protected $signature = 'firefly-iii:migrate-recurrence-meta {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:481-recurrence-meta {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -55,9 +52,6 @@ class MigrateRecurrenceMeta extends Command } $count = $this->migrateMetaData(); - if (0 === $count) { - $this->friendlyPositive('No recurrence meta data migrated.'); - } if ($count > 0) { $this->friendlyInfo(sprintf('Migrated %d meta data entries', $count)); } @@ -71,7 +65,7 @@ class MigrateRecurrenceMeta extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/MigrateRuleActions.php b/app/Console/Commands/Upgrade/UpgradesRuleActions.php similarity index 97% rename from app/Console/Commands/Upgrade/MigrateRuleActions.php rename to app/Console/Commands/Upgrade/UpgradesRuleActions.php index d6fcd62407..c6341238ae 100644 --- a/app/Console/Commands/Upgrade/MigrateRuleActions.php +++ b/app/Console/Commands/Upgrade/UpgradesRuleActions.php @@ -27,7 +27,7 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Models\RuleAction; use Illuminate\Console\Command; -class MigrateRuleActions extends Command +class UpgradesRuleActions extends Command { use ShowsFriendlyMessages; @@ -35,7 +35,7 @@ class MigrateRuleActions extends Command protected $description = 'Migrate rule actions away from expression engine'; - protected $signature = 'firefly-iii:migrate-rule-actions {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:600-rule-actions {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -63,7 +63,7 @@ class MigrateRuleActions extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/MigrateTagLocations.php b/app/Console/Commands/Upgrade/UpgradesTagLocations.php similarity index 91% rename from app/Console/Commands/Upgrade/MigrateTagLocations.php rename to app/Console/Commands/Upgrade/UpgradesTagLocations.php index 76a7f81a9e..a6fe336a49 100644 --- a/app/Console/Commands/Upgrade/MigrateTagLocations.php +++ b/app/Console/Commands/Upgrade/UpgradesTagLocations.php @@ -29,10 +29,7 @@ use FireflyIII\Models\Location; use FireflyIII\Models\Tag; use Illuminate\Console\Command; -/** - * Class MigrateTagLocations - */ -class MigrateTagLocations extends Command +class UpgradesTagLocations extends Command { use ShowsFriendlyMessages; @@ -40,7 +37,7 @@ class MigrateTagLocations extends Command protected $description = 'Migrate tag locations.'; - protected $signature = 'firefly-iii:migrate-tag-locations {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:500-tag-locations {--F|force : Force the execution of this command.}'; /** * Execute the console command. @@ -62,7 +59,7 @@ class MigrateTagLocations extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; diff --git a/app/Console/Commands/Upgrade/MigrateToGroups.php b/app/Console/Commands/Upgrade/UpgradesToGroups.php similarity index 95% rename from app/Console/Commands/Upgrade/MigrateToGroups.php rename to app/Console/Commands/Upgrade/UpgradesToGroups.php index d19c1bef60..bf3113f788 100644 --- a/app/Console/Commands/Upgrade/MigrateToGroups.php +++ b/app/Console/Commands/Upgrade/UpgradesToGroups.php @@ -36,20 +36,13 @@ use FireflyIII\Services\Internal\Destroy\JournalDestroyService; use Illuminate\Console\Command; use Illuminate\Support\Collection; -/** - * This command will take split transactions and migrate them to "transaction groups". - * - * It will only run once, but can be forced to run again. - * - * Class MigrateToGroups - */ -class MigrateToGroups extends Command +class UpgradesToGroups extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '480_migrated_to_groups'; protected $description = 'Migrates a pre-4.7.8 transaction structure to the 4.7.8+ transaction structure.'; - protected $signature = 'firefly-iii:migrate-to-groups {--F|force : Force the migration, even if it fired before.}'; + protected $signature = 'upgrade:480-migrate-to-groups {--F|force : Force the migration, even if it fired before.}'; private JournalCLIRepositoryInterface $cliRepository; private int $count; private TransactionGroupFactory $groupFactory; @@ -79,9 +72,6 @@ class MigrateToGroups extends Command if (0 !== $this->count) { $this->friendlyInfo(sprintf('Migrated %d transaction journal(s).', $this->count)); } - if (0 === $this->count) { - $this->friendlyPositive('No journals to migrate to groups.'); - } $this->markAsMigrated(); return 0; @@ -105,7 +95,7 @@ class MigrateToGroups extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; @@ -288,7 +278,7 @@ class MigrateToGroups extends Command { $set = $journal->transactions->filter( static function (Transaction $subject) use ($transaction) { - $amount = (float)$transaction->amount * -1 === (float)$subject->amount; // intentional float + $amount = (float) $transaction->amount * -1 === (float) $subject->amount; // intentional float $identifier = $transaction->identifier === $subject->identifier; app('log')->debug(sprintf('Amount the same? %s', var_export($amount, true))); app('log')->debug(sprintf('ID the same? %s', var_export($identifier, true))); @@ -370,9 +360,6 @@ class MigrateToGroups extends Command $this->giveGroup($array); } } - if (0 === $total) { - $this->friendlyPositive('No need to convert transaction journals.'); - } } private function giveGroup(array $array): void diff --git a/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php b/app/Console/Commands/Upgrade/UpgradesTransferCurrencies.php similarity index 96% rename from app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php rename to app/Console/Commands/Upgrade/UpgradesTransferCurrencies.php index fcc30b1079..a16fe46351 100644 --- a/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php +++ b/app/Console/Commands/Upgrade/UpgradesTransferCurrencies.php @@ -34,16 +34,13 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Journal\JournalCLIRepositoryInterface; use Illuminate\Console\Command; -/** - * Class TransferCurrenciesCorrections - */ -class TransferCurrenciesCorrections extends Command +class UpgradesTransferCurrencies extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '480_transfer_currencies'; protected $description = 'Updates transfer currency information.'; - protected $signature = 'firefly-iii:transfer-currencies {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-transfer-currencies {--F|force : Force the execution of this command.}'; private array $accountCurrencies; private AccountRepositoryInterface $accountRepos; private JournalCLIRepositoryInterface $cliRepos; @@ -71,15 +68,10 @@ class TransferCurrenciesCorrections extends Command $this->startUpdateRoutine(); $this->markAsExecuted(); - - if (0 === $this->count) { - $this->friendlyPositive('All transfers have correct currency information.'); - - return 0; + if ($this->count > 0) { + $this->friendlyInfo(sprintf('Verified currency information of %d transfer(s).', $this->count)); } - $this->friendlyInfo(sprintf('Verified currency information of %d transfer(s).', $this->count)); - return 0; } @@ -114,7 +106,7 @@ class TransferCurrenciesCorrections extends Command { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { - return (bool)$configVar->data; + return (bool) $configVar->data; } return false; @@ -320,7 +312,7 @@ class TransferCurrenciesCorrections extends Command { if (null !== $this->sourceCurrency && null === $this->sourceTransaction->foreign_amount - && (int)$this->sourceTransaction->transaction_currency_id !== $this->sourceCurrency->id + && (int) $this->sourceTransaction->transaction_currency_id !== $this->sourceCurrency->id ) { $message = sprintf( 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', @@ -366,7 +358,7 @@ class TransferCurrenciesCorrections extends Command { if (null !== $this->destinationCurrency && null === $this->destinationTransaction->foreign_amount - && (int)$this->destinationTransaction->transaction_currency_id !== $this->destinationCurrency->id + && (int) $this->destinationTransaction->transaction_currency_id !== $this->destinationCurrency->id ) { $message = sprintf( 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', @@ -469,7 +461,7 @@ class TransferCurrenciesCorrections extends Command */ private function fixTransactionJournalCurrency(TransactionJournal $journal): void { - if ((int)$journal->transaction_currency_id !== $this->sourceCurrency->id) { + if ((int) $journal->transaction_currency_id !== $this->sourceCurrency->id) { $oldCurrencyCode = $journal->transactionCurrency->code ?? '(nothing)'; $journal->transaction_currency_id = $this->sourceCurrency->id; $message = sprintf( diff --git a/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php b/app/Console/Commands/Upgrade/UpgradesVariousCurrencyInformation.php similarity index 97% rename from app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php rename to app/Console/Commands/Upgrade/UpgradesVariousCurrencyInformation.php index cfff687d28..9e2ecf0b53 100644 --- a/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php +++ b/app/Console/Commands/Upgrade/UpgradesVariousCurrencyInformation.php @@ -36,16 +36,13 @@ use FireflyIII\Repositories\Journal\JournalCLIRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use Illuminate\Console\Command; -/** - * Class OtherCurrenciesCorrections - */ -class OtherCurrenciesCorrections extends Command +class UpgradesVariousCurrencyInformation extends Command { use ShowsFriendlyMessages; public const string CONFIG_NAME = '480_other_currencies'; protected $description = 'Update all journal currency information.'; - protected $signature = 'firefly-iii:other-currencies {--F|force : Force the execution of this command.}'; + protected $signature = 'upgrade:480-currency-information {--F|force : Force the execution of this command.}'; private array $accountCurrencies; private AccountRepositoryInterface $accountRepos; private JournalCLIRepositoryInterface $cliRepos; @@ -239,11 +236,6 @@ class OtherCurrenciesCorrections extends Command return $currency; } - private function markAsExecuted(): void - { - app('fireflyconfig')->set(self::CONFIG_NAME, true); - } - private function isMultiCurrency(Account $account): bool { $value = $this->accountRepos->getMetaValue($account, 'is_multi_currency', false); @@ -253,4 +245,9 @@ class OtherCurrenciesCorrections extends Command return '1' === $value; } + + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } } diff --git a/app/Console/Commands/VerifiesAccessToken.php b/app/Console/Commands/VerifiesAccessToken.php index 0602f89683..9b9d3d06c2 100644 --- a/app/Console/Commands/VerifiesAccessToken.php +++ b/app/Console/Commands/VerifiesAccessToken.php @@ -40,7 +40,7 @@ trait VerifiesAccessToken */ public function getUser(): User { - $userId = (int)$this->option('user'); + $userId = (int) $this->option('user'); /** @var UserRepositoryInterface $repository */ $repository = app(UserRepositoryInterface::class); @@ -68,8 +68,8 @@ trait VerifiesAccessToken */ protected function verifyAccessToken(): bool { - $userId = (int)$this->option('user'); - $token = (string)$this->option('token'); + $userId = (int) $this->option('user'); + $token = (string) $this->option('token'); /** @var UserRepositoryInterface $repository */ $repository = app(UserRepositoryInterface::class); diff --git a/app/Entities/AccountBalance.php b/app/Entities/AccountBalance.php index 802017cbca..2f27ee183c 100644 --- a/app/Entities/AccountBalance.php +++ b/app/Entities/AccountBalance.php @@ -28,9 +28,9 @@ use FireflyIII\Models\Account; class AccountBalance { - public string $id; public string $amount; public string $currencyId; + public string $id; public static function fromArray(): self { diff --git a/app/Events/DetectedNewIPAddress.php b/app/Events/DetectedNewIPAddress.php index 6438b4c480..a04d65cb7c 100644 --- a/app/Events/DetectedNewIPAddress.php +++ b/app/Events/DetectedNewIPAddress.php @@ -34,15 +34,13 @@ class DetectedNewIPAddress extends Event { use SerializesModels; - public string $ipAddress; - public User $user; + public User $user; /** * Create a new event instance. This event is triggered when a new user registers. */ - public function __construct(User $user, string $ipAddress) + public function __construct(User $user) { - $this->ipAddress = $ipAddress; - $this->user = $user; + $this->user = $user; } } diff --git a/app/Events/Model/Account/Updated.php b/app/Events/Model/Account/Updated.php new file mode 100644 index 0000000000..8f86723bd3 --- /dev/null +++ b/app/Events/Model/Account/Updated.php @@ -0,0 +1,40 @@ +account = $account; + } +} diff --git a/app/Events/NewVersionAvailable.php b/app/Events/NewVersionAvailable.php index db3626b490..5d08de0d71 100644 --- a/app/Events/NewVersionAvailable.php +++ b/app/Events/NewVersionAvailable.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Events; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; /** * Class NewVersionAvailable @@ -40,6 +41,7 @@ class NewVersionAvailable extends Event */ public function __construct(string $message) { + Log::debug(__METHOD__); $this->message = $message; } } diff --git a/app/Events/Preferences/UserGroupChangedDefaultCurrency.php b/app/Events/Preferences/UserGroupChangedDefaultCurrency.php new file mode 100644 index 0000000000..baa392f605 --- /dev/null +++ b/app/Events/Preferences/UserGroupChangedDefaultCurrency.php @@ -0,0 +1,43 @@ +userGroup = $userGroup; + } +} diff --git a/app/Events/RegisteredUser.php b/app/Events/RegisteredUser.php index 0a4d116d8f..51539a856a 100644 --- a/app/Events/RegisteredUser.php +++ b/app/Events/RegisteredUser.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Events; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\User; use Illuminate\Queue\SerializesModels; @@ -34,13 +35,15 @@ class RegisteredUser extends Event { use SerializesModels; - public User $user; + public OwnerNotifiable $owner; + public User $user; /** * Create a new event instance. This event is triggered when a new user registers. */ - public function __construct(User $user) + public function __construct(OwnerNotifiable $owner, User $user) { - $this->user = $user; + $this->user = $user; + $this->owner = $owner; } } diff --git a/app/Events/Security/MFABackupFewLeft.php b/app/Events/Security/MFABackupFewLeft.php index f992bcae31..dd1129c7d3 100644 --- a/app/Events/Security/MFABackupFewLeft.php +++ b/app/Events/Security/MFABackupFewLeft.php @@ -33,8 +33,8 @@ class MFABackupFewLeft extends Event { use SerializesModels; + public int $count; public User $user; - public int $count; public function __construct(null|Authenticatable|User $user, int $count) { diff --git a/app/Events/Security/MFAManyFailedAttempts.php b/app/Events/Security/MFAManyFailedAttempts.php index b41f47933f..6363dc3a3d 100644 --- a/app/Events/Security/MFAManyFailedAttempts.php +++ b/app/Events/Security/MFAManyFailedAttempts.php @@ -33,8 +33,8 @@ class MFAManyFailedAttempts extends Event { use SerializesModels; + public int $count; public User $user; - public int $count; public function __construct(null|Authenticatable|User $user, int $count) { diff --git a/app/JsonApi/V2/Accounts/Capabilities/CrudAccountRelations.php b/app/Events/Security/UnknownUserAttemptedLogin.php similarity index 73% rename from app/JsonApi/V2/Accounts/Capabilities/CrudAccountRelations.php rename to app/Events/Security/UnknownUserAttemptedLogin.php index 0386b9b62d..871b0b7316 100644 --- a/app/JsonApi/V2/Accounts/Capabilities/CrudAccountRelations.php +++ b/app/Events/Security/UnknownUserAttemptedLogin.php @@ -1,7 +1,7 @@ address = $address; + } +} diff --git a/app/Events/Security/UserAttemptedLogin.php b/app/Events/Security/UserAttemptedLogin.php new file mode 100644 index 0000000000..6313bcbc35 --- /dev/null +++ b/app/Events/Security/UserAttemptedLogin.php @@ -0,0 +1,44 @@ +user = $user; + } + } +} diff --git a/app/Events/Test/OwnerTestNotificationChannel.php b/app/Events/Test/OwnerTestNotificationChannel.php new file mode 100644 index 0000000000..c246520c2d --- /dev/null +++ b/app/Events/Test/OwnerTestNotificationChannel.php @@ -0,0 +1,46 @@ +debug(sprintf('Triggered OwnerTestNotificationChannel("%s")', $channel)); + $this->owner = $owner; + $this->channel = $channel; + } +} diff --git a/app/Events/AdminRequestedTestMessage.php b/app/Events/Test/UserTestNotificationChannel.php similarity index 63% rename from app/Events/AdminRequestedTestMessage.php rename to app/Events/Test/UserTestNotificationChannel.php index 4f32c7841c..aff68cc409 100644 --- a/app/Events/AdminRequestedTestMessage.php +++ b/app/Events/Test/UserTestNotificationChannel.php @@ -1,8 +1,8 @@ . + * along with this program. If not, see https://www.gnu.org/licenses/. */ declare(strict_types=1); -namespace FireflyIII\Events; +namespace FireflyIII\Events\Test; use FireflyIII\User; use Illuminate\Queue\SerializesModels; -/** - * Class AdminRequestedTestMessage. - */ -class AdminRequestedTestMessage extends Event +class UserTestNotificationChannel { use SerializesModels; - public User $user; + public string $channel; + public User $user; /** * Create a new event instance. */ - public function __construct(User $user) + public function __construct(string $channel, User $user) { - app('log')->debug(sprintf('Triggered AdminRequestedTestMessage for user #%d (%s)', $user->id, $user->email)); - $this->user = $user; + app('log')->debug(sprintf('Triggered UserTestNotificationChannel("%s")', $channel)); + $this->user = $user; + $this->channel = $channel; } } diff --git a/app/Exceptions/GracefulNotFoundHandler.php b/app/Exceptions/GracefulNotFoundHandler.php index 5eab18f7a1..afc2372ceb 100644 --- a/app/Exceptions/GracefulNotFoundHandler.php +++ b/app/Exceptions/GracefulNotFoundHandler.php @@ -158,7 +158,7 @@ class GracefulNotFoundHandler extends ExceptionHandler $accountId = $param->id; } if (!($param instanceof Account) && !is_object($param)) { - $accountId = (int)$param; + $accountId = (int) $param; } /** @var null|Account $account */ @@ -188,7 +188,7 @@ class GracefulNotFoundHandler extends ExceptionHandler $user = auth()->user(); $route = $request->route(); $param = $route->parameter('transactionGroup'); - $groupId = !is_object($param) ? (int)$param : 0; + $groupId = !is_object($param) ? (int) $param : 0; /** @var null|TransactionGroup $group */ $group = $user->transactionGroups()->withTrashed()->find($groupId); @@ -228,7 +228,7 @@ class GracefulNotFoundHandler extends ExceptionHandler $user = auth()->user(); $route = $request->route(); $param = $route->parameter('attachment'); - $attachmentId = is_object($param) ? 0 : (int)$param; + $attachmentId = is_object($param) ? 0 : (int) $param; /** @var null|Attachment $attachment */ $attachment = $user->attachments()->withTrashed()->find($attachmentId); diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 0086841f41..60d31fa76f 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -36,8 +36,6 @@ use Illuminate\Session\TokenMismatchException; use Illuminate\Support\Arr; use Illuminate\Validation\ValidationException as LaravelValidationException; use Laravel\Passport\Exceptions\OAuthServerException as LaravelOAuthException; -use LaravelJsonApi\Core\Exceptions\JsonApiException; -use LaravelJsonApi\Exceptions\ExceptionParser; use League\OAuth2\Server\Exception\OAuthServerException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpFoundation\Response; @@ -65,18 +63,12 @@ class Handler extends ExceptionHandler HttpException::class, SuspiciousOperationException::class, BadHttpHeaderException::class, - JsonApiException::class, ]; /** * Register the exception handling callbacks for the application. */ - public function register(): void - { - $this->renderable( - ExceptionParser::make()->renderable() - ); - } + public function register(): void {} /** * Render an exception into an HTTP response. It's complex but lucky for us, we never use it because @@ -157,7 +149,7 @@ class Handler extends ExceptionHandler $errorCode = 500; $errorCode = $e instanceof MethodNotAllowedHttpException ? 405 : $errorCode; - $isDebug = (bool)config('app.debug', false); + $isDebug = (bool) config('app.debug', false); if ($isDebug) { app('log')->debug(sprintf('Return JSON %s with debug.', get_class($e))); @@ -214,7 +206,7 @@ class Handler extends ExceptionHandler */ public function report(\Throwable $e): void { - $doMailError = (bool)config('firefly.send_error_message'); + $doMailError = (bool) config('firefly.send_error_message'); if ($this->shouldntReportLocal($e) || !$doMailError) { parent::report($e); @@ -250,7 +242,7 @@ class Handler extends ExceptionHandler // create job that will mail. $ipAddress = request()->ip() ?? '0.0.0.0'; - $job = new MailError($userData, (string)config('firefly.site_owner'), $ipAddress, $data); + $job = new MailError($userData, (string) config('firefly.site_owner'), $ipAddress, $data); dispatch($job); parent::report($e); diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php index faaaaa6c52..5d6fea36e0 100644 --- a/app/Factory/AccountFactory.php +++ b/app/Factory/AccountFactory.php @@ -126,7 +126,7 @@ class AccountFactory */ protected function getAccountType(array $data): ?AccountType { - $accountTypeId = array_key_exists('account_type_id', $data) ? (int)$data['account_type_id'] : 0; + $accountTypeId = array_key_exists('account_type_id', $data) ? (int) $data['account_type_id'] : 0; $accountTypeName = array_key_exists('account_type_name', $data) ? $data['account_type_name'] : null; $result = null; // find by name or ID @@ -184,7 +184,7 @@ class AccountFactory 'iban' => $data['iban'], ]; // fix virtual balance when it's empty - if ('' === (string)$databaseData['virtual_balance']) { + if ('' === (string) $databaseData['virtual_balance']) { $databaseData['virtual_balance'] = null; } // remove virtual balance when not an asset account @@ -236,9 +236,9 @@ class AccountFactory */ private function cleanMetaDataArray(Account $account, array $data): array { - $currencyId = array_key_exists('currency_id', $data) ? (int)$data['currency_id'] : 0; - $currencyCode = array_key_exists('currency_code', $data) ? (string)$data['currency_code'] : ''; - $accountRole = array_key_exists('account_role', $data) ? (string)$data['account_role'] : null; + $currencyId = array_key_exists('currency_id', $data) ? (int) $data['currency_id'] : 0; + $currencyCode = array_key_exists('currency_code', $data) ? (string) $data['currency_code'] : ''; + $accountRole = array_key_exists('account_role', $data) ? (string) $data['account_role'] : null; $currency = $this->getCurrency($currencyId, $currencyCode); // only asset account may have a role: @@ -289,7 +289,7 @@ class AccountFactory $data[$field] = 1; } - $factory->crud($account, $field, (string)$data[$field]); + $factory->crud($account, $field, (string) $data[$field]); } } } @@ -351,7 +351,7 @@ class AccountFactory $order = $maxOrder + 1; } if (array_key_exists('order', $data)) { - $order = (int)($data['order'] > $maxOrder ? $maxOrder + 1 : $data['order']); + $order = (int) ($data['order'] > $maxOrder ? $maxOrder + 1 : $data['order']); $order = 0 === $order ? $maxOrder + 1 : $order; } diff --git a/app/Factory/AttachmentFactory.php b/app/Factory/AttachmentFactory.php index 44c16afc2a..bd64ea57a8 100644 --- a/app/Factory/AttachmentFactory.php +++ b/app/Factory/AttachmentFactory.php @@ -50,7 +50,7 @@ class AttachmentFactory // get journal instead of transaction. if (Transaction::class === $model) { /** @var null|Transaction $transaction */ - $transaction = $this->user->transactions()->find((int)$data['attachable_id']); + $transaction = $this->user->transactions()->find((int) $data['attachable_id']); if (null === $transaction) { throw new FireflyException('Unexpectedly could not find transaction'); } @@ -73,7 +73,7 @@ class AttachmentFactory 'uploaded' => 0, ] ); - $notes = (string)($data['notes'] ?? ''); + $notes = (string) ($data['notes'] ?? ''); if ('' !== $notes) { $note = new Note(); $note->noteable()->associate($attachment); diff --git a/app/Factory/BillFactory.php b/app/Factory/BillFactory.php index 9a1b38cd47..ae120c4fdb 100644 --- a/app/Factory/BillFactory.php +++ b/app/Factory/BillFactory.php @@ -48,7 +48,7 @@ class BillFactory { app('log')->debug(sprintf('Now in %s', __METHOD__), $data); $factory = app(TransactionCurrencyFactory::class); - $currency = $factory->find((int)($data['currency_id'] ?? null), (string)($data['currency_code'] ?? null)) ?? + $currency = $factory->find((int) ($data['currency_id'] ?? null), (string) ($data['currency_code'] ?? null)) ?? app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); try { @@ -58,23 +58,23 @@ class BillFactory /** @var Bill $bill */ $bill = Bill::create( [ - 'name' => $data['name'], - 'match' => 'MIGRATED_TO_RULES', - 'amount_min' => $data['amount_min'], - 'user_id' => $this->user->id, - 'user_group_id' => $this->user->user_group_id, - 'transaction_currency_id' => $currency->id, - 'amount_max' => $data['amount_max'], - 'date' => $data['date'], - 'date_tz' => $data['date']->format('e'), - 'end_date' => $data['end_date'] ?? null, - 'end_date_tz' => $data['end_date']?->format('e'), - 'extension_date' => $data['extension_date'] ?? null, - 'extension_date_tz' => $data['extension_date']?->format('e'), - 'repeat_freq' => $data['repeat_freq'], - 'skip' => $skip, - 'automatch' => true, - 'active' => $active, + 'name' => $data['name'], + 'match' => 'MIGRATED_TO_RULES', + 'amount_min' => $data['amount_min'], + 'user_id' => $this->user->id, + 'user_group_id' => $this->user->user_group_id, + 'transaction_currency_id' => $currency->id, + 'amount_max' => $data['amount_max'], + 'date' => $data['date'], + 'date_tz' => $data['date']->format('e'), + 'end_date' => $data['end_date'] ?? null, + 'end_date_tz' => $data['end_date']?->format('e'), + 'extension_date' => $data['extension_date'] ?? null, + 'extension_date_tz' => $data['extension_date']?->format('e'), + 'repeat_freq' => $data['repeat_freq'], + 'skip' => $skip, + 'automatch' => true, + 'active' => $active, ] ); } catch (QueryException $e) { @@ -85,7 +85,7 @@ class BillFactory } if (array_key_exists('notes', $data)) { - $this->updateNote($bill, (string)$data['notes']); + $this->updateNote($bill, (string) $data['notes']); } $objectGroupTitle = $data['object_group_title'] ?? ''; if ('' !== $objectGroupTitle) { @@ -96,7 +96,7 @@ class BillFactory } } // try also with ID: - $objectGroupId = (int)($data['object_group_id'] ?? 0); + $objectGroupId = (int) ($data['object_group_id'] ?? 0); if (0 !== $objectGroupId) { $objectGroup = $this->findObjectGroupById($objectGroupId); if (null !== $objectGroup) { @@ -110,8 +110,8 @@ class BillFactory public function find(?int $billId, ?string $billName): ?Bill { - $billId = (int)$billId; - $billName = (string)$billName; + $billId = (int) $billId; + $billName = (string) $billName; $bill = null; // first find by ID: if ($billId > 0) { diff --git a/app/Factory/BudgetFactory.php b/app/Factory/BudgetFactory.php index f5b438b714..48d424d27d 100644 --- a/app/Factory/BudgetFactory.php +++ b/app/Factory/BudgetFactory.php @@ -35,8 +35,8 @@ class BudgetFactory public function find(?int $budgetId, ?string $budgetName): ?Budget { - $budgetId = (int)$budgetId; - $budgetName = (string)$budgetName; + $budgetId = (int) $budgetId; + $budgetName = (string) $budgetName; if (0 === $budgetId && '' === $budgetName) { return null; diff --git a/app/Factory/CategoryFactory.php b/app/Factory/CategoryFactory.php index b5adc0d7e6..91e419d23e 100644 --- a/app/Factory/CategoryFactory.php +++ b/app/Factory/CategoryFactory.php @@ -40,8 +40,8 @@ class CategoryFactory */ public function findOrCreate(?int $categoryId, ?string $categoryName): ?Category { - $categoryId = (int)$categoryId; - $categoryName = (string)$categoryName; + $categoryId = (int) $categoryId; + $categoryName = (string) $categoryName; app('log')->debug(sprintf('Going to find category with ID %d and name "%s"', $categoryId, $categoryName)); diff --git a/app/Factory/PiggyBankEventFactory.php b/app/Factory/PiggyBankEventFactory.php index 5ae8ed384e..7cfed006a5 100644 --- a/app/Factory/PiggyBankEventFactory.php +++ b/app/Factory/PiggyBankEventFactory.php @@ -47,20 +47,13 @@ class PiggyBankEventFactory $piggyRepos = app(PiggyBankRepositoryInterface::class); $piggyRepos->setUser($journal->user); - $repetition = $piggyRepos->getRepetition($piggyBank); - if (null === $repetition) { - app('log')->error(sprintf('No piggy bank repetition on %s!', $journal->date->format('Y-m-d'))); - - return; - } - app('log')->debug('Found repetition'); - $amount = $piggyRepos->getExactAmount($piggyBank, $repetition, $journal); + $amount = $piggyRepos->getExactAmount($piggyBank, $journal); if (0 === bccomp($amount, '0')) { app('log')->debug('Amount is zero, will not create event.'); return; } // amount can be negative here - $piggyRepos->addAmountToRepetition($repetition, $amount, $journal); + $piggyRepos->addAmountToPiggyBank($piggyBank, $amount, $journal); } } diff --git a/app/Factory/PiggyBankFactory.php b/app/Factory/PiggyBankFactory.php index 505c92d0da..831f24b8b8 100644 --- a/app/Factory/PiggyBankFactory.php +++ b/app/Factory/PiggyBankFactory.php @@ -23,27 +23,131 @@ declare(strict_types=1); namespace FireflyIII\Factory; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\User; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\Log; /** * Class PiggyBankFactory */ class PiggyBankFactory { - private User $user; + use CreatesObjectGroups; + + public User $user { + set(User $value) { + $this->user = $value; + $this->currencyRepository->setUser($value); + $this->accountRepository->setUser($value); + $this->piggyBankRepository->setUser($value); + } + } + private AccountRepositoryInterface $accountRepository; + private CurrencyRepositoryInterface $currencyRepository; + private PiggyBankRepositoryInterface $piggyBankRepository; + + public function __construct() + { + $this->currencyRepository = app(CurrencyRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->piggyBankRepository = app(PiggyBankRepositoryInterface::class); + } + + /** + * Store a piggy bank or come back with an exception. + */ + public function store(array $data): PiggyBank + { + + $piggyBankData = $data; + + // unset some fields + unset($piggyBankData['object_group_title'], $piggyBankData['transaction_currency_code'], $piggyBankData['transaction_currency_id'], $piggyBankData['accounts'], $piggyBankData['object_group_id'], $piggyBankData['notes']); + + // validate amount: + if (array_key_exists('target_amount', $piggyBankData) && '' === (string) $piggyBankData['target_amount']) { + $piggyBankData['target_amount'] = '0'; + } + + $piggyBankData['start_date_tz'] = $piggyBankData['start_date']?->format('e'); + $piggyBankData['target_date_tz'] = $piggyBankData['target_date']?->format('e'); + $piggyBankData['account_id'] = null; + $piggyBankData['transaction_currency_id'] = $this->getCurrency($data)->id; + $piggyBankData['order'] = 131337; + + try { + /** @var PiggyBank $piggyBank */ + $piggyBank = PiggyBank::createQuietly($piggyBankData); + } catch (QueryException $e) { + app('log')->error(sprintf('Could not store piggy bank: %s', $e->getMessage()), $piggyBankData); + + throw new FireflyException('400005: Could not store new piggy bank.', 0, $e); + } + $piggyBank = $this->setOrder($piggyBank, $data); + $this->linkToAccountIds($piggyBank, $data['accounts']); + $this->piggyBankRepository->updateNote($piggyBank, $data['notes']); + + $objectGroupTitle = $data['object_group_title'] ?? ''; + if ('' !== $objectGroupTitle) { + $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); + if (null !== $objectGroup) { + $piggyBank->objectGroups()->sync([$objectGroup->id]); + } + } + // try also with ID + $objectGroupId = (int) ($data['object_group_id'] ?? 0); + if (0 !== $objectGroupId) { + $objectGroup = $this->findObjectGroupById($objectGroupId); + if (null !== $objectGroup) { + $piggyBank->objectGroups()->sync([$objectGroup->id]); + } + } + Log::debug('Touch piggy bank'); + $piggyBank->encrypted = false; + $piggyBank->save(); + $piggyBank->touch(); + + return $piggyBank; + } + + private function getCurrency(array $data): TransactionCurrency + { + // currency: + $defaultCurrency = app('amount')->getDefaultCurrency(); + $currency = null; + if (array_key_exists('transaction_currency_code', $data)) { + $currency = $this->currencyRepository->findByCode((string) ($data['transaction_currency_code'] ?? '')); + } + if (array_key_exists('transaction_currency_id', $data)) { + $currency = $this->currencyRepository->find((int) ($data['transaction_currency_id'] ?? 0)); + } + $currency ??= $defaultCurrency; + + return $currency; + } public function find(?int $piggyBankId, ?string $piggyBankName): ?PiggyBank { - $piggyBankId = (int)$piggyBankId; - $piggyBankName = (string)$piggyBankName; + $piggyBankId = (int) $piggyBankId; + $piggyBankName = (string) $piggyBankName; if ('' === $piggyBankName && 0 === $piggyBankId) { return null; } // first find by ID: if ($piggyBankId > 0) { - /** @var null|PiggyBank $piggyBank */ - $piggyBank = $this->user->piggyBanks()->find($piggyBankId); + $piggyBank = PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.id', $piggyBankId) + ->first(['piggy_banks.*']) + ; if (null !== $piggyBank) { return $piggyBank; } @@ -63,11 +167,92 @@ class PiggyBankFactory public function findByName(string $name): ?PiggyBank { - return $this->user->piggyBanks()->where('piggy_banks.name', $name)->first(); + return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.name', $name) + ->first(['piggy_banks.*']) + ; } - public function setUser(User $user): void + private function setOrder(PiggyBank $piggyBank, array $data): PiggyBank { - $this->user = $user; + $this->resetOrder(); + $order = $this->getMaxOrder() + 1; + if (array_key_exists('order', $data)) { + $order = $data['order']; + } + $piggyBank->order = $order; + $piggyBank->saveQuietly(); + + return $piggyBank; + + } + + public function resetOrder(): void + { + // TODO duplicate code + $set = PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->with( + [ + 'objectGroups', + ] + ) + ->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']) + ; + $current = 1; + foreach ($set as $piggyBank) { + if ($piggyBank->order !== $current) { + app('log')->debug(sprintf('Piggy bank #%d ("%s") was at place %d but should be on %d', $piggyBank->id, $piggyBank->name, $piggyBank->order, $current)); + $piggyBank->order = $current; + $piggyBank->save(); + } + ++$current; + } + } + + private function getMaxOrder(): int + { + return (int) $this->piggyBankRepository->getPiggyBanks()->max('order'); + + } + + public function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void + { + Log::debug(sprintf('Linking piggy bank #%d to %d accounts.', $piggyBank->id, count($accounts)), $accounts); + // collect current current_amount so the sync does not remove them. + // TODO this is a tedious check. Feels like a hack. + $toBeLinked = []; + foreach ($piggyBank->accounts as $account) { + foreach ($accounts as $info) { + if ($account->id === $info['account_id']) { + if (array_key_exists($account->id, $accounts)) { + $toBeLinked[$account->id] = ['current_amount' => $account->pivot?->current_amount ?? '0']; + Log::debug(sprintf('Prefilled for account #%d with amount %s', $account->id, $account->pivot?->current_amount ?? '0')); + } + } + } + } + + + /** @var array $info */ + foreach ($accounts as $info) { + $account = $this->accountRepository->find((int) ($info['account_id'] ?? 0)); + if (null === $account) { + continue; + } + if (array_key_exists('current_amount', $info)) { + $toBeLinked[$account->id] = ['current_amount' => $info['current_amount']]; + Log::debug(sprintf('Will link account #%d with amount %s', $account->id, $account->pivot?->current_amount ?? '0')); + } + if (!array_key_exists('current_amount', $info)) { + $toBeLinked[$account->id] ??= []; + Log::debug(sprintf('Will link account #%d with info: ', $account->id), $toBeLinked[$account->id]); + } + } + Log::debug(sprintf('Link information: %s', json_encode($toBeLinked))); + $piggyBank->accounts()->sync($toBeLinked); } } diff --git a/app/Factory/RecurrenceFactory.php b/app/Factory/RecurrenceFactory.php index ad74dcdec4..e0cf5ce9d7 100644 --- a/app/Factory/RecurrenceFactory.php +++ b/app/Factory/RecurrenceFactory.php @@ -80,7 +80,7 @@ class RecurrenceFactory $firstDate = $data['recurrence']['first_date']; } if (array_key_exists('nr_of_repetitions', $data['recurrence'])) { - $repetitions = (int)$data['recurrence']['nr_of_repetitions']; + $repetitions = (int) $data['recurrence']['nr_of_repetitions']; } if (array_key_exists('repeat_until', $data['recurrence'])) { $repeatUntil = $data['recurrence']['repeat_until']; @@ -117,7 +117,7 @@ class RecurrenceFactory $recurrence->save(); if (array_key_exists('notes', $data['recurrence'])) { - $this->updateNote($recurrence, (string)$data['recurrence']['notes']); + $this->updateNote($recurrence, (string) $data['recurrence']['notes']); } $this->createRepetitions($recurrence, $data['repetitions'] ?? []); diff --git a/app/Factory/TagFactory.php b/app/Factory/TagFactory.php index c0defbdd56..36c5a5b50a 100644 --- a/app/Factory/TagFactory.php +++ b/app/Factory/TagFactory.php @@ -69,9 +69,9 @@ class TagFactory public function create(array $data): ?Tag { - $zoomLevel = 0 === (int)$data['zoom_level'] ? null : (int)$data['zoom_level']; - $latitude = 0.0 === (float)$data['latitude'] ? null : (float)$data['latitude']; // intentional float - $longitude = 0.0 === (float)$data['longitude'] ? null : (float)$data['longitude']; // intentional float + $zoomLevel = 0 === (int) $data['zoom_level'] ? null : (int) $data['zoom_level']; + $latitude = 0.0 === (float) $data['latitude'] ? null : (float) $data['latitude']; // intentional float + $longitude = 0.0 === (float) $data['longitude'] ? null : (float) $data['longitude']; // intentional float $array = [ 'user_id' => $this->user->id, 'user_group_id' => $this->user->user_group_id, diff --git a/app/Factory/TransactionCurrencyFactory.php b/app/Factory/TransactionCurrencyFactory.php index 31c14d7e59..b4088e200d 100644 --- a/app/Factory/TransactionCurrencyFactory.php +++ b/app/Factory/TransactionCurrencyFactory.php @@ -41,7 +41,7 @@ class TransactionCurrencyFactory $data['code'] = e($data['code']); $data['symbol'] = e($data['symbol']); $data['name'] = e($data['name']); - $data['decimal_places'] = (int)$data['decimal_places']; + $data['decimal_places'] = (int) $data['decimal_places']; // if the code already exists (deleted) // force delete it and then create the transaction: $count = TransactionCurrency::withTrashed()->whereCode($data['code'])->count(); @@ -76,7 +76,7 @@ class TransactionCurrencyFactory public function find(?int $currencyId, ?string $currencyCode): ?TransactionCurrency { $currencyCode = e($currencyCode); - $currencyId = (int)$currencyId; + $currencyId = (int) $currencyId; if ('' === $currencyCode && 0 === $currencyId) { app('log')->debug('Cannot find anything on empty currency code and empty currency ID!'); diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index 15308c674f..d39f31699b 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -142,7 +142,7 @@ class TransactionFactory return; } - if ('' !== (string)$this->account->iban) { + if ('' !== (string) $this->account->iban) { app('log')->debug('Account already has IBAN information, will not update.'); return; diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index 0ad8ef096d..f7db35bc54 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -163,11 +163,11 @@ class TransactionJournalFactory $type = $this->typeRepository->findTransactionType(null, $row['type']); $carbon = $row['date'] ?? today(config('app.timezone')); $order = $row['order'] ?? 0; - $currency = $this->currencyRepository->findCurrency((int)$row['currency_id'], $row['currency_code']); + $currency = $this->currencyRepository->findCurrency((int) $row['currency_id'], $row['currency_code']); $foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'], $row['foreign_currency_code']); - $bill = $this->billRepository->findBill((int)$row['bill_id'], $row['bill_name']); + $bill = $this->billRepository->findBill((int) $row['bill_id'], $row['bill_name']); $billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null; - $description = (string)$row['description']; + $description = (string) $row['description']; // Manipulate basic fields $carbon->setTimezone(config('app.timezone')); @@ -254,7 +254,7 @@ class TransactionJournalFactory $transactionFactory->setReconciled($row['reconciled'] ?? false); try { - $negative = $transactionFactory->createNegative((string)$row['amount'], (string)$row['foreign_amount']); + $negative = $transactionFactory->createNegative((string) $row['amount'], (string) $row['foreign_amount']); } catch (FireflyException $e) { app('log')->error(sprintf('Exception creating negative transaction: %s', $e->getMessage())); $this->forceDeleteOnError(new Collection([$journal])); @@ -276,15 +276,15 @@ class TransactionJournalFactory // Firefly III will save the foreign currency information in such a way that both // asset accounts can look at the "amount" and "transaction_currency_id" column and // see the currency they expect to see. - $amount = (string)$row['amount']; - $foreignAmount = (string)$row['foreign_amount']; + $amount = (string) $row['amount']; + $foreignAmount = (string) $row['foreign_amount']; if (null !== $foreignCurrency && $foreignCurrency->id !== $currency->id - && TransactionType::TRANSFER === $type->type + && TransactionType::TRANSFER === $type->type ) { $transactionFactory->setCurrency($foreignCurrency); $transactionFactory->setForeignCurrency($currency); - $amount = (string)$row['foreign_amount']; - $foreignAmount = (string)$row['amount']; + $amount = (string) $row['foreign_amount']; + $foreignAmount = (string) $row['amount']; Log::debug('Swap native/foreign amounts in transfer for new save method.'); } @@ -354,7 +354,7 @@ class TransactionJournalFactory app('log')->warning(sprintf('Found a duplicate in errorIfDuplicate because hash %s is not unique!', $hash)); $journal = $result->transactionJournal()->withTrashed()->first(); $group = $journal?->transactionGroup()->withTrashed()->first(); - $groupId = (int)$group?->id; + $groupId = (int) $group?->id; throw new DuplicateTransactionException(sprintf('Duplicate of transaction #%d.', $groupId)); } @@ -372,10 +372,10 @@ class TransactionJournalFactory // validate source account. $array = [ - 'id' => null !== $data['source_id'] ? (int)$data['source_id'] : null, - 'name' => null !== $data['source_name'] ? (string)$data['source_name'] : null, - 'iban' => null !== $data['source_iban'] ? (string)$data['source_iban'] : null, - 'number' => null !== $data['source_number'] ? (string)$data['source_number'] : null, + 'id' => null !== $data['source_id'] ? (int) $data['source_id'] : null, + 'name' => null !== $data['source_name'] ? (string) $data['source_name'] : null, + 'iban' => null !== $data['source_iban'] ? (string) $data['source_iban'] : null, + 'number' => null !== $data['source_number'] ? (string) $data['source_number'] : null, ]; $validSource = $this->accountValidator->validateSource($array); @@ -387,10 +387,10 @@ class TransactionJournalFactory // validate destination account $array = [ - 'id' => null !== $data['destination_id'] ? (int)$data['destination_id'] : null, - 'name' => null !== $data['destination_name'] ? (string)$data['destination_name'] : null, - 'iban' => null !== $data['destination_iban'] ? (string)$data['destination_iban'] : null, - 'number' => null !== $data['destination_number'] ? (string)$data['destination_number'] : null, + 'id' => null !== $data['destination_id'] ? (int) $data['destination_id'] : null, + 'name' => null !== $data['destination_name'] ? (string) $data['destination_name'] : null, + 'iban' => null !== $data['destination_iban'] ? (string) $data['destination_iban'] : null, + 'number' => null !== $data['destination_number'] ? (string) $data['destination_number'] : null, ]; $validDestination = $this->accountValidator->validateDestination($array); @@ -537,7 +537,7 @@ class TransactionJournalFactory { app('log')->debug('Will now store piggy event.'); - $piggyBank = $this->piggyRepository->findPiggyBank((int)$data['piggy_bank_id'], $data['piggy_bank_name']); + $piggyBank = $this->piggyRepository->findPiggyBank((int) $data['piggy_bank_id'], $data['piggy_bank_name']); if (null !== $piggyBank) { $this->piggyEventFactory->create($journal, $piggyBank); @@ -560,7 +560,7 @@ class TransactionJournalFactory $set = [ 'journal' => $journal, 'name' => $field, - 'data' => (string)($data[$field] ?? ''), + 'data' => (string) ($data[$field] ?? ''), ]; if ($data[$field] instanceof Carbon) { $data[$field]->setTimezone(config('app.timezone')); diff --git a/app/Factory/TransactionJournalMetaFactory.php b/app/Factory/TransactionJournalMetaFactory.php index 19fe414ecc..56dd79d1e0 100644 --- a/app/Factory/TransactionJournalMetaFactory.php +++ b/app/Factory/TransactionJournalMetaFactory.php @@ -50,7 +50,7 @@ class TransactionJournalMetaFactory app('log')->debug('Is a carbon object.'); $value = $data['data']->toW3cString(); } - if ('' === (string)$value) { + if ('' === (string) $value) { // app('log')->debug('Is an empty string.'); // don't store blank strings. if (null !== $entry) { diff --git a/app/Generator/Chart/Basic/ChartJsGenerator.php b/app/Generator/Chart/Basic/ChartJsGenerator.php index 3675871dec..4fd3a258e7 100644 --- a/app/Generator/Chart/Basic/ChartJsGenerator.php +++ b/app/Generator/Chart/Basic/ChartJsGenerator.php @@ -47,7 +47,7 @@ class ChartJsGenerator implements GeneratorInterface $amounts = array_column($data, 'amount'); $next = next($amounts); $sortFlag = SORT_ASC; - if (!is_bool($next) && 1 === bccomp((string)$next, '0')) { + if (!is_bool($next) && 1 === bccomp((string) $next, '0')) { $sortFlag = SORT_DESC; } array_multisort($amounts, $sortFlag, $data); @@ -56,7 +56,7 @@ class ChartJsGenerator implements GeneratorInterface $index = 0; foreach ($data as $key => $valueArray) { // make larger than 0 - $chartData['datasets'][0]['data'][] = app('steam')->positive((string)$valueArray['amount']); + $chartData['datasets'][0]['data'][] = app('steam')->positive((string) $valueArray['amount']); $chartData['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index); $chartData['datasets'][0]['currency_symbol'][] = $valueArray['currency_symbol']; $chartData['labels'][] = $key; @@ -154,7 +154,7 @@ class ChartJsGenerator implements GeneratorInterface // different sort when values are positive and when they're negative. asort($data); $next = next($data); - if (!is_bool($next) && 1 === bccomp((string)$next, '0')) { + if (!is_bool($next) && 1 === bccomp((string) $next, '0')) { // next is positive, sort other way around. arsort($data); } @@ -163,7 +163,7 @@ class ChartJsGenerator implements GeneratorInterface $index = 0; foreach ($data as $key => $value) { // make larger than 0 - $chartData['datasets'][0]['data'][] = app('steam')->positive((string)$value); + $chartData['datasets'][0]['data'][] = app('steam')->positive((string) $value); $chartData['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index); $chartData['labels'][] = $key; diff --git a/app/Generator/Report/Audit/MonthReportGenerator.php b/app/Generator/Report/Audit/MonthReportGenerator.php index 5872a31fca..87d67d20c4 100644 --- a/app/Generator/Report/Audit/MonthReportGenerator.php +++ b/app/Generator/Report/Audit/MonthReportGenerator.php @@ -31,6 +31,7 @@ use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Collection; /** @@ -82,6 +83,9 @@ class MonthReportGenerator implements ReportGeneratorInterface 'create_date', 'update_date', + // more + 'notes', + // date fields. 'interest_date', 'book_date', @@ -125,12 +129,12 @@ class MonthReportGenerator implements ReportGeneratorInterface /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setRange($this->start, $this->end)->withAccountInformation() - ->withBudgetInformation()->withCategoryInformation()->withBillInformation() + ->withBudgetInformation()->withCategoryInformation()->withBillInformation()->withNotes() ; $journals = $collector->getExtractedJournals(); $journals = array_reverse($journals, true); - $dayBeforeBalance = app('steam')->balance($account, $date); - $startBalance = $dayBeforeBalance; + $dayBeforeBalance = Steam::finalAccountBalance($account, $date); + $startBalance = $dayBeforeBalance['balance']; $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); $currency = $accountRepository->getAccountCurrency($account) ?? $defaultCurrency; @@ -168,9 +172,9 @@ class MonthReportGenerator implements ReportGeneratorInterface 'journals' => $journals, 'currency' => $currency, 'exists' => 0 !== count($journals), - 'end' => $this->end->isoFormat((string)trans('config.month_and_day_moment_js', [], $locale)), - 'endBalance' => app('steam')->balance($account, $this->end), - 'dayBefore' => $date->isoFormat((string)trans('config.month_and_day_moment_js', [], $locale)), + 'end' => $this->end->isoFormat((string) trans('config.month_and_day_moment_js', [], $locale)), + 'endBalance' => Steam::finalAccountBalance($account, $this->end)['balance'], + 'dayBefore' => $date->isoFormat((string) trans('config.month_and_day_moment_js', [], $locale)), 'dayBeforeBalance' => $dayBeforeBalance, ]; } diff --git a/app/Handlers/Events/APIEventHandler.php b/app/Handlers/Events/APIEventHandler.php index 1770c1a1ca..79f3bfe22e 100644 --- a/app/Handlers/Events/APIEventHandler.php +++ b/app/Handlers/Events/APIEventHandler.php @@ -43,7 +43,7 @@ class APIEventHandler /** @var UserRepositoryInterface $repository */ $repository = app(UserRepositoryInterface::class); - $user = $repository->find((int)$event->userId); + $user = $repository->find((int) $event->userId); if (null !== $user) { try { diff --git a/app/Handlers/Events/AdminEventHandler.php b/app/Handlers/Events/AdminEventHandler.php index fb8565b137..531d947249 100644 --- a/app/Handlers/Events/AdminEventHandler.php +++ b/app/Handlers/Events/AdminEventHandler.php @@ -24,12 +24,18 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Events; use FireflyIII\Events\Admin\InvitationCreated; -use FireflyIII\Events\AdminRequestedTestMessage; use FireflyIII\Events\NewVersionAvailable; -use FireflyIII\Notifications\Admin\TestNotification; +use FireflyIII\Events\Security\UnknownUserAttemptedLogin; +use FireflyIII\Events\Test\OwnerTestNotificationChannel; +use FireflyIII\Notifications\Admin\UnknownUserLoginAttempt; use FireflyIII\Notifications\Admin\UserInvitation; use FireflyIII\Notifications\Admin\VersionCheckResult; -use FireflyIII\Repositories\User\UserRepositoryInterface; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\Notifications\Test\OwnerTestNotificationEmail; +use FireflyIII\Notifications\Test\OwnerTestNotificationNtfy; +use FireflyIII\Notifications\Test\OwnerTestNotificationPushover; +use FireflyIII\Notifications\Test\OwnerTestNotificationSlack; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Notification; /** @@ -39,87 +45,14 @@ class AdminEventHandler { public function sendInvitationNotification(InvitationCreated $event): void { - $sendMail = app('fireflyconfig')->get('notification_invite_created', true)->data; + $sendMail = app('fireflyconfig')->get('notification_invite_created', true)->data; if (false === $sendMail) { return; } - /** @var UserRepositoryInterface $repository */ - $repository = app(UserRepositoryInterface::class); - $all = $repository->all(); - foreach ($all as $user) { - if ($repository->hasRole($user, 'owner')) { - try { - Notification::send($user, new UserInvitation($event->invitee)); - } catch (\Exception $e) { // @phpstan-ignore-line - $message = $e->getMessage(); - if (str_contains($message, 'Bcc')) { - app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - if (str_contains($message, 'RFC 2822')) { - app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - } - } - } - } - - /** - * Send new version message to admin. - */ - public function sendNewVersion(NewVersionAvailable $event): void - { - $sendMail = app('fireflyconfig')->get('notification_new_version', true)->data; - if (false === $sendMail) { - return; - } - - /** @var UserRepositoryInterface $repository */ - $repository = app(UserRepositoryInterface::class); - $all = $repository->all(); - foreach ($all as $user) { - if ($repository->hasRole($user, 'owner')) { - try { - Notification::send($user, new VersionCheckResult($event->message)); - } catch (\Exception $e) {// @phpstan-ignore-line - $message = $e->getMessage(); - if (str_contains($message, 'Bcc')) { - app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - if (str_contains($message, 'RFC 2822')) { - app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - } - } - } - } - - /** - * Sends a test message to an administrator. - */ - public function sendTestMessage(AdminRequestedTestMessage $event): void - { - /** @var UserRepositoryInterface $repository */ - $repository = app(UserRepositoryInterface::class); - - if (!$repository->hasRole($event->user, 'owner')) { - return; - } - try { - Notification::send($event->user, new TestNotification($event->user->email)); + $owner = new OwnerNotifiable(); + Notification::send($owner, new UserInvitation($owner, $event->invitee)); } catch (\Exception $e) { // @phpstan-ignore-line $message = $e->getMessage(); if (str_contains($message, 'Bcc')) { @@ -136,4 +69,111 @@ class AdminEventHandler app('log')->error($e->getTraceAsString()); } } + + public function sendLoginAttemptNotification(UnknownUserAttemptedLogin $event): void + { + try { + $owner = new OwnerNotifiable(); + Notification::send($owner, new UnknownUserLoginAttempt($event->address)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + } + + /** + * Send new version message to admin. + */ + public function sendNewVersion(NewVersionAvailable $event): void + { + $sendMail = app('fireflyconfig')->get('notification_new_version', true)->data; + if (false === $sendMail) { + return; + } + + try { + $owner = new OwnerNotifiable(); + Notification::send($owner, new VersionCheckResult($event->message)); + } catch (\Exception $e) {// @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + } + + /** + * Sends a test message to an administrator. + */ + public function sendTestNotification(OwnerTestNotificationChannel $event): void + { + Log::debug(sprintf('Now in sendTestNotification("%s")', $event->channel)); + + switch ($event->channel) { + case 'email': + $class = OwnerTestNotificationEmail::class; + + break; + + case 'slack': + $class = OwnerTestNotificationSlack::class; + + break; + + case 'ntfy': + $class = OwnerTestNotificationNtfy::class; + + break; + + case 'pushover': + $class = OwnerTestNotificationPushover::class; + + break; + + default: + app('log')->error(sprintf('Unknown channel "%s" in sendTestNotification method.', $event->channel)); + + return; + } + Log::debug(sprintf('Will send %s as a notification.', $class)); + + try { + Notification::send($event->owner, new $class($event->owner)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + Log::debug(sprintf('If you see no errors above this line, test notification was sent over channel "%s"', $event->channel)); + } } diff --git a/app/Handlers/Events/Model/BudgetLimitHandler.php b/app/Handlers/Events/Model/BudgetLimitHandler.php index 71fd12ac60..a0a60b60e1 100644 --- a/app/Handlers/Events/Model/BudgetLimitHandler.php +++ b/app/Handlers/Events/Model/BudgetLimitHandler.php @@ -32,6 +32,7 @@ use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; use FireflyIII\User; +use Illuminate\Support\Facades\Log; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Spatie\Period\Boundaries; @@ -45,20 +46,20 @@ class BudgetLimitHandler { public function created(Created $event): void { - app('log')->debug(sprintf('BudgetLimitHandler::created(#%s)', $event->budgetLimit->id)); + Log::debug(sprintf('BudgetLimitHandler::created(#%s)', $event->budgetLimit->id)); $this->updateAvailableBudget($event->budgetLimit); } private function updateAvailableBudget(BudgetLimit $budgetLimit): void { - app('log')->debug(sprintf('Now in updateAvailableBudget(#%d)', $budgetLimit->id)); + Log::debug(sprintf('Now in updateAvailableBudget(limit #%d)', $budgetLimit->id)); $budget = Budget::find($budgetLimit->budget_id); if (null === $budget) { - app('log')->warning('Budget is null, probably deleted, find deleted version.'); + Log::warning('Budget is null, probably deleted, find deleted version.'); $budget = Budget::withTrashed()->find($budgetLimit->budget_id); } if (null === $budget) { - app('log')->warning('Budget is still null, cannot continue, will delete budget limit.'); + Log::warning('Budget is still null, cannot continue, will delete budget limit.'); $budgetLimit->forceDelete(); return; @@ -69,7 +70,7 @@ class BudgetLimitHandler // sanity check. It happens when the budget has been deleted so the original user is unknown. if (null === $user) { - app('log')->warning('User is null, cannot continue.'); + Log::warning('User is null, cannot continue.'); $budgetLimit->forceDelete(); return; @@ -82,14 +83,14 @@ class BudgetLimitHandler try { $viewRange = app('preferences')->getForUser($user, 'viewRange', '1M')->data; } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { - app('log')->error($e->getMessage()); + Log::error($e->getMessage()); $viewRange = '1M'; } // safety catch if (null === $viewRange || is_array($viewRange)) { $viewRange = '1M'; } - $viewRange = (string)$viewRange; + $viewRange = (string) $viewRange; $start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange); $end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange); @@ -97,7 +98,7 @@ class BudgetLimitHandler // limit period in total is: $limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE()); - app('log')->debug(sprintf('Limit period is from %s to %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); + Log::debug(sprintf('Limit period is from %s to %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); // from the start until the end of the budget limit, need to loop! $current = clone $start; @@ -106,44 +107,43 @@ class BudgetLimitHandler // create or find AB for this particular period, and set the amount accordingly. /** @var null|AvailableBudget $availableBudget */ - $availableBudget = $user->availableBudgets()->where('start_date', $current->format('Y-m-d'))->where( - 'end_date', - $currentEnd->format('Y-m-d') - )->where('transaction_currency_id', $budgetLimit->transaction_currency_id)->first(); + $availableBudget = $user->availableBudgets()->where('start_date', $current->format('Y-m-d'))->where('end_date', $currentEnd->format('Y-m-d'))->where('transaction_currency_id', $budgetLimit->transaction_currency_id)->first(); if (null !== $availableBudget) { - app('log')->debug('Found 1 AB, will update.'); + Log::debug('Found 1 AB, will update.'); $this->calculateAmount($availableBudget); } if (null === $availableBudget) { + Log::debug('No AB found, will create.'); // if not exists: $currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE()); $daily = $this->getDailyAmount($budgetLimit); - $amount = bcmul($daily, (string)$currentPeriod->length(), 12); + $amount = bcmul($daily, (string) $currentPeriod->length(), 12); // no need to calculate if period is equal. if ($currentPeriod->equals($limitPeriod)) { $amount = 0 === $budgetLimit->id ? '0' : $budgetLimit->amount; } if (0 === bccomp($amount, '0')) { - app('log')->debug('Amount is zero, will not create AB.'); + Log::debug('Amount is zero, will not create AB.'); } if (0 !== bccomp($amount, '0')) { - app('log')->debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d'))); + Log::debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d'))); $availableBudget = new AvailableBudget( [ - 'user_id' => $user->id, - 'user_group_id' => $user->user_group_id, - 'transaction_currency_id' => $budgetLimit->transaction_currency_id, - 'start_date' => $current, - 'start_date_tz' => $current->format('e'), - 'end_date' => $currentEnd, - 'end_date_tz' => $currentEnd->format('e'), - 'amount' => $amount, + 'user_id' => $user->id, + 'user_group_id' => $user->user_group_id, + 'transaction_currency_id' => $budgetLimit->transaction_currency_id, + 'start_date' => $current, + 'start_date_tz' => $current->format('e'), + 'end_date' => $currentEnd, + 'end_date_tz' => $currentEnd->format('e'), + 'amount' => $amount, ] ); $availableBudget->save(); - app('log')->debug(sprintf('ID of new AB is #%d', $availableBudget->id)); + Log::debug(sprintf('ID of new AB is #%d', $availableBudget->id)); + $this->calculateAmount($availableBudget); } } @@ -158,7 +158,7 @@ class BudgetLimitHandler $repository->setUser($availableBudget->user); $newAmount = '0'; $abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY()); - app('log')->debug( + Log::debug( sprintf( 'Now at AB #%d, ("%s" to "%s")', $availableBudget->id, @@ -168,11 +168,11 @@ class BudgetLimitHandler ); // have to recalculate everything just in case. $set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date); - app('log')->debug(sprintf('Found %d interesting budget limit(s).', $set->count())); + Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count())); /** @var BudgetLimit $budgetLimit */ foreach ($set as $budgetLimit) { - app('log')->debug( + Log::debug( sprintf( 'Found interesting budget limit #%d ("%s" to "%s")', $budgetLimit->id, @@ -189,31 +189,31 @@ class BudgetLimitHandler ); // if both equal each other, amount from this BL must be added to the AB if ($limitPeriod->equals($abPeriod)) { - app('log')->debug('This budget limit is equal to the available budget period.'); + Log::debug('This budget limit is equal to the available budget period.'); $newAmount = bcadd($newAmount, $budgetLimit->amount); } // if budget limit period is inside AB period, it can be added in full. if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) { - app('log')->debug('This budget limit is smaller than the available budget period.'); + Log::debug('This budget limit is smaller than the available budget period.'); $newAmount = bcadd($newAmount, $budgetLimit->amount); } if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) { - app('log')->debug('This budget limit is something else entirely!'); + Log::debug('This budget limit is something else entirely!'); $overlap = $abPeriod->overlap($limitPeriod); if (null !== $overlap) { $length = $overlap->length(); - $daily = bcmul($this->getDailyAmount($budgetLimit), (string)$length); + $daily = bcmul($this->getDailyAmount($budgetLimit), (string) $length); $newAmount = bcadd($newAmount, $daily); } } } if (0 === bccomp('0', $newAmount)) { - app('log')->debug('New amount is zero, deleting AB.'); + Log::debug('New amount is zero, deleting AB.'); $availableBudget->delete(); return; } - app('log')->debug(sprintf('Concluded new amount for this AB must be %s', $newAmount)); + Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount)); $availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places); $availableBudget->save(); } @@ -230,8 +230,8 @@ class BudgetLimitHandler boundaries: Boundaries::EXCLUDE_NONE() ); $days = $limitPeriod->length(); - $amount = bcdiv($budgetLimit->amount, (string)$days, 12); - app('log')->debug( + $amount = bcdiv($budgetLimit->amount, (string) $days, 12); + Log::debug( sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount) ); @@ -240,7 +240,7 @@ class BudgetLimitHandler public function deleted(Deleted $event): void { - app('log')->debug(sprintf('BudgetLimitHandler::deleted(#%s)', $event->budgetLimit->id)); + Log::debug(sprintf('BudgetLimitHandler::deleted(#%s)', $event->budgetLimit->id)); $budgetLimit = $event->budgetLimit; $budgetLimit->id = 0; $this->updateAvailableBudget($event->budgetLimit); @@ -248,7 +248,7 @@ class BudgetLimitHandler public function updated(Updated $event): void { - app('log')->debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id)); + Log::debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id)); $this->updateAvailableBudget($event->budgetLimit); } } diff --git a/app/Handlers/Events/PreferencesEventHandler.php b/app/Handlers/Events/PreferencesEventHandler.php new file mode 100644 index 0000000000..a18dcc75f7 --- /dev/null +++ b/app/Handlers/Events/PreferencesEventHandler.php @@ -0,0 +1,141 @@ + ['native_virtual_balance'], + 'available_budgets' => ['native_amount'], + 'bills' => ['native_amount_min', 'native_amount_max'], + // 'transactions' => ['native_amount', 'native_foreign_amount'] + ]; + foreach ($tables as $table => $columns) { + foreach ($columns as $column) { + Log::debug(sprintf('Resetting column %s in table %s.', $column, $table)); + DB::table($table)->where('user_group_id', $event->userGroup->id)->update([$column => null]); + } + } + $this->resetPiggyBanks($event->userGroup); + $this->resetBudgets($event->userGroup); + $this->resetTransactions($event->userGroup); + // fire laravel command to recalculate them all. + if (Amount::convertToNative()) { + Artisan::call('correction:recalculate-native-amounts'); + } + } + + private function resetPiggyBanks(UserGroup $userGroup): void + { + $repository = app(PiggyBankRepositoryInterface::class); + $repository->setUserGroup($userGroup); + $piggyBanks = $repository->getPiggyBanks(); + + /** @var PiggyBank $piggyBank */ + foreach ($piggyBanks as $piggyBank) { + if (null !== $piggyBank->native_target_amount) { + Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id)); + $piggyBank->native_target_amount = null; + $piggyBank->saveQuietly(); + } + foreach ($piggyBank->accounts as $account) { + if (null !== $account->pivot->native_current_amount) { + Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id)); + $account->pivot->native_current_amount = null; + $account->pivot->save(); + } + } + foreach ($piggyBank->piggyBankEvents as $event) { + if (null !== $event->native_amount) { + Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id)); + $event->native_amount = null; + $event->saveQuietly(); + } + } + } + } + + private function resetBudgets(UserGroup $userGroup): void + { + $repository = app(BudgetRepositoryInterface::class); + $repository->setUserGroup($userGroup); + $set = $repository->getBudgets(); + + /** @var Budget $budget */ + foreach ($set as $budget) { + foreach ($budget->autoBudgets as $autoBudget) { + if (null !== $autoBudget->native_amount) { + if (null !== $autoBudget->native_amount) { + Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id)); + $autoBudget->native_amount = null; + $autoBudget->saveQuietly(); + } + } + } + foreach ($budget->budgetlimits as $limit) { + if (null !== $limit->native_amount) { + Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id)); + $limit->native_amount = null; + $limit->saveQuietly(); + } + } + } + + } + + private function resetTransactions(UserGroup $userGroup): void + { + // custom query because of the potential size of this update. + $success = DB::table('transactions') + ->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.user_group_id', $userGroup->id) + ->where(static function (Builder $q): void { + $q->whereNotNull('native_amount') + ->orWhereNotNull('native_foreign_amount') + ; + }) + ->update(['native_amount' => null, 'native_foreign_amount' => null]) + ; + Log::debug(sprintf('Reset %d transactions.', $success)); + } +} diff --git a/app/Handlers/Events/Security/MFAHandler.php b/app/Handlers/Events/Security/MFAHandler.php index 80c3409adf..1320ac2080 100644 --- a/app/Handlers/Events/Security/MFAHandler.php +++ b/app/Handlers/Events/Security/MFAHandler.php @@ -42,56 +42,6 @@ use Illuminate\Support\Facades\Notification; class MFAHandler { - public function sendMFAEnabledMail(EnabledMFA $event): void - { - app('log')->debug(sprintf('Now in %s', __METHOD__)); - - $user = $event->user; - - try { - Notification::send($user, new EnabledMFANotification($user)); - } catch (\Exception $e) { // @phpstan-ignore-line - $message = $e->getMessage(); - if (str_contains($message, 'Bcc')) { - app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - if (str_contains($message, 'RFC 2822')) { - app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - } - } - - public function sendNewMFABackupCodesMail(MFANewBackupCodes $event): void - { - app('log')->debug(sprintf('Now in %s', __METHOD__)); - - $user = $event->user; - - try { - Notification::send($user, new NewBackupCodesNotification($user)); - } catch (\Exception $e) { // @phpstan-ignore-line - $message = $e->getMessage(); - if (str_contains($message, 'Bcc')) { - app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - if (str_contains($message, 'RFC 2822')) { - app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - } - } - public function sendBackupFewLeftMail(MFABackupFewLeft $event): void { app('log')->debug(sprintf('Now in %s', __METHOD__)); @@ -118,6 +68,81 @@ class MFAHandler } } + public function sendBackupNoLeftMail(MFABackupNoLeft $event): void + { + app('log')->debug(sprintf('Now in %s', __METHOD__)); + + $user = $event->user; + + try { + Notification::send($user, new MFABackupNoLeftNotification($user)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + } + + public function sendMFADisabledMail(DisabledMFA $event): void + { + app('log')->debug(sprintf('Now in %s', __METHOD__)); + + $user = $event->user; + + try { + Notification::send($user, new DisabledMFANotification($user)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + } + + public function sendMFAEnabledMail(EnabledMFA $event): void + { + app('log')->debug(sprintf('Now in %s', __METHOD__)); + + $user = $event->user; + + try { + Notification::send($user, new EnabledMFANotification($user)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + } + public function sendMFAFailedAttemptsMail(MFAManyFailedAttempts $event): void { app('log')->debug(sprintf('Now in %s', __METHOD__)); @@ -144,14 +169,14 @@ class MFAHandler } } - public function sendBackupNoLeftMail(MFABackupNoLeft $event): void + public function sendNewMFABackupCodesMail(MFANewBackupCodes $event): void { app('log')->debug(sprintf('Now in %s', __METHOD__)); $user = $event->user; try { - Notification::send($user, new MFABackupNoLeftNotification($user)); + Notification::send($user, new NewBackupCodesNotification($user)); } catch (\Exception $e) { // @phpstan-ignore-line $message = $e->getMessage(); if (str_contains($message, 'Bcc')) { @@ -193,29 +218,4 @@ class MFAHandler app('log')->error($e->getTraceAsString()); } } - - public function sendMFADisabledMail(DisabledMFA $event): void - { - app('log')->debug(sprintf('Now in %s', __METHOD__)); - - $user = $event->user; - - try { - Notification::send($user, new DisabledMFANotification($user)); - } catch (\Exception $e) { // @phpstan-ignore-line - $message = $e->getMessage(); - if (str_contains($message, 'Bcc')) { - app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - if (str_contains($message, 'RFC 2822')) { - app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - } - } } diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index e534a662e5..91efe68017 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -31,6 +31,8 @@ use FireflyIII\Events\Admin\InvitationCreated; use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\RegisteredUser; use FireflyIII\Events\RequestedNewPassword; +use FireflyIII\Events\Security\UserAttemptedLogin; +use FireflyIII\Events\Test\UserTestNotificationChannel; use FireflyIII\Events\UserChangedEmail; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Mail\ConfirmEmailChangeMail; @@ -40,12 +42,18 @@ use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserRole; use FireflyIII\Notifications\Admin\UserRegistration as AdminRegistrationNotification; +use FireflyIII\Notifications\Security\UserFailedLoginAttempt; +use FireflyIII\Notifications\Test\UserTestNotificationEmail; +use FireflyIII\Notifications\Test\UserTestNotificationNtfy; +use FireflyIII\Notifications\Test\UserTestNotificationPushover; +use FireflyIII\Notifications\Test\UserTestNotificationSlack; use FireflyIII\Notifications\User\UserLogin; use FireflyIII\Notifications\User\UserNewPassword; use FireflyIII\Notifications\User\UserRegistration as UserRegistrationNotification; use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Auth\Events\Login; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Notification; use Mail; @@ -179,14 +187,13 @@ class UserEventHandler */ public function notifyNewIPAddress(DetectedNewIPAddress $event): void { - $user = $event->user; - $ipAddress = $event->ipAddress; + $user = $event->user; if ($user->hasRole('demo')) { return; // do not email demo user. } - $list = app('preferences')->getForUser($user, 'login_ip_history', [])->data; + $list = app('preferences')->getForUser($user, 'login_ip_history', [])->data; if (!is_array($list)) { $list = []; } @@ -195,7 +202,7 @@ class UserEventHandler foreach ($list as $index => $entry) { if (false === $entry['notified']) { try { - Notification::send($user, new UserLogin($ipAddress)); + Notification::send($user, new UserLogin()); } catch (\Exception $e) { // @phpstan-ignore-line $message = $e->getMessage(); if (str_contains($message, 'Bcc')) { @@ -220,31 +227,26 @@ class UserEventHandler public function sendAdminRegistrationNotification(RegisteredUser $event): void { - $sendMail = (bool)app('fireflyconfig')->get('notification_admin_new_reg', true)->data; + $sendMail = (bool) app('fireflyconfig')->get('notification_admin_new_reg', true)->data; if ($sendMail) { - /** @var UserRepositoryInterface $repository */ - $repository = app(UserRepositoryInterface::class); - $all = $repository->all(); - foreach ($all as $user) { - if ($repository->hasRole($user, 'owner')) { - try { - Notification::send($user, new AdminRegistrationNotification($event->user)); - } catch (\Exception $e) { // @phpstan-ignore-line - $message = $e->getMessage(); - if (str_contains($message, 'Bcc')) { - app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + $owner = $event->owner; - return; - } - if (str_contains($message, 'RFC 2822')) { - app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + try { + Notification::send($owner, new AdminRegistrationNotification($event->owner, $event->user)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - return; - } - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); - } + return; } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); } } } @@ -285,7 +287,7 @@ class UserEventHandler $oldEmail = $event->oldEmail; $user = $event->user; $token = app('preferences')->getForUser($user, 'email_change_undo_token', 'invalid'); - $hashed = hash('sha256', sprintf('%s%s', (string)config('app.key'), $oldEmail)); + $hashed = hash('sha256', sprintf('%s%s', (string) config('app.key'), $oldEmail)); $url = route('profile.undo-email-change', [$token->data, $hashed]); try { @@ -298,6 +300,27 @@ class UserEventHandler } } + public function sendLoginAttemptNotification(UserAttemptedLogin $event): void + { + try { + Notification::send($event->user, new UserFailedLoginAttempt($event->user)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + } + /** * Send a new password to the user. */ @@ -347,7 +370,7 @@ class UserEventHandler */ public function sendRegistrationMail(RegisteredUser $event): void { - $sendMail = (bool)app('fireflyconfig')->get('notification_user_new_reg', true)->data; + $sendMail = (bool) app('fireflyconfig')->get('notification_user_new_reg', true)->data; if ($sendMail) { try { Notification::send($event->user, new UserRegistrationNotification()); @@ -369,6 +392,61 @@ class UserEventHandler } } + /** + * Sends a test message to an administrator. + */ + public function sendTestNotification(UserTestNotificationChannel $event): void + { + Log::debug(sprintf('Now in (user) sendTestNotification("%s")', $event->channel)); + + switch ($event->channel) { + case 'email': + $class = UserTestNotificationEmail::class; + + break; + + case 'slack': + $class = UserTestNotificationSlack::class; + + break; + + case 'ntfy': + $class = UserTestNotificationNtfy::class; + + break; + + case 'pushover': + $class = UserTestNotificationPushover::class; + + break; + + default: + app('log')->error(sprintf('Unknown channel "%s" in (user) sendTestNotification method.', $event->channel)); + + return; + } + Log::debug(sprintf('Will send %s as a notification.', $class)); + + try { + Notification::send($event->user, new $class($event->user)); + } catch (\Exception $e) { // @phpstan-ignore-line + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + } + Log::debug(sprintf('If you see no errors above this line, test notification was sent over channel "%s"', $event->channel)); + } + /** * @throws FireflyException */ @@ -425,7 +503,7 @@ class UserEventHandler app('preferences')->setForUser($user, 'login_ip_history', $preference); if (false === $inArray && true === $send) { - event(new DetectedNewIPAddress($user, $ip)); + event(new DetectedNewIPAddress($user)); } } } diff --git a/app/Handlers/Events/VersionCheckEventHandler.php b/app/Handlers/Events/VersionCheckEventHandler.php index 70a104ac07..a1e909c68d 100644 --- a/app/Handlers/Events/VersionCheckEventHandler.php +++ b/app/Handlers/Events/VersionCheckEventHandler.php @@ -49,7 +49,7 @@ class VersionCheckEventHandler // should not check for updates: $permission = app('fireflyconfig')->get('permission_update_check', -1); - $value = (int)$permission->data; + $value = (int) $permission->data; if (1 !== $value) { app('log')->debug('Update check is not enabled.'); $this->warnToCheckForUpdates($event); @@ -111,7 +111,7 @@ class VersionCheckEventHandler // last check time was more than a week ago. app('log')->debug('Have warned about a new version in four weeks!'); - session()->flash('info', (string)trans('firefly.disabled_but_check')); + session()->flash('info', (string) trans('firefly.disabled_but_check')); app('fireflyconfig')->set('last_update_warning', time()); } } diff --git a/app/Handlers/Observer/AccountObserver.php b/app/Handlers/Observer/AccountObserver.php index d93fd9d427..fc839ec78b 100644 --- a/app/Handlers/Observer/AccountObserver.php +++ b/app/Handlers/Observer/AccountObserver.php @@ -25,21 +25,56 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Observer; use FireflyIII\Models\Account; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; +use Illuminate\Support\Facades\Log; /** * Class AccountObserver */ class AccountObserver { + public function created(Account $account): void + { + // Log::debug('Observe "created" of an account.'); + $this->updateNativeAmount($account); + } + + private function updateNativeAmount(Account $account): void + { + if (!Amount::convertToNative($account->user)) { + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); + $repository = app(AccountRepositoryInterface::class); + $currency = $repository->getAccountCurrency($account); + if (null !== $currency && $currency->id !== $userCurrency->id && '' !== (string) $account->virtual_balance && 0 !== bccomp($account->virtual_balance, '0')) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $account->native_virtual_balance = $converter->convert($currency, $userCurrency, today(), $account->virtual_balance); + + } + if ('' === (string) $account->virtual_balance || ('' !== (string) $account->virtual_balance && 0 === bccomp($account->virtual_balance, '0'))) { + $account->virtual_balance = null; + $account->native_virtual_balance = null; + } + $account->saveQuietly(); + // Log::debug('Account native virtual balance is updated.'); + } + /** * Also delete related objects. */ public function deleting(Account $account): void { - app('log')->debug('Observe "deleting" of an account.'); + // app('log')->debug('Observe "deleting" of an account.'); $account->accountMeta()->delete(); + + /** @var PiggyBank $piggy */ foreach ($account->piggyBanks()->get() as $piggy) { - $piggy->delete(); + $piggy->accounts()->detach($account); } foreach ($account->attachments()->get() as $attachment) { $attachment->delete(); @@ -50,4 +85,10 @@ class AccountObserver $account->notes()->delete(); $account->locations()->delete(); } + + public function updated(Account $account): void + { + // Log::debug('Observe "updated" of an account.'); + $this->updateNativeAmount($account); + } } diff --git a/app/Handlers/Observer/AutoBudgetObserver.php b/app/Handlers/Observer/AutoBudgetObserver.php new file mode 100644 index 0000000000..6109617b52 --- /dev/null +++ b/app/Handlers/Observer/AutoBudgetObserver.php @@ -0,0 +1,61 @@ +updateNativeAmount($autoBudget); + } + + public function updated(AutoBudget $autoBudget): void + { + Log::debug('Observe "updated" of an auto budget.'); + $this->updateNativeAmount($autoBudget); + } + + private function updateNativeAmount(AutoBudget $autoBudget): void + { + if (!Amount::convertToNative($autoBudget->budget->user)) { + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($autoBudget->budget->user->userGroup); + $autoBudget->native_amount = null; + if ($autoBudget->transactionCurrency->id !== $userCurrency->id) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $autoBudget->native_amount = $converter->convert($autoBudget->transactionCurrency, $userCurrency, today(), $autoBudget->amount); + } + $autoBudget->saveQuietly(); + Log::debug('Auto budget native amount is updated.'); + } +} diff --git a/app/Handlers/Observer/AvailableBudgetObserver.php b/app/Handlers/Observer/AvailableBudgetObserver.php new file mode 100644 index 0000000000..7d749af5b2 --- /dev/null +++ b/app/Handlers/Observer/AvailableBudgetObserver.php @@ -0,0 +1,64 @@ +updateNativeAmount($availableBudget); + } + + public function updated(AvailableBudget $availableBudget): void + { + // Log::debug('Observe "updated" of an available budget.'); + $this->updateNativeAmount($availableBudget); + } + + private function updateNativeAmount(AvailableBudget $availableBudget): void + { + if (!Amount::convertToNative($availableBudget->user)) { + // Log::debug('Do not update native available amount of the available budget.'); + + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($availableBudget->user->userGroup); + $availableBudget->native_amount = null; + if ($availableBudget->transactionCurrency->id !== $userCurrency->id) { + $converter = new ExchangeRateConverter(); + $converter->setUserGroup($availableBudget->user->userGroup); + $converter->setIgnoreSettings(true); + $availableBudget->native_amount = $converter->convert($availableBudget->transactionCurrency, $userCurrency, today(), $availableBudget->amount); + } + $availableBudget->saveQuietly(); + Log::debug('Available budget native amount is updated.'); + } +} diff --git a/app/Handlers/Observer/BillObserver.php b/app/Handlers/Observer/BillObserver.php index 5d93e8609f..3dcc1ac5a5 100644 --- a/app/Handlers/Observer/BillObserver.php +++ b/app/Handlers/Observer/BillObserver.php @@ -24,12 +24,21 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Observer; use FireflyIII\Models\Bill; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; +use Illuminate\Support\Facades\Log; /** * Class BillObserver */ class BillObserver { + public function created(Bill $bill): void + { + Log::debug('Observe "created" of a bill.'); + $this->updateNativeAmount($bill); + } + public function deleting(Bill $bill): void { app('log')->debug('Observe "deleting" of a bill.'); @@ -38,4 +47,28 @@ class BillObserver } $bill->notes()->delete(); } + + public function updated(Bill $bill): void + { + Log::debug('Observe "updated" of a bill.'); + $this->updateNativeAmount($bill); + } + + private function updateNativeAmount(Bill $bill): void + { + if (!Amount::convertToNative($bill->user)) { + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($bill->user->userGroup); + $bill->native_amount_min = null; + $bill->native_amount_max = null; + if ($bill->transactionCurrency->id !== $userCurrency->id) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $bill->native_amount_min = $converter->convert($bill->transactionCurrency, $userCurrency, today(), $bill->amount_min); + $bill->native_amount_max = $converter->convert($bill->transactionCurrency, $userCurrency, today(), $bill->amount_max); + } + $bill->saveQuietly(); + Log::debug('Bill native amounts are updated.'); + } } diff --git a/app/Handlers/Observer/BudgetLimitObserver.php b/app/Handlers/Observer/BudgetLimitObserver.php new file mode 100644 index 0000000000..aaac82cb5d --- /dev/null +++ b/app/Handlers/Observer/BudgetLimitObserver.php @@ -0,0 +1,63 @@ +updateNativeAmount($budgetLimit); + } + + public function updated(BudgetLimit $budgetLimit): void + { + Log::debug('Observe "updated" of a budget limit.'); + $this->updateNativeAmount($budgetLimit); + } + + private function updateNativeAmount(BudgetLimit $budgetLimit): void + { + if (!Amount::convertToNative($budgetLimit->budget->user)) { + // Log::debug('Do not update native amount of the budget limit.'); + + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($budgetLimit->budget->user->userGroup); + $budgetLimit->native_amount = null; + if ($budgetLimit->transactionCurrency->id !== $userCurrency->id) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $budgetLimit->native_amount = $converter->convert($budgetLimit->transactionCurrency, $userCurrency, today(), $budgetLimit->amount); + } + $budgetLimit->saveQuietly(); + Log::debug('Budget limit native amounts are updated.'); + } +} diff --git a/app/Handlers/Observer/PiggyBankEventObserver.php b/app/Handlers/Observer/PiggyBankEventObserver.php new file mode 100644 index 0000000000..b764fed9d6 --- /dev/null +++ b/app/Handlers/Observer/PiggyBankEventObserver.php @@ -0,0 +1,61 @@ +updateNativeAmount($event); + } + + public function updated(PiggyBankEvent $event): void + { + Log::debug('Observe "updated" of a piggy bank event.'); + $this->updateNativeAmount($event); + } + + private function updateNativeAmount(PiggyBankEvent $event): void + { + if (!Amount::convertToNative($event->piggyBank->accounts()->first()->user)) { + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($event->piggyBank->accounts()->first()->user->userGroup); + $event->native_amount = null; + if ($event->piggyBank->transactionCurrency->id !== $userCurrency->id) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $event->native_amount = $converter->convert($event->piggyBank->transactionCurrency, $userCurrency, today(), $event->amount); + } + $event->saveQuietly(); + Log::debug('Piggy bank event native amount is updated.'); + } +} diff --git a/app/Handlers/Observer/PiggyBankObserver.php b/app/Handlers/Observer/PiggyBankObserver.php index 5bd961f0e5..cd8acc327d 100644 --- a/app/Handlers/Observer/PiggyBankObserver.php +++ b/app/Handlers/Observer/PiggyBankObserver.php @@ -25,7 +25,8 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Observer; use FireflyIII\Models\PiggyBank; -use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; +use Illuminate\Support\Facades\Log; /** * Class PiggyBankObserver @@ -34,15 +35,8 @@ class PiggyBankObserver { public function created(PiggyBank $piggyBank): void { - app('log')->debug('Observe "created" of a piggy bank.'); - $repetition = new PiggyBankRepetition(); - $repetition->piggyBank()->associate($piggyBank); - $repetition->startdate = $piggyBank->startdate; - $repetition->startdate_tz = $piggyBank->startdate->format('e'); - $repetition->targetdate = $piggyBank->targetdate; - $repetition->targetdate_tz = $piggyBank->targetdate?->format('e'); - $repetition->currentamount = '0'; - $repetition->save(); + Log::debug('Observe "created" of a piggy bank.'); + $this->updateNativeAmount($piggyBank); } /** @@ -61,4 +55,33 @@ class PiggyBankObserver $piggyBank->notes()->delete(); } + + public function updated(PiggyBank $piggyBank): void + { + Log::debug('Observe "updated" of a piggy bank.'); + $this->updateNativeAmount($piggyBank); + } + + private function updateNativeAmount(PiggyBank $piggyBank): void + { + $group = $piggyBank->accounts()->first()?->user->userGroup; + if (null === $group) { + Log::debug(sprintf('No account(s) yet for piggy bank #%d.', $piggyBank->id)); + + return; + } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($group); + if (null === $userCurrency) { + return; + } + $piggyBank->native_target_amount = null; + if ($piggyBank->transactionCurrency->id !== $userCurrency->id) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $converter->setUserGroup($group); + $piggyBank->native_target_amount = $converter->convert($piggyBank->transactionCurrency, $userCurrency, today(), $piggyBank->target_amount); + } + $piggyBank->saveQuietly(); + Log::debug('Piggy bank native target amount is updated.'); + } } diff --git a/app/Handlers/Observer/TransactionObserver.php b/app/Handlers/Observer/TransactionObserver.php index ece823e6b3..ebf2fca3c8 100644 --- a/app/Handlers/Observer/TransactionObserver.php +++ b/app/Handlers/Observer/TransactionObserver.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Observer; use FireflyIII\Models\Transaction; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Models\AccountBalanceCalculator; use Illuminate\Support\Facades\Log; @@ -32,6 +34,20 @@ use Illuminate\Support\Facades\Log; */ class TransactionObserver { + public static bool $recalculate = true; + + public function created(Transaction $transaction): void + { + Log::debug('Observe "created" of a transaction.'); + if (config('firefly.feature_flags.running_balance_column')) { + if (1 === bccomp($transaction->amount, '0') && self::$recalculate) { + Log::debug('Trigger recalculateForJournal'); + AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); + } + } + $this->updateNativeAmount($transaction); + } + public function deleting(?Transaction $transaction): void { app('log')->debug('Observe "deleting" of a transaction.'); @@ -40,23 +56,40 @@ class TransactionObserver public function updated(Transaction $transaction): void { - Log::debug('Observe "updated" of a transaction.'); - if (config('firefly.feature_flags.running_balance_column')) { + // Log::debug('Observe "updated" of a transaction.'); + if (config('firefly.feature_flags.running_balance_column') && self::$recalculate) { if (1 === bccomp($transaction->amount, '0')) { Log::debug('Trigger recalculateForJournal'); AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); } } + $this->updateNativeAmount($transaction); } - public function created(Transaction $transaction): void + private function updateNativeAmount(Transaction $transaction): void { - Log::debug('Observe "created" of a transaction.'); - if (config('firefly.feature_flags.running_balance_column')) { - if (1 === bccomp($transaction->amount, '0')) { - Log::debug('Trigger recalculateForJournal'); - AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); - } + if (!Amount::convertToNative($transaction->transactionJournal->user)) { + return; } + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($transaction->transactionJournal->user->userGroup); + $transaction->native_amount = null; + $transaction->native_foreign_amount = null; + // first normal amount + if ($transaction->transactionCurrency->id !== $userCurrency->id && (null === $transaction->foreign_currency_id || (null !== $transaction->foreign_currency_id && $transaction->foreign_currency_id !== $userCurrency->id))) { + $converter = new ExchangeRateConverter(); + $converter->setUserGroup($transaction->transactionJournal->user->userGroup); + $converter->setIgnoreSettings(true); + $transaction->native_amount = $converter->convert($transaction->transactionCurrency, $userCurrency, $transaction->transactionJournal->date, $transaction->amount); + } + // then foreign amount + if ($transaction->foreignCurrency?->id !== $userCurrency->id && null !== $transaction->foreign_amount && null !== $transaction->foreignCurrency) { + $converter = new ExchangeRateConverter(); + $converter->setUserGroup($transaction->transactionJournal->user->userGroup); + $converter->setIgnoreSettings(true); + $transaction->native_foreign_amount = $converter->convert($transaction->foreignCurrency, $userCurrency, $transaction->transactionJournal->date, $transaction->foreign_amount); + } + + $transaction->saveQuietly(); + Log::debug('Transaction native amounts are updated.'); } } diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index b0b241a819..42b53f8c56 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -54,8 +54,8 @@ class AttachmentHelper implements AttachmentHelperInterface */ public function __construct() { - $this->maxUploadSize = (int)config('firefly.maxUploadSize'); - $this->allowedMimes = (array)config('firefly.allowedMimes'); + $this->maxUploadSize = (int) config('firefly.maxUploadSize'); + $this->allowedMimes = (array) config('firefly.allowedMimes'); $this->errors = new MessageBag(); $this->messages = new MessageBag(); $this->attachments = new Collection(); @@ -67,7 +67,7 @@ class AttachmentHelper implements AttachmentHelperInterface */ public function getAttachmentContent(Attachment $attachment): string { - $encryptedData = (string)$this->uploadDisk->get(sprintf('at-%d.data', $attachment->id)); + $encryptedData = (string) $this->uploadDisk->get(sprintf('at-%d.data', $attachment->id)); try { $unencryptedData = \Crypt::decrypt($encryptedData); // verified @@ -146,7 +146,7 @@ class AttachmentHelper implements AttachmentHelperInterface return false; } - $mime = (string)finfo_file($finfo, $path); + $mime = (string) finfo_file($finfo, $path); $allowedMime = config('firefly.allowedMimes'); if (!in_array($mime, $allowedMime, true)) { Log::error(sprintf('Mime type %s is not allowed for API file upload.', $mime)); @@ -162,7 +162,7 @@ class AttachmentHelper implements AttachmentHelperInterface $this->uploadDisk->put($file, $content); // update attachment. - $attachment->md5 = (string)md5_file($path); + $attachment->md5 = (string) md5_file($path); $attachment->mime = $mime; $attachment->size = strlen($content); $attachment->uploaded = true; @@ -224,7 +224,7 @@ class AttachmentHelper implements AttachmentHelperInterface $attachment = new Attachment(); // create Attachment object. $attachment->user()->associate($user); $attachment->attachable()->associate($model); - $attachment->md5 = (string)md5_file($file->getRealPath()); + $attachment->md5 = (string) md5_file($file->getRealPath()); $attachment->filename = $file->getClientOriginalName(); $attachment->mime = $file->getMimeType(); $attachment->size = $file->getSize(); @@ -241,7 +241,7 @@ class AttachmentHelper implements AttachmentHelperInterface return null; } - $content = (string)$fileObject->fread($file->getSize()); + $content = (string) $fileObject->fread($file->getSize()); Log::debug(sprintf('Full file length is %d and upload size is %d.', strlen($content), $file->getSize())); // store it without encryption. @@ -251,7 +251,7 @@ class AttachmentHelper implements AttachmentHelperInterface $this->attachments->push($attachment); $name = e($file->getClientOriginalName()); // add message: - $msg = (string)trans('validation.file_attached', ['name' => $name]); + $msg = (string) trans('validation.file_attached', ['name' => $name]); $this->messages->add('attachments', $msg); } @@ -298,7 +298,7 @@ class AttachmentHelper implements AttachmentHelperInterface $result = true; if (!in_array($mime, $this->allowedMimes, true)) { - $msg = (string)trans('validation.file_invalid_mime', ['name' => $name, 'mime' => $mime]); + $msg = (string) trans('validation.file_invalid_mime', ['name' => $name, 'mime' => $mime]); $this->errors->add('attachments', $msg); Log::error($msg); @@ -317,7 +317,7 @@ class AttachmentHelper implements AttachmentHelperInterface $name = e($file->getClientOriginalName()); $result = true; if ($size > $this->maxUploadSize) { - $msg = (string)trans('validation.file_too_large', ['name' => $name]); + $msg = (string) trans('validation.file_too_large', ['name' => $name]); $this->errors->add('attachments', $msg); Log::error($msg); @@ -345,7 +345,7 @@ class AttachmentHelper implements AttachmentHelperInterface } $result = false; if ($count > 0) { - $msg = (string)trans('validation.file_already_attached', ['name' => $name]); + $msg = (string) trans('validation.file_already_attached', ['name' => $name]); $this->errors->add('attachments', $msg); Log::error($msg); $result = true; diff --git a/app/Helpers/Collector/Extensions/AccountCollection.php b/app/Helpers/Collector/Extensions/AccountCollection.php index 1de78d7f31..32eb27d169 100644 --- a/app/Helpers/Collector/Extensions/AccountCollection.php +++ b/app/Helpers/Collector/Extensions/AccountCollection.php @@ -25,8 +25,11 @@ declare(strict_types=1); namespace FireflyIII\Helpers\Collector\Extensions; use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\Account; +use FireflyIII\Support\Facades\Steam; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Trait AccountCollection @@ -228,4 +231,76 @@ trait AccountCollection return $this; } + + #[\Override] + public function accountBalanceIs(string $direction, string $operator, string $value): GroupCollectorInterface + { + Log::warning(sprintf('GroupCollector will be SLOW: accountBalanceIs: "%s" "%s" "%s"', $direction, $operator, $value)); + + /** + * @param int $index + * @param array $object + * + * @return bool + */ + $filter = static function (array $object) use ($direction, $operator, $value): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + $key = sprintf('%s_account_id', $direction); + $accountId = $transaction[$key] ?? 0; + if (0 === $accountId) { + return false; + } + // in theory, this could lead to finding other users accounts. + $balance = Steam::finalAccountBalance(Account::find($accountId), $transaction['date']); + $result = bccomp($balance['balance'], $value); + Log::debug(sprintf('"%s" vs "%s" is %d', $balance['balance'], $value, $result)); + + switch ($operator) { + default: + Log::error(sprintf('GroupCollector: accountBalanceIs: unknown operator "%s"', $operator)); + + return false; + + case '==': + Log::debug('Expect result to be 0 (equal)'); + + return 0 === $result; + + case '!=': + Log::debug('Expect result to be -1 or 1 (not equal)'); + + return 0 !== $result; + + case '>': + Log::debug('Expect result to be 1 (greater then)'); + + return 1 === $result; + + case '>=': + Log::debug('Expect result to be 0 or 1 (greater then or equal)'); + + return -1 !== $result; + + case '<': + Log::debug('Expect result to be -1 (less than)'); + + return -1 === $result; + + case '<=': + Log::debug('Expect result to be -1 or 0 (less than or equal)'); + + return 1 !== $result; + } + // if($balance['balance'] $operator $value) { + + // } + } + + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } } diff --git a/app/Helpers/Collector/Extensions/AttachmentCollection.php b/app/Helpers/Collector/Extensions/AttachmentCollection.php index 6e93eeed87..ab7e420cd5 100644 --- a/app/Helpers/Collector/Extensions/AttachmentCollection.php +++ b/app/Helpers/Collector/Extensions/AttachmentCollection.php @@ -327,7 +327,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && $notes === $value; } @@ -351,7 +351,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && $notes !== $value; } @@ -375,7 +375,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && str_contains(strtolower($notes), strtolower($value)); } @@ -399,7 +399,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && !str_contains(strtolower($notes), strtolower($value)); } @@ -423,7 +423,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && !str_ends_with(strtolower($notes), strtolower($value)); } @@ -447,7 +447,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && !str_starts_with(strtolower($notes), strtolower($value)); } @@ -471,7 +471,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && str_ends_with(strtolower($notes), strtolower($value)); } @@ -495,7 +495,7 @@ trait AttachmentCollection foreach ($transaction['attachments'] as $attachment) { /** @var null|Attachment $object */ $object = auth()->user()->attachments()->find($attachment['id']); - $notes = (string)$object?->notes()->first()?->text; + $notes = (string) $object?->notes()->first()?->text; return '' !== $notes && str_starts_with(strtolower($notes), strtolower($value)); } diff --git a/app/Helpers/Collector/Extensions/MetaCollection.php b/app/Helpers/Collector/Extensions/MetaCollection.php index 2c1bc774e0..5c1ad48277 100644 --- a/app/Helpers/Collector/Extensions/MetaCollection.php +++ b/app/Helpers/Collector/Extensions/MetaCollection.php @@ -195,7 +195,7 @@ trait MetaCollection public function excludeInternalReference(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); $this->joinMetaDataTables(); @@ -216,7 +216,7 @@ trait MetaCollection public function externalIdContains(string $externalId): GroupCollectorInterface { - $externalId = (string)json_encode($externalId); + $externalId = (string) json_encode($externalId); $externalId = str_replace('\\', '\\\\', trim($externalId, '"')); $this->joinMetaDataTables(); @@ -228,7 +228,7 @@ trait MetaCollection public function externalIdDoesNotContain(string $externalId): GroupCollectorInterface { - $externalId = (string)json_encode($externalId); + $externalId = (string) json_encode($externalId); $externalId = str_replace('\\', '\\\\', trim($externalId, '"')); $this->joinMetaDataTables(); @@ -240,7 +240,7 @@ trait MetaCollection public function externalIdDoesNotEnd(string $externalId): GroupCollectorInterface { - $externalId = (string)json_encode($externalId); + $externalId = (string) json_encode($externalId); $externalId = str_replace('\\', '\\\\', trim($externalId, '"')); $this->joinMetaDataTables(); @@ -252,7 +252,7 @@ trait MetaCollection public function externalIdDoesNotStart(string $externalId): GroupCollectorInterface { - $externalId = (string)json_encode($externalId); + $externalId = (string) json_encode($externalId); $externalId = str_replace('\\', '\\\\', trim($externalId, '"')); $this->joinMetaDataTables(); @@ -264,7 +264,7 @@ trait MetaCollection public function externalIdEnds(string $externalId): GroupCollectorInterface { - $externalId = (string)json_encode($externalId); + $externalId = (string) json_encode($externalId); $externalId = str_replace('\\', '\\\\', trim($externalId, '"')); $this->joinMetaDataTables(); @@ -276,7 +276,7 @@ trait MetaCollection public function externalIdStarts(string $externalId): GroupCollectorInterface { - $externalId = (string)json_encode($externalId); + $externalId = (string) json_encode($externalId); $externalId = str_replace('\\', '\\\\', trim($externalId, '"')); $this->joinMetaDataTables(); @@ -289,7 +289,7 @@ trait MetaCollection public function externalUrlContains(string $url): GroupCollectorInterface { $this->joinMetaDataTables(); - $url = (string)json_encode($url); + $url = (string) json_encode($url); $url = str_replace('\\', '\\\\', trim($url, '"')); $this->query->where('journal_meta.name', '=', 'external_url'); $this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $url)); @@ -300,7 +300,7 @@ trait MetaCollection public function externalUrlDoesNotContain(string $url): GroupCollectorInterface { $this->joinMetaDataTables(); - $url = (string)json_encode($url); + $url = (string) json_encode($url); $url = str_replace('\\', '\\\\', trim($url, '"')); $this->query->where('journal_meta.name', '=', 'external_url'); $this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $url)); @@ -311,7 +311,7 @@ trait MetaCollection public function externalUrlDoesNotEnd(string $url): GroupCollectorInterface { $this->joinMetaDataTables(); - $url = (string)json_encode($url); + $url = (string) json_encode($url); $url = str_replace('\\', '\\\\', ltrim($url, '"')); $this->query->where('journal_meta.name', '=', 'external_url'); $this->query->whereNotLike('journal_meta.data', sprintf('%%%s', $url)); @@ -322,7 +322,7 @@ trait MetaCollection public function externalUrlDoesNotStart(string $url): GroupCollectorInterface { $this->joinMetaDataTables(); - $url = (string)json_encode($url); + $url = (string) json_encode($url); $url = str_replace('\\', '\\\\', rtrim($url, '"')); // var_dump($url); @@ -335,7 +335,7 @@ trait MetaCollection public function externalUrlEnds(string $url): GroupCollectorInterface { $this->joinMetaDataTables(); - $url = (string)json_encode($url); + $url = (string) json_encode($url); $url = str_replace('\\', '\\\\', ltrim($url, '"')); $this->query->where('journal_meta.name', '=', 'external_url'); $this->query->whereLike('journal_meta.data', sprintf('%%%s', $url)); @@ -346,7 +346,7 @@ trait MetaCollection public function externalUrlStarts(string $url): GroupCollectorInterface { $this->joinMetaDataTables(); - $url = (string)json_encode($url); + $url = (string) json_encode($url); $url = str_replace('\\', '\\\\', rtrim($url, '"')); // var_dump($url); @@ -397,7 +397,7 @@ trait MetaCollection public function internalReferenceContains(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); // var_dump($internalReference); // exit; @@ -411,7 +411,7 @@ trait MetaCollection public function internalReferenceDoesNotContain(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); $this->joinMetaDataTables(); @@ -423,7 +423,7 @@ trait MetaCollection public function internalReferenceDoesNotEnd(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); $this->joinMetaDataTables(); @@ -435,7 +435,7 @@ trait MetaCollection public function internalReferenceDoesNotStart(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); $this->joinMetaDataTables(); @@ -447,7 +447,7 @@ trait MetaCollection public function internalReferenceEnds(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); $this->joinMetaDataTables(); @@ -459,7 +459,7 @@ trait MetaCollection public function internalReferenceStarts(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); $this->joinMetaDataTables(); @@ -712,7 +712,7 @@ trait MetaCollection public function setInternalReference(string $internalReference): GroupCollectorInterface { - $internalReference = (string)json_encode($internalReference); + $internalReference = (string) json_encode($internalReference); $internalReference = str_replace('\\', '\\\\', trim($internalReference, '"')); $this->joinMetaDataTables(); diff --git a/app/Helpers/Collector/Extensions/TimeCollection.php b/app/Helpers/Collector/Extensions/TimeCollection.php index c596cfdb77..bb19eb13ae 100644 --- a/app/Helpers/Collector/Extensions/TimeCollection.php +++ b/app/Helpers/Collector/Extensions/TimeCollection.php @@ -125,7 +125,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $transaction[$field]->day >= (int)$day; + return $transaction[$field]->day >= (int) $day; } } @@ -143,7 +143,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $transaction[$field]->day <= (int)$day; + return $transaction[$field]->day <= (int) $day; } } @@ -161,7 +161,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return (int)$day === $transaction[$field]->day; + return (int) $day === $transaction[$field]->day; } } @@ -179,7 +179,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return (int)$day !== $transaction[$field]->day; + return (int) $day !== $transaction[$field]->day; } } @@ -197,7 +197,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $transaction[$field]->month >= (int)$month; + return $transaction[$field]->month >= (int) $month; } } @@ -215,7 +215,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $transaction[$field]->month <= (int)$month; + return $transaction[$field]->month <= (int) $month; } } @@ -233,7 +233,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return (int)$month === $transaction[$field]->month; + return (int) $month === $transaction[$field]->month; } } @@ -251,7 +251,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return (int)$month !== $transaction[$field]->month; + return (int) $month !== $transaction[$field]->month; } } @@ -269,7 +269,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $transaction[$field]->year >= (int)$year; + return $transaction[$field]->year >= (int) $year; } } @@ -287,7 +287,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $transaction[$field]->year <= (int)$year; + return $transaction[$field]->year <= (int) $year; } } @@ -305,7 +305,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $year === (string)$transaction[$field]->year; + return $year === (string) $transaction[$field]->year; } } @@ -323,7 +323,7 @@ trait TimeCollection foreach ($object['transactions'] as $transaction) { if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon ) { - return $year !== (string)$transaction[$field]->year; + return $year !== (string) $transaction[$field]->year; } } diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index 00329c2ce8..00a08e70f6 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -131,6 +131,7 @@ class GroupCollector implements GroupCollectorInterface // currency info: 'source.amount as amount', + 'source.native_amount as native_amount', 'source.transaction_currency_id as currency_id', 'currency.code as currency_code', 'currency.name as currency_name', @@ -139,6 +140,7 @@ class GroupCollector implements GroupCollectorInterface // foreign currency info 'source.foreign_amount as foreign_amount', + 'source.native_foreign_amount as native_foreign_amount', 'source.foreign_currency_id as foreign_currency_id', 'foreign_currency.code as foreign_currency_code', 'foreign_currency.name as foreign_currency_name', @@ -292,7 +294,7 @@ class GroupCollector implements GroupCollectorInterface foreach ($params as $param) { $replace = sprintf('"%s"', $param); if (is_int($param)) { - $replace = (string)$param; + $replace = (string) $param; } $pos = strpos($query, '?'); if (false !== $pos) { @@ -463,7 +465,6 @@ class GroupCollector implements GroupCollectorInterface $this->query->orWhereIn('transaction_journals.transaction_group_id', $groupIds); } $result = $this->query->get($this->fields); - // now to parse this into an array. $collection = $this->parseArray($result); @@ -504,13 +505,13 @@ class GroupCollector implements GroupCollectorInterface /** @var TransactionJournal $augumentedJournal */ foreach ($collection as $augumentedJournal) { - $groupId = (int)$augumentedJournal->transaction_group_id; + $groupId = (int) $augumentedJournal->transaction_group_id; if (!array_key_exists($groupId, $groups)) { // make new array $parsedGroup = $this->parseAugmentedJournal($augumentedJournal); $groupArray = [ - 'id' => (int)$augumentedJournal->transaction_group_id, + 'id' => (int) $augumentedJournal->transaction_group_id, 'user_id' => $augumentedJournal->user_id, 'user_group_id' => $augumentedJournal->user_group_id, // Field transaction_group_title was added by the query. @@ -523,7 +524,7 @@ class GroupCollector implements GroupCollectorInterface 'transactions' => [], ]; // Field transaction_journal_id was added by the query. - $journalId = (int)$augumentedJournal->transaction_journal_id; // @phpstan-ignore-line + $journalId = (int) $augumentedJournal->transaction_journal_id; // @phpstan-ignore-line $groupArray['transactions'][$journalId] = $parsedGroup; $groups[$groupId] = $groupArray; @@ -531,7 +532,7 @@ class GroupCollector implements GroupCollectorInterface } // or parse the rest. // Field transaction_journal_id was added by the query. - $journalId = (int)$augumentedJournal->transaction_journal_id; // @phpstan-ignore-line + $journalId = (int) $augumentedJournal->transaction_journal_id; // @phpstan-ignore-line if (array_key_exists($journalId, $groups[$groupId]['transactions'])) { // append data to existing group + journal (for multiple tags or multiple attachments) $groups[$groupId]['transactions'][$journalId] = $this->mergeTags($groups[$groupId]['transactions'][$journalId], $augumentedJournal); @@ -584,7 +585,7 @@ class GroupCollector implements GroupCollectorInterface $dates = ['interest_date', 'payment_date', 'invoice_date', 'book_date', 'due_date', 'process_date']; if (array_key_exists('meta_name', $result) && in_array($result['meta_name'], $dates, true)) { $name = $result['meta_name']; - if (array_key_exists('meta_data', $result) && '' !== (string)$result['meta_data']) { + if (array_key_exists('meta_data', $result) && '' !== (string) $result['meta_data']) { $result[$name] = Carbon::createFromFormat('!Y-m-d', substr(json_decode($result['meta_data']), 0, 10)); } } @@ -595,9 +596,9 @@ class GroupCollector implements GroupCollectorInterface // convert back to strings because SQLite is dumb like that. $result = $this->convertToStrings($result); - $result['reconciled'] = 1 === (int)$result['reconciled']; + $result['reconciled'] = 1 === (int) $result['reconciled']; if (array_key_exists('tag_id', $result) && null !== $result['tag_id']) { // assume the other fields are present as well. - $tagId = (int)$augumentedJournal['tag_id']; + $tagId = (int) $augumentedJournal['tag_id']; $tagDate = null; try { @@ -607,7 +608,7 @@ class GroupCollector implements GroupCollectorInterface } $result['tags'][$tagId] = [ - 'id' => (int)$result['tag_id'], + 'id' => (int) $result['tag_id'], 'name' => $result['tag_name'], 'date' => $tagDate, 'description' => $result['tag_description'], @@ -616,8 +617,8 @@ class GroupCollector implements GroupCollectorInterface // also merge attachments: if (array_key_exists('attachment_id', $result)) { - $uploaded = 1 === (int)$result['attachment_uploaded']; - $attachmentId = (int)$augumentedJournal['attachment_id']; + $uploaded = 1 === (int) $result['attachment_uploaded']; + $attachmentId = (int) $augumentedJournal['attachment_id']; if (0 !== $attachmentId && $uploaded) { $result['attachments'][$attachmentId] = [ 'id' => $attachmentId, @@ -643,7 +644,7 @@ class GroupCollector implements GroupCollectorInterface private function convertToInteger(array $array): array { foreach ($this->integerFields as $field) { - $array[$field] = array_key_exists($field, $array) ? (int)$array[$field] : null; + $array[$field] = array_key_exists($field, $array) ? (int) $array[$field] : null; } return $array; @@ -652,7 +653,7 @@ class GroupCollector implements GroupCollectorInterface private function convertToStrings(array $array): array { foreach ($this->stringFields as $field) { - $array[$field] = array_key_exists($field, $array) && null !== $array[$field] ? (string)$array[$field] : null; + $array[$field] = array_key_exists($field, $array) && null !== $array[$field] ? (string) $array[$field] : null; } return $array; @@ -662,7 +663,7 @@ class GroupCollector implements GroupCollectorInterface { $newArray = $newJournal->toArray(); if (array_key_exists('tag_id', $newArray)) { // assume the other fields are present as well. - $tagId = (int)$newJournal['tag_id']; + $tagId = (int) $newJournal['tag_id']; $tagDate = null; @@ -673,7 +674,7 @@ class GroupCollector implements GroupCollectorInterface } $existingJournal['tags'][$tagId] = [ - 'id' => (int)$newArray['tag_id'], + 'id' => (int) $newArray['tag_id'], 'name' => $newArray['tag_name'], 'date' => $tagDate, 'description' => $newArray['tag_description'], @@ -687,7 +688,7 @@ class GroupCollector implements GroupCollectorInterface { $newArray = $newJournal->toArray(); if (array_key_exists('attachment_id', $newArray)) { - $attachmentId = (int)$newJournal['attachment_id']; + $attachmentId = (int) $newJournal['attachment_id']; $existingJournal['attachments'][$attachmentId] = [ 'id' => $attachmentId, @@ -706,7 +707,7 @@ class GroupCollector implements GroupCollectorInterface foreach ($groups as $groudId => $group) { /** @var array $transaction */ foreach ($group['transactions'] as $transaction) { - $currencyId = (int)$transaction['currency_id']; + $currencyId = (int) $transaction['currency_id']; if (null === $transaction['amount']) { throw new FireflyException(sprintf('Amount is NULL for a transaction in group #%d, please investigate.', $groudId)); } @@ -722,7 +723,7 @@ class GroupCollector implements GroupCollectorInterface $groups[$groudId]['sums'][$currencyId]['amount'] = bcadd($groups[$groudId]['sums'][$currencyId]['amount'], $transaction['amount']); if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) { - $currencyId = (int)$transaction['foreign_currency_id']; + $currencyId = (int) $transaction['foreign_currency_id']; // set default: if (!array_key_exists($currencyId, $groups[$groudId]['sums'])) { @@ -745,7 +746,7 @@ class GroupCollector implements GroupCollectorInterface $currentCollection = $collection; $countFilters = count($this->postFilters); $countCollection = count($currentCollection); - if (0 === $countFilters && 0 === $countCollection) { + if (0 === $countFilters) { return $currentCollection; } app('log')->debug(sprintf('GroupCollector: postFilterCollection has %d filter(s) and %d transaction(s).', count($this->postFilters), count($currentCollection))); @@ -873,6 +874,16 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Limit results to a specific currency, only normal one. + */ + public function setNormalCurrency(TransactionCurrency $currency): GroupCollectorInterface + { + $this->query->where('source.transaction_currency_id', $currency->id); + + return $this; + } + public function setEndRow(int $endRow): self { $this->endRow = $endRow; @@ -1049,6 +1060,7 @@ class GroupCollector implements GroupCollectorInterface ->whereNull('transaction_groups.deleted_at') ->whereNull('transaction_journals.deleted_at') ->whereNull('source.deleted_at') + ->whereNotNull('transaction_groups.id') ->whereNull('destination.deleted_at') ->orderBy('transaction_journals.date', 'DESC') ->orderBy('transaction_journals.order', 'ASC') diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index 9171a65b44..6ff39b81b1 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -457,6 +457,11 @@ interface GroupCollectorInterface */ public function setCurrency(TransactionCurrency $currency): self; + /** + * Limit results to a specific currency, either foreign or normal one. + */ + public function setNormalCurrency(TransactionCurrency $currency): self; + /** * Set destination accounts. */ @@ -732,4 +737,6 @@ interface GroupCollectorInterface public function yearIs(string $year): self; public function yearIsNot(string $year): self; + + public function accountBalanceIs(string $direction, string $operator, string $value): self; } diff --git a/app/Helpers/Fiscal/FiscalHelper.php b/app/Helpers/Fiscal/FiscalHelper.php index c977bc773b..13c9d14bdc 100644 --- a/app/Helpers/Fiscal/FiscalHelper.php +++ b/app/Helpers/Fiscal/FiscalHelper.php @@ -38,7 +38,7 @@ class FiscalHelper implements FiscalHelperInterface */ public function __construct() { - $this->useCustomFiscalYear = (bool)app('preferences')->get('customFiscalYear', false)->data; + $this->useCustomFiscalYear = (bool) app('preferences')->get('customFiscalYear', false)->data; } /** @@ -73,9 +73,9 @@ class FiscalHelper implements FiscalHelperInterface if (is_array($prefStartStr)) { $prefStartStr = '01-01'; } - $prefStartStr = (string)$prefStartStr; + $prefStartStr = (string) $prefStartStr; [$mth, $day] = explode('-', $prefStartStr); - $startDate->day((int)$day)->month((int)$mth); + $startDate->day((int) $day)->month((int) $mth); // if start date is after passed date, sub 1 year. if ($startDate > $date) { diff --git a/app/Helpers/Report/NetWorth.php b/app/Helpers/Report/NetWorth.php index 8e971216a1..85bb500406 100644 --- a/app/Helpers/Report/NetWorth.php +++ b/app/Helpers/Report/NetWorth.php @@ -33,7 +33,8 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; -use FireflyIII\Support\Http\Api\ExchangeRateConverter; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; @@ -65,82 +66,53 @@ class NetWorth implements NetWorthInterface public function byAccounts(Collection $accounts, Carbon $date): array { // start in the past, end in the future? use $date - $ids = implode(',', $accounts->pluck('id')->toArray()); - $cache = new CacheProperties(); + $convertToNative = Amount::convertToNative(); + $ids = implode(',', $accounts->pluck('id')->toArray()); + $cache = new CacheProperties(); $cache->addProperty($date); + $cache->addProperty($convertToNative); $cache->addProperty('net-worth-by-accounts'); $cache->addProperty($ids); if ($cache->has()) { return $cache->get(); } - app('log')->debug(sprintf('Now in byAccounts("%s", "%s")', $ids, $date->format('Y-m-d'))); - Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); - $default = app('amount')->getDefaultCurrency(); - $converter = new ExchangeRateConverter(); - - // default "native" currency has everything twice, for consistency. - $netWorth = [ - 'native' => [ - 'balance' => '0', - 'native_balance' => '0', - 'currency_id' => $default->id, - 'currency_code' => $default->code, - 'currency_name' => $default->name, - 'currency_symbol' => $default->symbol, - 'currency_decimal_places' => $default->decimal_places, - 'native_currency_id' => $default->id, - 'native_currency_code' => $default->code, - 'native_currency_name' => $default->name, - 'native_currency_symbol' => $default->symbol, - 'native_currency_decimal_places' => $default->decimal_places, - ], - ]; - $balances = app('steam')->balancesByAccountsConverted($accounts, $date); + Log::debug(sprintf('Now in byAccounts("%s", "%s")', $ids, $date->format('Y-m-d H:i:s'))); + $default = Amount::getDefaultCurrency(); + $netWorth = []; + $balances = Steam::finalAccountsBalance($accounts, $date); /** @var Account $account */ foreach ($accounts as $account) { - app('log')->debug(sprintf('Now at account #%d ("%s")', $account->id, $account->name)); - $currency = $this->getRepository()->getAccountCurrency($account); - if (null === $currency) { - $currency = app('amount')->getDefaultCurrency(); - } - $currencyCode = $currency->code; - $balance = '0'; - $nativeBalance = '0'; + Log::debug(sprintf('Now at account #%d ("%s")', $account->id, $account->name)); + $currency = $this->getRepository()->getAccountCurrency($account) ?? $default; + $useNative = $convertToNative && $default->id !== $currency->id; + $currency = $useNative ? $default : $currency; + $currencyCode = $currency->code; + $balance = '0'; + $nativeBalance = '0'; if (array_key_exists($account->id, $balances)) { $balance = $balances[$account->id]['balance'] ?? '0'; $nativeBalance = $balances[$account->id]['native_balance'] ?? '0'; } - app('log')->debug(sprintf('Balance is %s, native balance is %s', $balance, $nativeBalance)); - // always subtract virtual balance - $virtualBalance = $account->virtual_balance; - if ('' !== $virtualBalance) { - $balance = bcsub($balance, $virtualBalance); - $nativeVirtualBalance = $converter->convert($default, $currency, $account->created_at, $virtualBalance); - $nativeBalance = bcsub($nativeBalance, $nativeVirtualBalance); - } + Log::debug(sprintf('Balance is %s, native balance is %s', $balance, $nativeBalance)); + // always subtract virtual balance again. + $balance = '' !== (string) $account->virtual_balance ? bcsub($balance, $account->virtual_balance) : $balance; + $nativeBalance = '' !== (string) $account->native_virtual_balance ? bcsub($nativeBalance, $account->native_virtual_balance) : $nativeBalance; + $amountToUse = $useNative ? $nativeBalance : $balance; + Log::debug(sprintf('Will use %s %s', $currencyCode, $amountToUse)); + $netWorth[$currencyCode] ??= [ - 'balance' => '0', - 'native_balance' => '0', - 'currency_id' => (string)$currency->id, - 'currency_code' => $currency->code, - 'currency_name' => $currency->name, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - 'native_currency_id' => (string)$default->id, - 'native_currency_code' => $default->code, - 'native_currency_name' => $default->name, - 'native_currency_symbol' => $default->symbol, - 'native_currency_decimal_places' => $default->decimal_places, + 'balance' => '0', + 'currency_id' => (string) $currency->id, + 'currency_code' => $currency->code, + 'currency_name' => $currency->name, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, ]; - $netWorth[$currencyCode]['balance'] = bcadd($balance, $netWorth[$currencyCode]['balance']); - $netWorth[$currencyCode]['native_balance'] = bcadd($nativeBalance, $netWorth[$currencyCode]['native_balance']); - $netWorth['native']['balance'] = bcadd($nativeBalance, $netWorth['native']['balance']); - $netWorth['native']['native_balance'] = bcadd($nativeBalance, $netWorth['native']['native_balance']); + $netWorth[$currencyCode]['balance'] = bcadd($amountToUse, $netWorth[$currencyCode]['balance']); } $cache->store($netWorth); - $converter->summarize(); return $netWorth; } @@ -187,10 +159,10 @@ class NetWorth implements NetWorthInterface */ $accounts = $this->getAccounts(); $return = []; - $balances = app('steam')->balancesByAccounts($accounts, $date); + $balances = Steam::finalAccountsBalance($accounts, $date); foreach ($accounts as $account) { $currency = $this->getRepository()->getAccountCurrency($account); - $balance = $balances[$account->id] ?? '0'; + $balance = $balances[$account->id]['balance'] ?? '0'; // always subtract virtual balance. $virtualBalance = $account->virtual_balance; @@ -199,7 +171,7 @@ class NetWorth implements NetWorthInterface } $return[$currency->id] ??= [ - 'id' => (string)$currency->id, + 'id' => (string) $currency->id, 'name' => $currency->name, 'symbol' => $currency->symbol, 'code' => $currency->code, @@ -221,7 +193,7 @@ class NetWorth implements NetWorthInterface /** @var Account $account */ foreach ($accounts as $account) { - if (1 === (int)$this->getRepository()->getMetaValue($account, 'include_net_worth')) { + if (1 === (int) $this->getRepository()->getMetaValue($account, 'include_net_worth')) { $filtered->push($account); } } diff --git a/app/Helpers/Report/PopupReport.php b/app/Helpers/Report/PopupReport.php index 9860751cfd..b3c027b098 100644 --- a/app/Helpers/Report/PopupReport.php +++ b/app/Helpers/Report/PopupReport.php @@ -66,7 +66,7 @@ class PopupReport implements PopupReportInterface if (null !== $currencyId) { /** @var CurrencyRepositoryInterface $repos */ $repos = app(CurrencyRepositoryInterface::class); - $currency = $repos->find((int)$currencyId); + $currency = $repos->find((int) $currencyId); } /** @var GroupCollectorInterface $collector */ @@ -98,7 +98,7 @@ class PopupReport implements PopupReportInterface if (null !== $currencyId) { /** @var CurrencyRepositoryInterface $repos */ $repos = app(CurrencyRepositoryInterface::class); - $currency = $repos->find((int)$currencyId); + $currency = $repos->find((int) $currencyId); } /** @var GroupCollectorInterface $collector */ @@ -135,7 +135,7 @@ class PopupReport implements PopupReportInterface if (null !== $currencyId) { /** @var CurrencyRepositoryInterface $repos */ $repos = app(CurrencyRepositoryInterface::class); - $currency = $repos->find((int)$currencyId); + $currency = $repos->find((int) $currencyId); } /** @var GroupCollectorInterface $collector */ @@ -174,7 +174,7 @@ class PopupReport implements PopupReportInterface if (null !== $currencyId) { /** @var CurrencyRepositoryInterface $repos */ $repos = app(CurrencyRepositoryInterface::class); - $currency = $repos->find((int)$currencyId); + $currency = $repos->find((int) $currencyId); } /** @var JournalRepositoryInterface $repository */ diff --git a/app/Helpers/Report/ReportHelper.php b/app/Helpers/Report/ReportHelper.php index 9582c7287d..3fce549ae0 100644 --- a/app/Helpers/Report/ReportHelper.php +++ b/app/Helpers/Report/ReportHelper.php @@ -128,7 +128,7 @@ class ReportHelper implements ReportHelperInterface $currentEnd = clone $start; $currentEnd->endOfMonth(); $months[$year]['months'][] = [ - 'formatted' => $start->isoFormat((string)trans('config.month_js')), + 'formatted' => $start->isoFormat((string) trans('config.month_js')), 'start' => $start->format('Y-m-d'), 'end' => $currentEnd->format('Y-m-d'), 'month' => $start->month, diff --git a/app/Helpers/Update/UpdateTrait.php b/app/Helpers/Update/UpdateTrait.php index e274ccfac7..8a779cdfc2 100644 --- a/app/Helpers/Update/UpdateTrait.php +++ b/app/Helpers/Update/UpdateTrait.php @@ -43,7 +43,7 @@ trait UpdateTrait /** @var UpdateRequestInterface $checker */ $checker = app(UpdateRequestInterface::class); $channelConfig = app('fireflyconfig')->get('update_channel', 'stable'); - $channel = (string)$channelConfig->data; + $channel = (string) $channelConfig->data; return $checker->getUpdateInformation($channel); } diff --git a/app/Http/Controllers/Account/CreateController.php b/app/Http/Controllers/Account/CreateController.php index be7d8fafc0..20dec447c6 100644 --- a/app/Http/Controllers/Account/CreateController.php +++ b/app/Http/Controllers/Account/CreateController.php @@ -59,7 +59,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); - app('view')->share('title', (string)trans('firefly.accounts')); + app('view')->share('title', (string) trans('firefly.accounts')); $this->repository = app(AccountRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); @@ -76,9 +76,8 @@ class CreateController extends Controller */ public function create(Request $request, string $objectType) { - $defaultCurrency = app('amount')->getDefaultCurrency(); $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType)); - $subTitle = (string)trans(sprintf('firefly.make_new_%s_account', $objectType)); + $subTitle = (string) trans(sprintf('firefly.make_new_%s_account', $objectType)); $roles = $this->getRoles(); $liabilityTypes = $this->getLiabilityTypes(); $hasOldInput = null !== $request->old('_token'); @@ -97,17 +96,17 @@ class CreateController extends Controller // interest calculation periods: $interestPeriods = [ - 'daily' => (string)trans('firefly.interest_calc_daily'), - 'monthly' => (string)trans('firefly.interest_calc_monthly'), - 'yearly' => (string)trans('firefly.interest_calc_yearly'), + 'daily' => (string) trans('firefly.interest_calc_daily'), + 'monthly' => (string) trans('firefly.interest_calc_monthly'), + 'yearly' => (string) trans('firefly.interest_calc_yearly'), ]; // pre fill some data $request->session()->flash( 'preFilled', [ - 'currency_id' => $defaultCurrency->id, - 'include_net_worth' => $hasOldInput ? (bool)$request->old('include_net_worth') : true, + 'currency_id' => $this->defaultCurrency->id, + 'include_net_worth' => $hasOldInput ? (bool) $request->old('include_net_worth') : true, ] ); // issue #8321 @@ -140,7 +139,7 @@ class CreateController extends Controller { $data = $request->getAccountData(); $account = $this->repository->store($data); - $request->session()->flash('success', (string)trans('firefly.stored_new_account', ['name' => $account->name])); + $request->session()->flash('success', (string) trans('firefly.stored_new_account', ['name' => $account->name])); app('preferences')->mark(); Log::channel('audit')->info('Stored new account.', $data); @@ -163,7 +162,7 @@ class CreateController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -172,7 +171,7 @@ class CreateController extends Controller // redirect to previous URL. $redirect = redirect($this->getPreviousUrl('accounts.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { // set value so create routine will not overwrite URL: $request->session()->put('accounts.create.fromStore', true); diff --git a/app/Http/Controllers/Account/DeleteController.php b/app/Http/Controllers/Account/DeleteController.php index 24f1cb4375..0763718cd1 100644 --- a/app/Http/Controllers/Account/DeleteController.php +++ b/app/Http/Controllers/Account/DeleteController.php @@ -52,7 +52,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); - app('view')->share('title', (string)trans('firefly.accounts')); + app('view')->share('title', (string) trans('firefly.accounts')); $this->repository = app(AccountRepositoryInterface::class); @@ -73,7 +73,7 @@ class DeleteController extends Controller } $typeName = config(sprintf('firefly.shortNamesByFullName.%s', $account->accountType->type)); - $subTitle = (string)trans(sprintf('firefly.delete_%s_account', $typeName), ['name' => $account->name]); + $subTitle = (string) trans(sprintf('firefly.delete_%s_account', $typeName), ['name' => $account->name]); $accountList = app('expandedform')->makeSelectListWithEmpty($this->repository->getAccountsByType([$account->accountType->type])); $objectType = $typeName; unset($accountList[$account->id]); @@ -98,11 +98,11 @@ class DeleteController extends Controller $type = $account->accountType->type; $typeName = config(sprintf('firefly.shortNamesByFullName.%s', $type)); $name = $account->name; - $moveTo = $this->repository->find((int)$request->get('move_account_before_delete')); + $moveTo = $this->repository->find((int) $request->get('move_account_before_delete')); $this->repository->destroy($account, $moveTo); - $request->session()->flash('success', (string)trans(sprintf('firefly.%s_deleted', $typeName), ['name' => $name])); + $request->session()->flash('success', (string) trans(sprintf('firefly.%s_deleted', $typeName), ['name' => $name])); app('preferences')->mark(); return redirect($this->getPreviousUrl('accounts.delete.url')); diff --git a/app/Http/Controllers/Account/EditController.php b/app/Http/Controllers/Account/EditController.php index e379db4e88..a72c579179 100644 --- a/app/Http/Controllers/Account/EditController.php +++ b/app/Http/Controllers/Account/EditController.php @@ -58,7 +58,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); - app('view')->share('title', (string)trans('firefly.accounts')); + app('view')->share('title', (string) trans('firefly.accounts')); $this->repository = app(AccountRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); @@ -82,7 +82,7 @@ class EditController extends Controller } $objectType = config('firefly.shortNamesByFullName')[$account->accountType->type]; - $subTitle = (string)trans(sprintf('firefly.edit_%s_account', $objectType), ['name' => $account->name]); + $subTitle = (string) trans(sprintf('firefly.edit_%s_account', $objectType), ['name' => $account->name]); $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType)); $roles = $this->getRoles(); $liabilityTypes = $this->getLiabilityTypes(); @@ -90,6 +90,7 @@ class EditController extends Controller $latitude = null !== $location ? $location->latitude : config('firefly.default_location.latitude'); $longitude = null !== $location ? $location->longitude : config('firefly.default_location.longitude'); $zoomLevel = null !== $location ? $location->zoom_level : config('firefly.default_location.zoom_level'); + $canEditCurrency = 0 === $account->piggyBanks()->count(); $hasLocation = null !== $location; $locations = [ 'location' => [ @@ -107,9 +108,9 @@ class EditController extends Controller // interest calculation periods: $interestPeriods = [ - 'daily' => (string)trans('firefly.interest_calc_daily'), - 'monthly' => (string)trans('firefly.interest_calc_monthly'), - 'yearly' => (string)trans('firefly.interest_calc_yearly'), + 'daily' => (string) trans('firefly.interest_calc_daily'), + 'monthly' => (string) trans('firefly.interest_calc_monthly'), + 'yearly' => (string) trans('firefly.interest_calc_yearly'), ]; // put previous url in session if not redirect from store (not "return_to_edit"). @@ -118,12 +119,12 @@ class EditController extends Controller } $request->session()->forget('accounts.edit.fromUpdate'); - $openingBalanceAmount = (string)$repository->getOpeningBalanceAmount($account); + $openingBalanceAmount = (string) $repository->getOpeningBalanceAmount($account); if ('0' === $openingBalanceAmount) { $openingBalanceAmount = ''; } $openingBalanceDate = $repository->getOpeningBalanceDate($account); - $currency = $this->repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); + $currency = $this->repository->getAccountCurrency($account) ?? $this->defaultCurrency; // include this account in net-worth charts? $includeNetWorth = $repository->getMetaValue($account, 'include_net_worth'); @@ -144,17 +145,17 @@ class EditController extends Controller 'cc_type' => $repository->getMetaValue($account, 'cc_type'), 'cc_monthly_payment_date' => $repository->getMetaValue($account, 'cc_monthly_payment_date'), 'BIC' => $repository->getMetaValue($account, 'BIC'), - 'opening_balance_date' => substr((string)$openingBalanceDate, 0, 10), + 'opening_balance_date' => substr((string) $openingBalanceDate, 0, 10), 'liability_type_id' => $account->account_type_id, 'opening_balance' => app('steam')->bcround($openingBalanceAmount, $currency->decimal_places), 'liability_direction' => $this->repository->getMetaValue($account, 'liability_direction'), 'virtual_balance' => app('steam')->bcround($virtualBalance, $currency->decimal_places), 'currency_id' => $currency->id, - 'include_net_worth' => $hasOldInput ? (bool)$request->old('include_net_worth') : $includeNetWorth, + 'include_net_worth' => $hasOldInput ? (bool) $request->old('include_net_worth') : $includeNetWorth, 'interest' => $repository->getMetaValue($account, 'interest'), 'interest_period' => $repository->getMetaValue($account, 'interest_period'), 'notes' => $this->repository->getNoteText($account), - 'active' => $hasOldInput ? (bool)$request->old('active') : $account->active, + 'active' => $hasOldInput ? (bool) $request->old('active') : $account->active, ]; if ('' === $openingBalanceAmount) { $preFilled['opening_balance'] = ''; @@ -162,7 +163,7 @@ class EditController extends Controller $request->session()->flash('preFilled', $preFilled); - return view('accounts.edit', compact('account', 'currency', 'showNetWorth', 'subTitle', 'subTitleIcon', 'locations', 'liabilityDirections', 'objectType', 'roles', 'preFilled', 'liabilityTypes', 'interestPeriods')); + return view('accounts.edit', compact('account', 'currency', 'canEditCurrency', 'showNetWorth', 'subTitle', 'subTitleIcon', 'locations', 'liabilityDirections', 'objectType', 'roles', 'preFilled', 'liabilityTypes', 'interestPeriods')); } /** @@ -179,7 +180,7 @@ class EditController extends Controller $data = $request->getAccountData(); $this->repository->update($account, $data); Log::channel('audit')->info(sprintf('Updated account #%d.', $account->id), $data); - $request->session()->flash('success', (string)trans('firefly.updated_account', ['name' => $account->name])); + $request->session()->flash('success', (string) trans('firefly.updated_account', ['name' => $account->name])); // store new attachment(s): /** @var null|array $files */ @@ -189,7 +190,7 @@ class EditController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -198,7 +199,7 @@ class EditController extends Controller // redirect $redirect = redirect($this->getPreviousUrl('accounts.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { // set value so edit routine will not overwrite URL: $request->session()->put('accounts.edit.fromUpdate', true); diff --git a/app/Http/Controllers/Account/IndexController.php b/app/Http/Controllers/Account/IndexController.php index 870999de13..0bfaec642b 100644 --- a/app/Http/Controllers/Account/IndexController.php +++ b/app/Http/Controllers/Account/IndexController.php @@ -28,6 +28,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\BasicDataSupport; use Illuminate\Contracts\View\Factory; use Illuminate\Http\Request; @@ -54,7 +55,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); - app('view')->share('title', (string)trans('firefly.accounts')); + app('view')->share('title', (string) trans('firefly.accounts')); $this->repository = app(AccountRepositoryInterface::class); @@ -71,13 +72,13 @@ class IndexController extends Controller public function inactive(Request $request, string $objectType) { $inactivePage = true; - $subTitle = (string)trans(sprintf('firefly.%s_accounts_inactive', $objectType)); + $subTitle = (string) trans(sprintf('firefly.%s_accounts_inactive', $objectType)); $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType)); $types = config(sprintf('firefly.accountTypesByIdentifier.%s', $objectType)); $collection = $this->repository->getInactiveAccountsByType($types); $total = $collection->count(); - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $accounts = $collection->slice(($page - 1) * $pageSize, $pageSize); unset($collection); @@ -89,21 +90,23 @@ class IndexController extends Controller $start->subDay(); $ids = $accounts->pluck('id')->toArray(); - $startBalances = app('steam')->balancesByAccounts($accounts, $start); - $endBalances = app('steam')->balancesByAccounts($accounts, $end); + $startBalances = app('steam')->finalAccountsBalance($accounts, $start); + $endBalances = app('steam')->finalAccountsBalance($accounts, $end); $activities = app('steam')->getLastActivities($ids); + $accounts->each( function (Account $account) use ($activities, $startBalances, $endBalances): void { + $currency = $this->repository->getAccountCurrency($account); $account->lastActivityDate = $this->isInArrayDate($activities, $account->id); - $account->startBalance = $this->isInArray($startBalances, $account->id); - $account->endBalance = $this->isInArray($endBalances, $account->id); - $account->difference = bcsub($account->endBalance, $account->startBalance); + $account->startBalances = Steam::filterAccountBalance($startBalances[$account->id] ?? [], $account, $this->convertToNative, $currency); + $account->endBalances = Steam::filterAccountBalance($endBalances[$account->id] ?? [], $account, $this->convertToNative, $currency); + $account->differences = $this->subtract($account->startBalances, $account->endBalances); $account->interest = app('steam')->bcround($this->repository->getMetaValue($account, 'interest'), 4); - $account->interestPeriod = (string)trans(sprintf('firefly.interest_calc_%s', $this->repository->getMetaValue($account, 'interest_period'))); - $account->accountTypeString = (string)trans(sprintf('firefly.account_type_%s', $account->accountType->type)); + $account->interestPeriod = (string) trans(sprintf('firefly.interest_calc_%s', $this->repository->getMetaValue($account, 'interest_period'))); + $account->accountTypeString = (string) trans(sprintf('firefly.account_type_%s', $account->accountType->type)); $account->current_debt = '0'; - $account->iban = implode(' ', str_split((string)$account->iban, 4)); + $account->iban = implode(' ', str_split((string) $account->iban, 4)); } ); @@ -124,7 +127,7 @@ class IndexController extends Controller public function index(Request $request, string $objectType) { app('log')->debug(sprintf('Now at %s', __METHOD__)); - $subTitle = (string)trans(sprintf('firefly.%s_accounts', $objectType)); + $subTitle = (string) trans(sprintf('firefly.%s_accounts', $objectType)); $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType)); $types = config(sprintf('firefly.accountTypesByIdentifier.%s', $objectType)); @@ -132,8 +135,8 @@ class IndexController extends Controller $collection = $this->repository->getActiveAccountsByType($types); $total = $collection->count(); - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $accounts = $collection->slice(($page - 1) * $pageSize, $pageSize); $inactiveCount = $this->repository->getInactiveAccountsByType($types)->count(); @@ -149,29 +152,33 @@ class IndexController extends Controller $start->subDay(); $ids = $accounts->pluck('id')->toArray(); - $startBalances = app('steam')->balancesByAccounts($accounts, $start); - $endBalances = app('steam')->balancesByAccounts($accounts, $end); + $startBalances = app('steam')->finalAccountsBalance($accounts, $start); + $endBalances = app('steam')->finalAccountsBalance($accounts, $end); $activities = app('steam')->getLastActivities($ids); + $accounts->each( function (Account $account) use ($activities, $startBalances, $endBalances): void { - $interest = (string)$this->repository->getMetaValue($account, 'interest'); + $interest = (string) $this->repository->getMetaValue($account, 'interest'); $interest = '' === $interest ? '0' : $interest; + $currency = $this->repository->getAccountCurrency($account); - // See reference nr. 68 + $account->startBalances = Steam::filterAccountBalance($startBalances[$account->id] ?? [], $account, $this->convertToNative, $currency); + $account->endBalances = Steam::filterAccountBalance($endBalances[$account->id] ?? [], $account, $this->convertToNative, $currency); + $account->differences = $this->subtract($account->startBalances, $account->endBalances); $account->lastActivityDate = $this->isInArrayDate($activities, $account->id); - $account->startBalance = $this->isInArray($startBalances, $account->id); - $account->endBalance = $this->isInArray($endBalances, $account->id); - $account->difference = bcsub($account->endBalance, $account->startBalance); $account->interest = app('steam')->bcround($interest, 4); - $account->interestPeriod = (string)trans( + $account->interestPeriod = (string) trans( sprintf('firefly.interest_calc_%s', $this->repository->getMetaValue($account, 'interest_period')) ); - $account->accountTypeString = (string)trans(sprintf('firefly.account_type_%s', $account->accountType->type)); + $account->accountTypeString = (string) trans(sprintf('firefly.account_type_%s', $account->accountType->type)); $account->location = $this->repository->getLocation($account); $account->liability_direction = $this->repository->getMetaValue($account, 'liability_direction'); $account->current_debt = $this->repository->getMetaValue($account, 'current_debt') ?? '-'; - $account->iban = implode(' ', str_split((string)$account->iban, 4)); + $account->currency = $currency ?? $this->defaultCurrency; + $account->iban = implode(' ', str_split((string) $account->iban, 4)); + + } ); // make paginator: @@ -186,4 +193,14 @@ class IndexController extends Controller return view('accounts.index', compact('objectType', 'inactiveCount', 'subTitleIcon', 'subTitle', 'page', 'accounts')); } + + private function subtract(array $startBalances, array $endBalances) + { + $result = []; + foreach ($endBalances as $key => $value) { + $result[$key] = bcsub($value, $startBalances[$key] ?? '0'); + } + + return $result; + } } diff --git a/app/Http/Controllers/Account/ReconcileController.php b/app/Http/Controllers/Account/ReconcileController.php index a679b69739..ec3673c014 100644 --- a/app/Http/Controllers/Account/ReconcileController.php +++ b/app/Http/Controllers/Account/ReconcileController.php @@ -34,6 +34,7 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; @@ -59,7 +60,7 @@ class ReconcileController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); - app('view')->share('title', (string)trans('firefly.accounts')); + app('view')->share('title', (string) trans('firefly.accounts')); $this->repository = app(JournalRepositoryInterface::class); $this->accountRepos = app(AccountRepositoryInterface::class); @@ -81,11 +82,11 @@ class ReconcileController extends Controller return $this->redirectAccountToAccount($account); } if (AccountType::ASSET !== $account->accountType->type) { - session()->flash('error', (string)trans('firefly.must_be_asset_account')); + session()->flash('error', (string) trans('firefly.must_be_asset_account')); return redirect(route('accounts.index', [config(sprintf('firefly.shortNamesByFullName.%s', $account->accountType->type))])); } - $currency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); + $currency = $this->accountRepos->getAccountCurrency($account) ?? $this->defaultCurrency; // no start or end: $range = app('navigation')->getViewRange(false); @@ -110,10 +111,10 @@ class ReconcileController extends Controller $startDate = clone $start; $startDate->subDay(); - $startBalance = app('steam')->bcround(app('steam')->balance($account, $startDate), $currency->decimal_places); - $endBalance = app('steam')->bcround(app('steam')->balance($account, $end), $currency->decimal_places); + $startBalance = Steam::finalAccountBalance($account, $startDate)['balance']; + $endBalance = Steam::finalAccountBalance($account, $end)['balance']; $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $account->accountType->type)); - $subTitle = (string)trans('firefly.reconcile_account', ['account' => $account->name]); + $subTitle = (string) trans('firefly.reconcile_account', ['account' => $account->name]); // various links $transactionsUrl = route('accounts.reconcile.transactions', [$account->id, '%start%', '%end%']); @@ -158,7 +159,7 @@ class ReconcileController extends Controller /** @var string $journalId */ foreach ($data['journals'] as $journalId) { - $this->repository->reconcileById((int)$journalId); + $this->repository->reconcileById((int) $journalId); } app('log')->debug('Reconciled all transactions.'); @@ -175,10 +176,10 @@ class ReconcileController extends Controller app('log')->debug('End of routine.'); app('preferences')->mark(); if ('' === $result) { - session()->flash('success', (string)trans('firefly.reconciliation_stored')); + session()->flash('success', (string) trans('firefly.reconciliation_stored')); } if ('' !== $result) { - session()->flash('error', (string)trans('firefly.reconciliation_error', ['error' => $result])); + session()->flash('error', (string) trans('firefly.reconciliation_error', ['error' => $result])); } return redirect(route('accounts.show', [$account->id])); @@ -196,7 +197,7 @@ class ReconcileController extends Controller } $reconciliation = $this->accountRepos->getReconciliation($account); - $currency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); + $currency = $this->accountRepos->getAccountCurrency($account) ?? $this->defaultCurrency; $source = $reconciliation; $destination = $account; if (1 === bccomp($difference, '0')) { diff --git a/app/Http/Controllers/Account/ShowController.php b/app/Http/Controllers/Account/ShowController.php index b4d7b7cc7d..badc25693d 100644 --- a/app/Http/Controllers/Account/ShowController.php +++ b/app/Http/Controllers/Account/ShowController.php @@ -30,6 +30,7 @@ use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\PeriodOverview; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; @@ -60,7 +61,7 @@ class ShowController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); - app('view')->share('title', (string)trans('firefly.accounts')); + app('view')->share('title', (string) trans('firefly.accounts')); $this->repository = app(AccountRepositoryInterface::class); @@ -89,26 +90,27 @@ class ShowController extends Controller // @var Carbon $end $end ??= session('end'); - if ($end < $start) { + if ($end->lt($start)) { [$start, $end] = [$end, $start]; } $location = $this->repository->getLocation($account); $attachments = $this->repository->getAttachments($account); $today = today(config('app.timezone')); $subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $account->accountType->type)); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; - $currency = $this->repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; + $accountCurrency = $this->repository->getAccountCurrency($account); + $currency = $accountCurrency ?? $this->defaultCurrency; $fStart = $start->isoFormat($this->monthAndDayFormat); $fEnd = $end->isoFormat($this->monthAndDayFormat); - $subTitle = (string)trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]); + $subTitle = (string) trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]); $chartUrl = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]); $firstTransaction = $this->repository->oldestJournalDate($account) ?? $start; $periods = $this->getAccountPeriodOverview($account, $firstTransaction, $end); // if layout = v2, overrule the page title. if ('v1' !== config('view.layout')) { - $subTitle = (string)trans('firefly.all_journals_for_account', ['name' => $account->name]); + $subTitle = (string) trans('firefly.all_journals_for_account', ['name' => $account->name]); } /** @var GroupCollectorInterface $collector */ @@ -128,7 +130,7 @@ class ShowController extends Controller $groups->setPath(route('accounts.show', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')])); $showAll = false; - $balance = app('steam')->balance($account, $end); + $balances = Steam::filterAccountBalance(Steam::finalAccountBalance($account, $end), $account, $this->convertToNative, $accountCurrency); return view( 'accounts.show', @@ -147,7 +149,7 @@ class ShowController extends Controller 'end', 'chartUrl', 'location', - 'balance' + 'balances' ) ); } @@ -164,33 +166,34 @@ class ShowController extends Controller if (!$this->isEditableAccount($account)) { return $this->redirectAccountToAccount($account); } - $location = $this->repository->getLocation($account); - $isLiability = $this->repository->isLiability($account); - $attachments = $this->repository->getAttachments($account); - $objectType = config(sprintf('firefly.shortNamesByFullName.%s', $account->accountType->type)); - $end = today(config('app.timezone')); - $today = today(config('app.timezone')); - $start = $this->repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth(); - $subTitleIcon = config('firefly.subIconsByIdentifier.'.$account->accountType->type); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; - $currency = $this->repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); - $subTitle = (string)trans('firefly.all_journals_for_account', ['name' => $account->name]); - $periods = new Collection(); + $location = $this->repository->getLocation($account); + $isLiability = $this->repository->isLiability($account); + $attachments = $this->repository->getAttachments($account); + $objectType = config(sprintf('firefly.shortNamesByFullName.%s', $account->accountType->type)); + $end = today(config('app.timezone')); + $today = today(config('app.timezone')); + $accountCurrency = $this->repository->getAccountCurrency($account); + $start = $this->repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth(); + $subTitleIcon = config('firefly.subIconsByIdentifier.'.$account->accountType->type); + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; + $currency = $this->repository->getAccountCurrency($account) ?? $this->defaultCurrency; + $subTitle = (string) trans('firefly.all_journals_for_account', ['name' => $account->name]); + $periods = new Collection(); /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page)->withAccountInformation()->withCategoryInformation(); // this search will not include transaction groups where this asset account (or liability) // is just part of ONE of the journals. To force this: $collector->setExpandGroupSearch(true); - $groups = $collector->getPaginatedGroups(); + $groups = $collector->getPaginatedGroups(); $groups->setPath(route('accounts.show.all', [$account->id])); - $chartUrl = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]); - $showAll = true; - $balance = app('steam')->balance($account, $end); + $chartUrl = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]); + $showAll = true; + $balances = Steam::filterAccountBalance(Steam::finalAccountBalance($account, $end), $account, $this->convertToNative, $accountCurrency); return view( 'accounts.show', @@ -210,7 +213,7 @@ class ShowController extends Controller 'subTitle', 'start', 'end', - 'balance' + 'balances' ) ); } diff --git a/app/Http/Controllers/Admin/ConfigurationController.php b/app/Http/Controllers/Admin/ConfigurationController.php index 25e810aeab..f5058a1165 100644 --- a/app/Http/Controllers/Admin/ConfigurationController.php +++ b/app/Http/Controllers/Admin/ConfigurationController.php @@ -45,7 +45,7 @@ class ConfigurationController extends Controller $this->middleware( static function ($request, $next) { - app('view')->share('title', (string)trans('firefly.administration')); + app('view')->share('title', (string) trans('firefly.administration')); app('view')->share('mainTitleIcon', 'fa-hand-spock-o'); return $next($request); @@ -61,7 +61,7 @@ class ConfigurationController extends Controller */ public function index() { - $subTitle = (string)trans('firefly.instance_configuration'); + $subTitle = (string) trans('firefly.instance_configuration'); $subTitleIcon = 'fa-wrench'; Log::channel('audit')->info('User visits admin config index.'); @@ -93,7 +93,7 @@ class ConfigurationController extends Controller app('fireflyconfig')->set('is_demo_site', $data['is_demo_site']); // flash message - session()->flash('success', (string)trans('firefly.configuration_updated')); + session()->flash('success', (string) trans('firefly.configuration_updated')); app('preferences')->mark(); return redirect()->route('admin.configuration.index'); diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index a641b2f4a5..6bfe4159cf 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -23,15 +23,9 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Admin; -use FireflyIII\Events\AdminRequestedTestMessage; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Middleware\IsDemoUser; -use FireflyIII\Support\Notifications\UrlValidator; -use FireflyIII\User; use Illuminate\Contracts\View\Factory; -use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; -use Illuminate\Routing\Redirector; use Illuminate\Support\Facades\Log; use Illuminate\View\View; @@ -57,7 +51,7 @@ class HomeController extends Controller public function index() { Log::channel('audit')->info('User visits admin index.'); - $title = (string)trans('firefly.administration'); + $title = (string) trans('firefly.administration'); $mainTitleIcon = 'fa-hand-spock-o'; $email = auth()->user()->email; $pref = app('preferences')->get('remote_guard_alt_email'); @@ -65,53 +59,6 @@ class HomeController extends Controller $email = $pref->data; } - // admin notification settings: - $notifications = []; - foreach (config('firefly.admin_notifications') as $item) { - $notifications[$item] = app('fireflyconfig')->get(sprintf('notification_%s', $item), true)->data; - } - $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; - - return view('admin.index', compact('title', 'mainTitleIcon', 'email', 'notifications', 'slackUrl')); - } - - public function notifications(Request $request): RedirectResponse - { - foreach (config('firefly.admin_notifications') as $item) { - $value = false; - if ($request->has(sprintf('notification_%s', $item))) { - $value = true; - } - app('fireflyconfig')->set(sprintf('notification_%s', $item), $value); - } - $url = (string)$request->get('slackUrl'); - if ('' === $url) { - app('fireflyconfig')->delete('slack_webhook_url'); - } - if (UrlValidator::isValidWebhookURL($url)) { - app('fireflyconfig')->set('slack_webhook_url', $url); - } - - session()->flash('success', (string)trans('firefly.notification_settings_saved')); - - return redirect(route('admin.index')); - } - - /** - * Send a test message to the admin. - * - * @return Redirector|RedirectResponse - */ - public function testMessage() - { - Log::channel('audit')->info('User sends test message.'); - - /** @var User $user */ - $user = auth()->user(); - app('log')->debug('Now in testMessage() controller.'); - event(new AdminRequestedTestMessage($user)); - session()->flash('info', (string)trans('firefly.send_test_triggered')); - - return redirect(route('admin.index')); + return view('admin.index', compact('title', 'mainTitleIcon', 'email')); } } diff --git a/app/Http/Controllers/Admin/LinkController.php b/app/Http/Controllers/Admin/LinkController.php index a2ccd109d8..92dc5ccd53 100644 --- a/app/Http/Controllers/Admin/LinkController.php +++ b/app/Http/Controllers/Admin/LinkController.php @@ -51,7 +51,7 @@ class LinkController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.administration')); + app('view')->share('title', (string) trans('firefly.administration')); app('view')->share('mainTitleIcon', 'fa-hand-spock-o'); $this->repository = app(LinkTypeRepositoryInterface::class); @@ -70,7 +70,7 @@ class LinkController extends Controller { Log::channel('audit')->info('User visits link index.'); - $subTitle = (string)trans('firefly.create_new_link_type'); + $subTitle = (string) trans('firefly.create_new_link_type'); $subTitleIcon = 'fa-link'; // put previous url in session if not redirect from store (not "create another"). @@ -89,17 +89,17 @@ class LinkController extends Controller public function delete(Request $request, LinkType $linkType) { if (!$linkType->editable) { - $request->session()->flash('error', (string)trans('firefly.cannot_edit_link_type', ['name' => e($linkType->name)])); + $request->session()->flash('error', (string) trans('firefly.cannot_edit_link_type', ['name' => e($linkType->name)])); return redirect(route('admin.links.index')); } Log::channel('audit')->info(sprintf('User wants to delete link type #%d', $linkType->id)); - $subTitle = (string)trans('firefly.delete_link_type', ['name' => $linkType->name]); + $subTitle = (string) trans('firefly.delete_link_type', ['name' => $linkType->name]); $otherTypes = $this->repository->get(); $count = $this->repository->countJournals($linkType); $moveTo = []; - $moveTo[0] = (string)trans('firefly.do_not_save_connection'); + $moveTo[0] = (string) trans('firefly.do_not_save_connection'); /** @var LinkType $otherType */ foreach ($otherTypes as $otherType) { @@ -123,10 +123,10 @@ class LinkController extends Controller { Log::channel('audit')->info(sprintf('User destroyed link type #%d', $linkType->id)); $name = $linkType->name; - $moveTo = $this->repository->find((int)$request->get('move_link_type_before_delete')); + $moveTo = $this->repository->find((int) $request->get('move_link_type_before_delete')); $this->repository->destroy($linkType, $moveTo); - $request->session()->flash('success', (string)trans('firefly.deleted_link_type', ['name' => $name])); + $request->session()->flash('success', (string) trans('firefly.deleted_link_type', ['name' => $name])); app('preferences')->mark(); return redirect($this->getPreviousUrl('link-types.delete.url')); @@ -140,11 +140,11 @@ class LinkController extends Controller public function edit(Request $request, LinkType $linkType) { if (!$linkType->editable) { - $request->session()->flash('error', (string)trans('firefly.cannot_edit_link_type', ['name' => e($linkType->name)])); + $request->session()->flash('error', (string) trans('firefly.cannot_edit_link_type', ['name' => e($linkType->name)])); return redirect(route('admin.links.index')); } - $subTitle = (string)trans('firefly.edit_link_type', ['name' => $linkType->name]); + $subTitle = (string) trans('firefly.edit_link_type', ['name' => $linkType->name]); $subTitleIcon = 'fa-link'; Log::channel('audit')->info(sprintf('User wants to edit link type #%d', $linkType->id)); @@ -165,7 +165,7 @@ class LinkController extends Controller */ public function index() { - $subTitle = (string)trans('firefly.journal_link_configuration'); + $subTitle = (string) trans('firefly.journal_link_configuration'); $subTitleIcon = 'fa-link'; $linkTypes = $this->repository->get(); @@ -186,7 +186,7 @@ class LinkController extends Controller */ public function show(LinkType $linkType) { - $subTitle = (string)trans('firefly.overview_for_link', ['name' => $linkType->name]); + $subTitle = (string) trans('firefly.overview_for_link', ['name' => $linkType->name]); $subTitleIcon = 'fa-link'; $links = $this->repository->getJournalLinks($linkType); @@ -211,9 +211,9 @@ class LinkController extends Controller Log::channel('audit')->info('User stored new link type.', $linkType->toArray()); - $request->session()->flash('success', (string)trans('firefly.stored_new_link_type', ['name' => $linkType->name])); + $request->session()->flash('success', (string) trans('firefly.stored_new_link_type', ['name' => $linkType->name])); $redirect = redirect($this->getPreviousUrl('link-types.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { // set value so create routine will not overwrite URL: $request->session()->put('link-types.create.fromStore', true); @@ -232,7 +232,7 @@ class LinkController extends Controller public function update(LinkTypeFormRequest $request, LinkType $linkType) { if (!$linkType->editable) { - $request->session()->flash('error', (string)trans('firefly.cannot_edit_link_type', ['name' => e($linkType->name)])); + $request->session()->flash('error', (string) trans('firefly.cannot_edit_link_type', ['name' => e($linkType->name)])); return redirect(route('admin.links.index')); } @@ -246,10 +246,10 @@ class LinkController extends Controller Log::channel('audit')->info(sprintf('User update link type #%d.', $linkType->id), $data); - $request->session()->flash('success', (string)trans('firefly.updated_link_type', ['name' => $linkType->name])); + $request->session()->flash('success', (string) trans('firefly.updated_link_type', ['name' => $linkType->name])); app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('link-types.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { // set value so edit routine will not overwrite URL: $request->session()->put('link-types.edit.fromUpdate', true); diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php new file mode 100644 index 0000000000..39351c6e09 --- /dev/null +++ b/app/Http/Controllers/Admin/NotificationController.php @@ -0,0 +1,144 @@ +info('User visits notifications index.'); + $title = (string) trans('firefly.administration'); + $mainTitleIcon = 'fa-hand-spock-o'; + $subTitle = (string) trans('firefly.title_owner_notifications'); + $subTitleIcon = 'envelope-o'; + + // notification settings: + $slackUrl = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; + $pushoverAppToken = app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data; + $pushoverUserToken = app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data; + $ntfyServer = app('fireflyconfig')->getEncrypted('ntfy_server', 'https://ntfy.sh')->data; + $ntfyTopic = app('fireflyconfig')->getEncrypted('ntfy_topic', '')->data; + $ntfyAuth = app('fireflyconfig')->get('ntfy_auth', false)->data; + $ntfyUser = app('fireflyconfig')->getEncrypted('ntfy_user', '')->data; + $ntfyPass = app('fireflyconfig')->getEncrypted('ntfy_pass', '')->data; + $channels = config('notifications.channels'); + $forcedAvailability = []; + + // admin notification settings: + $notifications = []; + foreach (config('notifications.notifications.owner') as $key => $info) { + if ($info['enabled']) { + $notifications[$key] = app('fireflyconfig')->get(sprintf('notification_%s', $key), true)->data; + } + } + + // loop all channels to see if they are available. + foreach ($channels as $channel => $info) { + $forcedAvailability[$channel] = true; + } + $forcedAvailability['ntfy'] = '' !== $ntfyTopic; + $forcedAvailability['pushover'] = '' !== $pushoverAppToken && '' !== $pushoverUserToken; + + return view( + 'admin.notifications.index', + compact( + 'title', + 'subTitle', + 'forcedAvailability', + 'mainTitleIcon', + 'subTitleIcon', + 'channels', + 'slackUrl', + 'notifications', + 'pushoverAppToken', + 'pushoverUserToken', + 'ntfyServer', + 'ntfyTopic', + 'ntfyAuth', + 'ntfyUser', + 'ntfyPass' + ) + ); + } + + public function postIndex(NotificationRequest $request): RedirectResponse + { + $all = $request->getAll(); + + foreach (config('notifications.notifications.owner') as $key => $info) { + if (array_key_exists($key, $all)) { + app('fireflyconfig')->set(sprintf('notification_%s', $key), $all[$key]); + } + } + $variables = ['slack_webhook_url', 'pushover_app_token', 'pushover_user_token', 'ntfy_server', 'ntfy_topic', 'ntfy_user', 'ntfy_pass']; + foreach ($variables as $variable) { + if ('' === $all[$variable]) { + app('fireflyconfig')->delete($variable); + } + if ('' !== $all[$variable]) { + app('fireflyconfig')->setEncrypted($variable, $all[$variable]); + } + } + app('fireflyconfig')->set('ntfy_auth', $all['ntfy_auth'] ?? false); + + + session()->flash('success', (string) trans('firefly.notification_settings_saved')); + + return redirect(route('admin.notification.index')); + } + + public function testNotification(Request $request): RedirectResponse + { + + $all = $request->all(); + $channel = $all['test_submit'] ?? ''; + + switch ($channel) { + default: + session()->flash('error', (string) trans('firefly.notification_test_failed', ['channel' => $channel])); + + break; + + case 'email': + case 'slack': + case 'pushover': + case 'ntfy': + $owner = new OwnerNotifiable(); + app('log')->debug(sprintf('Now in testNotification("%s") controller.', $channel)); + event(new OwnerTestNotificationChannel($channel, $owner)); + session()->flash('success', (string) trans('firefly.notification_test_executed', ['channel' => $channel])); + } + + return redirect(route('admin.notification.index')); + } +} diff --git a/app/Http/Controllers/Admin/UpdateController.php b/app/Http/Controllers/Admin/UpdateController.php index b335ed9d07..4a83da8ad9 100644 --- a/app/Http/Controllers/Admin/UpdateController.php +++ b/app/Http/Controllers/Admin/UpdateController.php @@ -47,7 +47,7 @@ class UpdateController extends Controller parent::__construct(); $this->middleware( static function ($request, $next) { - app('view')->share('title', (string)trans('firefly.administration')); + app('view')->share('title', (string) trans('firefly.administration')); app('view')->share('mainTitleIcon', 'fa-hand-spock-o'); return $next($request); @@ -63,22 +63,22 @@ class UpdateController extends Controller */ public function index() { - $subTitle = (string)trans('firefly.update_check_title'); + $subTitle = (string) trans('firefly.update_check_title'); $subTitleIcon = 'fa-star'; $permission = app('fireflyconfig')->get('permission_update_check', -1); $channel = app('fireflyconfig')->get('update_channel', 'stable'); $selected = $permission->data; $channelSelected = $channel->data; $options = [ - -1 => (string)trans('firefly.updates_ask_me_later'), - 0 => (string)trans('firefly.updates_do_not_check'), - 1 => (string)trans('firefly.updates_enable_check'), + -1 => (string) trans('firefly.updates_ask_me_later'), + 0 => (string) trans('firefly.updates_do_not_check'), + 1 => (string) trans('firefly.updates_enable_check'), ]; $channelOptions = [ - 'stable' => (string)trans('firefly.update_channel_stable'), - 'beta' => (string)trans('firefly.update_channel_beta'), - 'alpha' => (string)trans('firefly.update_channel_alpha'), + 'stable' => (string) trans('firefly.update_channel_stable'), + 'beta' => (string) trans('firefly.update_channel_beta'), + 'alpha' => (string) trans('firefly.update_channel_alpha'), ]; return view('admin.update.index', compact('subTitle', 'subTitleIcon', 'selected', 'options', 'channelSelected', 'channelOptions')); @@ -91,14 +91,14 @@ class UpdateController extends Controller */ public function post(Request $request) { - $checkForUpdates = (int)$request->get('check_for_updates'); + $checkForUpdates = (int) $request->get('check_for_updates'); $channel = $request->get('update_channel'); $channel = in_array($channel, ['stable', 'beta', 'alpha'], true) ? $channel : 'stable'; app('fireflyconfig')->set('permission_update_check', $checkForUpdates); app('fireflyconfig')->set('last_update_check', time()); app('fireflyconfig')->set('update_channel', $channel); - session()->flash('success', (string)trans('firefly.configuration_updated')); + session()->flash('success', (string) trans('firefly.configuration_updated')); return redirect(route('admin.update-check')); } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 6c060b3bb5..dc8ed34398 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -55,7 +55,7 @@ class UserController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.administration')); + app('view')->share('title', (string) trans('firefly.administration')); app('view')->share('mainTitleIcon', 'fa-hand-spock-o'); $this->repository = app(UserRepositoryInterface::class); @@ -77,7 +77,7 @@ class UserController extends Controller return redirect(route('admin.users')); } - $subTitle = (string)trans('firefly.delete_user', ['email' => $user->email]); + $subTitle = (string) trans('firefly.delete_user', ['email' => $user->email]); return view('admin.users.delete', compact('user', 'subTitle')); } @@ -111,7 +111,7 @@ class UserController extends Controller return redirect(route('admin.users')); } $this->repository->destroy($user); - session()->flash('success', (string)trans('firefly.user_deleted')); + session()->flash('success', (string) trans('firefly.user_deleted')); return redirect(route('admin.users')); } @@ -133,15 +133,15 @@ class UserController extends Controller } session()->forget('users.edit.fromUpdate'); - $subTitle = (string)trans('firefly.edit_user', ['email' => $user->email]); + $subTitle = (string) trans('firefly.edit_user', ['email' => $user->email]); $subTitleIcon = 'fa-user-o'; $currentUser = auth()->user(); $isAdmin = $this->repository->hasRole($user, 'owner'); $codes = [ - '' => (string)trans('firefly.no_block_code'), - 'bounced' => (string)trans('firefly.block_code_bounced'), - 'expired' => (string)trans('firefly.block_code_expired'), - 'email_changed' => (string)trans('firefly.block_code_email_changed'), + '' => (string) trans('firefly.no_block_code'), + 'bounced' => (string) trans('firefly.block_code_bounced'), + 'expired' => (string) trans('firefly.block_code_expired'), + 'email_changed' => (string) trans('firefly.block_code_email_changed'), ]; return view('admin.users.edit', compact('user', 'canEditDetails', 'subTitle', 'subTitleIcon', 'codes', 'currentUser', 'isAdmin')); @@ -154,10 +154,10 @@ class UserController extends Controller */ public function index() { - $subTitle = (string)trans('firefly.user_administration'); + $subTitle = (string) trans('firefly.user_administration'); $subTitleIcon = 'fa-users'; $users = $this->repository->all(); - $singleUserMode = (bool)app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; + $singleUserMode = (bool) app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; $allowInvites = false; if (!$this->externalIdentity && $singleUserMode) { // also registration enabled. @@ -179,7 +179,7 @@ class UserController extends Controller public function invite(InviteUserFormRequest $request): RedirectResponse { - $address = (string)$request->get('invited_user'); + $address = (string) $request->get('invited_user'); $invitee = $this->repository->inviteUser(auth()->user(), $address); session()->flash('info', trans('firefly.user_is_invited', ['address' => $address])); @@ -196,9 +196,9 @@ class UserController extends Controller */ public function show(User $user) { - $title = (string)trans('firefly.administration'); + $title = (string) trans('firefly.administration'); $mainTitleIcon = 'fa-hand-spock-o'; - $subTitle = (string)trans('firefly.single_user_administration', ['email' => $user->email]); + $subTitle = (string) trans('firefly.single_user_administration', ['email' => $user->email]); $subTitleIcon = 'fa-user'; $information = $this->repository->getUserData($user); @@ -242,10 +242,10 @@ class UserController extends Controller $this->repository->changeStatus($user, $data['blocked'], $data['blocked_code']); $this->repository->updateEmail($user, $data['email']); - session()->flash('success', (string)trans('firefly.updated_user', ['email' => $user->email])); + session()->flash('success', (string) trans('firefly.updated_user', ['email' => $user->email])); app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('users.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { session()->put('users.edit.fromUpdate', true); $redirect = redirect(route('admin.users.edit', [$user->id]))->withInput(['return_to_edit' => 1]); diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index 2ae7fa576a..c45b947126 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -53,7 +53,7 @@ class AttachmentController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-paperclip'); - app('view')->share('title', (string)trans('firefly.attachments')); + app('view')->share('title', (string) trans('firefly.attachments')); $this->repository = app(AttachmentRepositoryInterface::class); return $next($request); @@ -68,7 +68,7 @@ class AttachmentController extends Controller */ public function delete(Attachment $attachment) { - $subTitle = (string)trans('firefly.delete_attachment', ['name' => $attachment->filename]); + $subTitle = (string) trans('firefly.delete_attachment', ['name' => $attachment->filename]); // put previous url in session $this->rememberPreviousUrl('attachments.delete.url'); @@ -87,7 +87,7 @@ class AttachmentController extends Controller $this->repository->destroy($attachment); - $request->session()->flash('success', (string)trans('firefly.attachment_deleted', ['name' => $name])); + $request->session()->flash('success', (string) trans('firefly.attachment_deleted', ['name' => $name])); app('preferences')->mark(); return redirect($this->getPreviousUrl('attachments.delete.url')); @@ -117,7 +117,7 @@ class AttachmentController extends Controller ->header('Expires', '0') ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') ->header('Pragma', 'public') - ->header('Content-Length', (string)strlen($content)) + ->header('Content-Length', (string) strlen($content)) ; return $response; @@ -134,7 +134,7 @@ class AttachmentController extends Controller public function edit(Request $request, Attachment $attachment) { $subTitleIcon = 'fa-pencil'; - $subTitle = (string)trans('firefly.edit_attachment', ['name' => $attachment->filename]); + $subTitle = (string) trans('firefly.edit_attachment', ['name' => $attachment->filename]); // put previous url in session if not redirect from store (not "return_to_edit"). if (true !== session('attachments.edit.fromUpdate')) { @@ -176,11 +176,11 @@ class AttachmentController extends Controller $data = $request->getAttachmentData(); $this->repository->update($attachment, $data); - $request->session()->flash('success', (string)trans('firefly.attachment_updated', ['name' => $attachment->filename])); + $request->session()->flash('success', (string) trans('firefly.attachment_updated', ['name' => $attachment->filename])); app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('attachments.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { $request->session()->put('attachments.edit.fromUpdate', true); $redirect = redirect(route('attachments.edit', [$attachment->id]))->withInput(['return_to_edit' => 1]); diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 3b04ae1f7a..de924f295b 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -79,7 +79,7 @@ class ForgotPasswordController extends Controller $user = User::where('email', $request->get('email'))->first(); if (null !== $user && $repository->hasRole($user, 'demo')) { - return back()->withErrors(['email' => (string)trans('firefly.cannot_reset_demo_user')]); + return back()->withErrors(['email' => (string) trans('firefly.cannot_reset_demo_user')]); } // We will send the password reset link to this user. Once we have attempted @@ -101,7 +101,7 @@ class ForgotPasswordController extends Controller */ private function validateHost(): void { - $configuredHost = parse_url((string)config('app.url'), PHP_URL_HOST); + $configuredHost = parse_url((string) config('app.url'), PHP_URL_HOST); if (false === $configuredHost || null === $configuredHost) { throw new FireflyException('Please set a valid and correct Firefly III URL in the APP_URL environment variable.'); } @@ -132,7 +132,7 @@ class ForgotPasswordController extends Controller $singleUserMode = app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; $userCount = User::count(); $allowRegistration = true; - $pageTitle = (string)trans('firefly.forgot_pw_page_title'); + $pageTitle = (string) trans('firefly.forgot_pw_page_title'); if (true === $singleUserMode && $userCount > 0) { $allowRegistration = false; } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index fa2b6c922c..6496591e6b 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -25,9 +25,12 @@ namespace FireflyIII\Http\Controllers\Auth; use Cookie; use FireflyIII\Events\ActuallyLoggedIn; +use FireflyIII\Events\Security\UnknownUserAttemptedLogin; +use FireflyIII\Events\Security\UserAttemptedLogin; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Providers\RouteServiceProvider; +use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\View; @@ -56,7 +59,8 @@ class LoginController extends Controller /** * Where to redirect users after login. */ - protected string $redirectTo = RouteServiceProvider::HOME; + protected string $redirectTo = RouteServiceProvider::HOME; + private UserRepositoryInterface $repository; private string $username; @@ -66,8 +70,9 @@ class LoginController extends Controller public function __construct() { parent::__construct(); - $this->username = 'email'; + $this->username = 'email'; $this->middleware('guest')->except('logout'); + $this->repository = app(UserRepositoryInterface::class); } /** @@ -122,6 +127,15 @@ class LoginController extends Controller return $this->sendLoginResponse($request); } app('log')->warning('Login attempt failed.'); + $username = (string) $request->get($this->username()); + $user = $this->repository->findByEmail($username); + if (null === $user) { + // send event to owner. + event(new UnknownUserAttemptedLogin($username)); + } + if (null !== $user) { + event(new UserAttemptedLogin($user)); + } // Copied directly from AuthenticatesUsers, but with logging added: // If the login attempt was unsuccessful we will increment the number of attempts @@ -211,7 +225,7 @@ class LoginController extends Controller $count = \DB::table('users')->count(); $guard = config('auth.defaults.guard'); - $title = (string)trans('firefly.login_page_title'); + $title = (string) trans('firefly.login_page_title'); if (0 === $count && 'web' === $guard) { return redirect(route('register')); diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index b84453522f..effbd4f322 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -26,6 +26,7 @@ namespace FireflyIII\Http\Controllers\Auth; use FireflyIII\Events\RegisteredUser; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Support\Http\Controllers\CreateStuff; use FireflyIII\User; @@ -83,7 +84,7 @@ class RegisterController extends Controller public function register(Request $request) { $allowRegistration = $this->allowedToRegister(); - $inviteCode = (string)$request->get('invite_code'); + $inviteCode = (string) $request->get('invite_code'); $repository = app(UserRepositoryInterface::class); $validCode = $repository->validateInviteCode($inviteCode); @@ -94,11 +95,12 @@ class RegisterController extends Controller $this->validator($request->all())->validate(); $user = $this->createUser($request->all()); app('log')->info(sprintf('Registered new user %s', $user->email)); - event(new RegisteredUser($user)); + $owner = new OwnerNotifiable(); + event(new RegisteredUser($owner, $user)); $this->guard()->login($user); - session()->flash('success', (string)trans('firefly.registered')); + session()->flash('success', (string) trans('firefly.registered')); $this->registered($request, $user); @@ -144,7 +146,7 @@ class RegisterController extends Controller public function showInviteForm(Request $request, string $code) { $isDemoSite = app('fireflyconfig')->get('is_demo_site', config('firefly.configuration.is_demo_site'))->data; - $pageTitle = (string)trans('firefly.register_page_title'); + $pageTitle = (string) trans('firefly.register_page_title'); $repository = app(UserRepositoryInterface::class); $allowRegistration = $this->allowedToRegister(); $inviteCode = $code; @@ -176,7 +178,7 @@ class RegisterController extends Controller public function showRegistrationForm(Request $request) { $isDemoSite = app('fireflyconfig')->get('is_demo_site', config('firefly.configuration.is_demo_site'))->data; - $pageTitle = (string)trans('firefly.register_page_title'); + $pageTitle = (string) trans('firefly.register_page_title'); $allowRegistration = $this->allowedToRegister(); if (false === $allowRegistration) { diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 09982b2063..442d4956b4 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -130,7 +130,7 @@ class ResetPasswordController extends Controller $singleUserMode = app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; $userCount = User::count(); $allowRegistration = true; - $pageTitle = (string)trans('firefly.reset_pw_page_title'); + $pageTitle = (string) trans('firefly.reset_pw_page_title'); if (true === $singleUserMode && $userCount > 0) { $allowRegistration = false; } diff --git a/app/Http/Controllers/Auth/TwoFactorController.php b/app/Http/Controllers/Auth/TwoFactorController.php index d868d2823f..b1e7e053a2 100644 --- a/app/Http/Controllers/Auth/TwoFactorController.php +++ b/app/Http/Controllers/Auth/TwoFactorController.php @@ -163,6 +163,22 @@ class TwoFactorController extends Controller app('preferences')->set('mfa_history', $newHistory); } + private function addToMFAFailureCounter(): void + { + $preference = (int) app('preferences')->get('mfa_failure_count', 0)->data; + ++$preference; + Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference)); + app('preferences')->set('mfa_failure_count', $preference); + } + + private function getMFAFailureCounter(): int + { + $value = (int) app('preferences')->get('mfa_failure_count', 0)->data; + Log::channel('audit')->info(sprintf('MFA failure count is %d.', $value)); + + return $value; + } + private function addToMFAHistory(string $mfaCode): void { /** @var array $mfaHistory */ @@ -177,6 +193,12 @@ class TwoFactorController extends Controller $this->filterMFAHistory(); } + private function resetMFAFailureCounter(): void + { + app('preferences')->set('mfa_failure_count', 0); + Log::channel('audit')->info('MFA failure count is set to zero.'); + } + /** * Checks if code is in users backup codes. */ @@ -219,26 +241,4 @@ class TwoFactorController extends Controller app('preferences')->set('mfa_recovery', $newList); } - - private function addToMFAFailureCounter(): void - { - $preference = (int) app('preferences')->get('mfa_failure_count', 0)->data; - ++$preference; - Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference)); - app('preferences')->set('mfa_failure_count', $preference); - } - - private function getMFAFailureCounter(): int - { - $value = (int) app('preferences')->get('mfa_failure_count', 0)->data; - Log::channel('audit')->info(sprintf('MFA failure count is %d.', $value)); - - return $value; - } - - private function resetMFAFailureCounter(): void - { - app('preferences')->set('mfa_failure_count', 0); - Log::channel('audit')->info('MFA failure count is set to zero.'); - } } diff --git a/app/Http/Controllers/Bill/CreateController.php b/app/Http/Controllers/Bill/CreateController.php index f55075856d..664f7d8689 100644 --- a/app/Http/Controllers/Bill/CreateController.php +++ b/app/Http/Controllers/Bill/CreateController.php @@ -52,7 +52,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.bills')); + app('view')->share('title', (string) trans('firefly.bills')); app('view')->share('mainTitleIcon', 'fa-calendar-o'); $this->attachments = app(AttachmentHelperInterface::class); $this->repository = app(BillRepositoryInterface::class); @@ -74,10 +74,10 @@ class CreateController extends Controller /** @var array $billPeriods */ $billPeriods = config('firefly.bill_periods'); foreach ($billPeriods as $current) { - $periods[$current] = (string)trans('firefly.repeat_freq_'.$current); + $periods[$current] = (string) trans('firefly.repeat_freq_'.$current); } - $subTitle = (string)trans('firefly.create_new_bill'); - $defaultCurrency = app('amount')->getDefaultCurrency(); + $subTitle = (string) trans('firefly.create_new_bill'); + $defaultCurrency = $this->defaultCurrency; // put previous url in session if not redirect from store (not "create another"). if (true !== session('bills.create.fromStore')) { @@ -101,13 +101,13 @@ class CreateController extends Controller $bill = $this->repository->store($billData); } catch (FireflyException $e) { app('log')->error($e->getMessage()); - $request->session()->flash('error', (string)trans('firefly.bill_store_error')); + $request->session()->flash('error', (string) trans('firefly.bill_store_error')); return redirect(route('bills.create'))->withInput(); } Log::channel('audit')->info('Stored new bill.', $billData); - $request->session()->flash('success', (string)trans('firefly.stored_new_bill', ['name' => $bill->name])); + $request->session()->flash('success', (string) trans('firefly.stored_new_bill', ['name' => $bill->name])); app('preferences')->mark(); /** @var null|array $files */ @@ -117,7 +117,7 @@ class CreateController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { diff --git a/app/Http/Controllers/Bill/DeleteController.php b/app/Http/Controllers/Bill/DeleteController.php index c69aa3d22f..6dccb056a6 100644 --- a/app/Http/Controllers/Bill/DeleteController.php +++ b/app/Http/Controllers/Bill/DeleteController.php @@ -51,7 +51,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.bills')); + app('view')->share('title', (string) trans('firefly.bills')); app('view')->share('mainTitleIcon', 'fa-calendar-o'); $this->repository = app(BillRepositoryInterface::class); @@ -69,7 +69,7 @@ class DeleteController extends Controller { // put previous url in session $this->rememberPreviousUrl('bills.delete.url'); - $subTitle = (string)trans('firefly.delete_bill', ['name' => $bill->name]); + $subTitle = (string) trans('firefly.delete_bill', ['name' => $bill->name]); return view('bills.delete', compact('bill', 'subTitle')); } @@ -84,7 +84,7 @@ class DeleteController extends Controller $name = $bill->name; $this->repository->destroy($bill); - $request->session()->flash('success', (string)trans('firefly.deleted_bill', ['name' => $name])); + $request->session()->flash('success', (string) trans('firefly.deleted_bill', ['name' => $name])); app('preferences')->mark(); return redirect($this->getPreviousUrl('bills.delete.url')); diff --git a/app/Http/Controllers/Bill/EditController.php b/app/Http/Controllers/Bill/EditController.php index 73909da27d..c3f7b24969 100644 --- a/app/Http/Controllers/Bill/EditController.php +++ b/app/Http/Controllers/Bill/EditController.php @@ -52,7 +52,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.bills')); + app('view')->share('title', (string) trans('firefly.bills')); app('view')->share('mainTitleIcon', 'fa-calendar-o'); $this->attachments = app(AttachmentHelperInterface::class); $this->repository = app(BillRepositoryInterface::class); @@ -75,21 +75,20 @@ class EditController extends Controller $billPeriods = config('firefly.bill_periods'); foreach ($billPeriods as $current) { - $periods[$current] = (string)trans('firefly.'.$current); + $periods[$current] = (string) trans('firefly.'.$current); } - $subTitle = (string)trans('firefly.edit_bill', ['name' => $bill->name]); + $subTitle = (string) trans('firefly.edit_bill', ['name' => $bill->name]); // put previous url in session if not redirect from store (not "return_to_edit"). if (true !== session('bills.edit.fromUpdate')) { $this->rememberPreviousUrl('bills.edit.url'); } - $currency = app('amount')->getDefaultCurrency(); - $bill->amount_min = app('steam')->bcround($bill->amount_min, $currency->decimal_places); - $bill->amount_max = app('steam')->bcround($bill->amount_max, $currency->decimal_places); + $bill->amount_min = app('steam')->bcround($bill->amount_min, $bill->transactionCurrency->decimal_places); + $bill->amount_max = app('steam')->bcround($bill->amount_max, $bill->transactionCurrency->decimal_places); $rules = $this->repository->getRulesForBill($bill); - $defaultCurrency = app('amount')->getDefaultCurrency(); + $defaultCurrency = $this->defaultCurrency; // code to handle active-checkboxes $hasOldInput = null !== $request->old('_token'); @@ -99,7 +98,7 @@ class EditController extends Controller 'extension_date' => $bill->extension_date, 'notes' => $this->repository->getNoteText($bill), 'transaction_currency_id' => $bill->transaction_currency_id, - 'active' => $hasOldInput ? (bool)$request->old('active') : $bill->active, + 'active' => $hasOldInput ? (bool) $request->old('active') : $bill->active, 'object_group' => null !== $bill->objectGroups->first() ? $bill->objectGroups->first()->title : '', ]; @@ -119,7 +118,7 @@ class EditController extends Controller Log::channel('audit')->info(sprintf('Updated bill #%d.', $bill->id), $billData); - $request->session()->flash('success', (string)trans('firefly.updated_bill', ['name' => $bill->name])); + $request->session()->flash('success', (string) trans('firefly.updated_bill', ['name' => $bill->name])); app('preferences')->mark(); /** @var null|array $files */ @@ -129,7 +128,7 @@ class EditController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } // flash messages @@ -138,7 +137,7 @@ class EditController extends Controller } $redirect = redirect($this->getPreviousUrl('bills.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { $request->session()->put('bills.edit.fromUpdate', true); $redirect = redirect(route('bills.edit', [$bill->id]))->withInput(['return_to_edit' => 1]); diff --git a/app/Http/Controllers/Bill/IndexController.php b/app/Http/Controllers/Bill/IndexController.php index b78df5289c..cded2adb12 100644 --- a/app/Http/Controllers/Bill/IndexController.php +++ b/app/Http/Controllers/Bill/IndexController.php @@ -55,7 +55,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.bills')); + app('view')->share('title', (string) trans('firefly.bills')); app('view')->share('mainTitleIcon', 'fa-calendar-o'); $this->repository = app(BillRepositoryInterface::class); @@ -71,32 +71,33 @@ class IndexController extends Controller { $this->cleanupObjectGroups(); $this->repository->correctOrder(); - $start = session('start'); - $end = session('end'); - $collection = $this->repository->getBills(); - $total = $collection->count(); + $start = session('start'); + $end = session('end'); + $collection = $this->repository->getBills(); + $total = $collection->count(); - $defaultCurrency = app('amount')->getDefaultCurrency(); - $parameters = new ParameterBag(); + $parameters = new ParameterBag(); // sub one day from temp start so the last paid date is one day before it should be. - $tempStart = clone $start; + $tempStart = clone $start; // 2023-06-23 do not sub one day from temp start, fix is in BillTransformer::payDates instead // $tempStart->subDay(); $parameters->set('start', $tempStart); $parameters->set('end', $end); + $parameters->set('convertToNative', $this->convertToNative); + $parameters->set('defaultCurrency', $this->defaultCurrency); /** @var BillTransformer $transformer */ - $transformer = app(BillTransformer::class); + $transformer = app(BillTransformer::class); $transformer->setParameters($parameters); // loop all bills, convert to array and add rules and stuff. - $rules = $this->repository->getRulesForBills($collection); + $rules = $this->repository->getRulesForBills($collection); // make bill groups: - $bills = [ + $bills = [ 0 => [ // the index is the order, not the ID. 'object_group_id' => 0, - 'object_group_title' => (string)trans('firefly.default_group_title_name'), + 'object_group_title' => (string) trans('firefly.default_group_title_name'), 'bills' => [], ], ]; @@ -104,7 +105,7 @@ class IndexController extends Controller /** @var Bill $bill */ foreach ($collection as $bill) { $array = $transformer->transform($bill); - $groupOrder = (int)$array['object_group_order']; + $groupOrder = (int) $array['object_group_order']; // make group array if necessary: $bills[$groupOrder] ??= [ 'object_group_id' => $array['object_group_id'], @@ -112,7 +113,7 @@ class IndexController extends Controller 'bills' => [], ]; - $currency = $bill->transactionCurrency ?? $defaultCurrency; + $currency = $bill->transactionCurrency ?? $this->defaultCurrency; $array['currency_id'] = $currency->id; $array['currency_name'] = $currency->name; $array['currency_symbol'] = $currency->symbol; @@ -122,14 +123,13 @@ class IndexController extends Controller $array['rules'] = $rules[$bill['id']] ?? []; $bills[$groupOrder]['bills'][] = $array; } - // order by key ksort($bills); // summarise per currency / per group. - $sums = $this->getSums($bills); - $totals = $this->getTotals($sums); - $today = now()->startOfDay(); + $sums = $this->getSums($bills); + $totals = $this->getTotals($sums); + $today = now()->startOfDay(); return view('bills.index', compact('bills', 'sums', 'total', 'totals', 'today')); } @@ -164,8 +164,8 @@ class IndexController extends Controller // only fill in avg when bill is active. if (null !== $bill['next_expected_match']) { - $avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2'); - $avg = bcmul($avg, (string)count($bill['pay_dates'])); + $avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2'); + $avg = bcmul($avg, (string) count($bill['pay_dates'])); $sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg); } // fill in per period regardless: @@ -178,7 +178,7 @@ class IndexController extends Controller private function amountPerPeriod(array $bill, string $range): string { - $avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2'); + $avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2'); app('log')->debug(sprintf('Amount per period for bill #%d "%s"', $bill['id'], $bill['name'])); app('log')->debug(sprintf('Average is %s', $avg)); @@ -191,8 +191,8 @@ class IndexController extends Controller 'weekly' => '52.17', 'daily' => '365.24', ]; - $yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string)($bill['skip'] + 1))); - app('log')->debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string)($bill['skip'] + 1))); + $yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1))); + app('log')->debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1))); // per period: $division = [ @@ -256,8 +256,8 @@ class IndexController extends Controller */ public function setOrder(Request $request, Bill $bill): JsonResponse { - $objectGroupTitle = (string)$request->get('objectGroupTitle'); - $newOrder = (int)$request->get('order'); + $objectGroupTitle = (string) $request->get('objectGroupTitle'); + $newOrder = (int) $request->get('order'); $this->repository->setOrder($bill, $newOrder); if ('' !== $objectGroupTitle) { $this->repository->setObjectGroup($bill, $objectGroupTitle); diff --git a/app/Http/Controllers/Bill/ShowController.php b/app/Http/Controllers/Bill/ShowController.php index 6fcd5b2625..a50540e6b3 100644 --- a/app/Http/Controllers/Bill/ShowController.php +++ b/app/Http/Controllers/Bill/ShowController.php @@ -62,7 +62,7 @@ class ShowController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.bills')); + app('view')->share('title', (string) trans('firefly.bills')); app('view')->share('mainTitleIcon', 'fa-calendar-o'); $this->repository = app(BillRepositoryInterface::class); @@ -80,13 +80,13 @@ class ShowController extends Controller { $total = 0; if (false === $bill->active) { - $request->session()->flash('warning', (string)trans('firefly.cannot_scan_inactive_bill')); + $request->session()->flash('warning', (string) trans('firefly.cannot_scan_inactive_bill')); return redirect(route('bills.show', [$bill->id])); } $set = $this->repository->getRulesForBill($bill); if (0 === $set->count()) { - $request->session()->flash('error', (string)trans('firefly.no_rules_for_bill')); + $request->session()->flash('error', (string) trans('firefly.no_rules_for_bill')); return redirect(route('bills.show', [$bill->id])); } @@ -125,8 +125,8 @@ class ShowController extends Controller /** @var Carbon $end */ $end = session('end'); $year = $start->year; - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $yearAverage = $this->repository->getYearAverage($bill, $start); $overallAverage = $this->repository->getOverallAverage($bill); $manager = new Manager(); diff --git a/app/Http/Controllers/Budget/BudgetLimitController.php b/app/Http/Controllers/Budget/BudgetLimitController.php index effd7ac2cb..4adc1a76ab 100644 --- a/app/Http/Controllers/Budget/BudgetLimitController.php +++ b/app/Http/Controllers/Budget/BudgetLimitController.php @@ -63,7 +63,7 @@ class BudgetLimitController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.budgets')); + app('view')->share('title', (string) trans('firefly.budgets')); app('view')->share('mainTitleIcon', 'fa-pie-chart'); $this->repository = app(BudgetRepositoryInterface::class); $this->opsRepository = app(OperationsRepositoryInterface::class); @@ -112,6 +112,26 @@ class BudgetLimitController extends Controller return redirect(route('budgets.index')); } + /** + * @return Factory|View + */ + public function edit(BudgetLimit $budgetLimit) + { + $notes = $this->blRepository->getNoteText($budgetLimit); + + return view('budgets.budget-limits.edit', compact('budgetLimit', 'notes')); + } + + /** + * @return Factory|View + */ + public function show(BudgetLimit $budgetLimit) + { + $notes = $this->blRepository->getNoteText($budgetLimit); + + return view('budgets.budget-limits.show', compact('budgetLimit', 'notes')); + } + /** * TODO why redirect AND json response? * @@ -121,8 +141,8 @@ class BudgetLimitController extends Controller { app('log')->debug('Going to store new budget-limit.', $request->all()); // first search for existing one and update it if necessary. - $currency = $this->currencyRepos->find((int)$request->get('transaction_currency_id')); - $budget = $this->repository->find((int)$request->get('budget_id')); + $currency = $this->currencyRepos->find((int) $request->get('transaction_currency_id')); + $budget = $this->repository->find((int) $request->get('budget_id')); if (null === $currency || null === $budget) { throw new FireflyException('No valid currency or budget.'); } @@ -133,7 +153,7 @@ class BudgetLimitController extends Controller return response()->json([]); } - $amount = (string)$request->get('amount'); + $amount = (string) $request->get('amount'); $start->startOfDay(); $end->startOfDay(); @@ -154,7 +174,7 @@ class BudgetLimitController extends Controller // return empty=ish array: return response()->json([]); } - if ((int)$amount > 268435456) { // intentional cast to integer + if ((int) $amount > 268435456) { // intentional cast to integer $amount = '268435456'; } if (-1 === bccomp($amount, '0')) { @@ -169,13 +189,16 @@ class BudgetLimitController extends Controller $limit = $this->blRepository->store( [ 'budget_id' => $request->get('budget_id'), - 'currency_id' => (int)$request->get('transaction_currency_id'), + 'currency_id' => (int) $request->get('transaction_currency_id'), 'start_date' => $start, 'end_date' => $end, 'amount' => $amount, ] ); } + // parse notes, if any. + $notes = (string) $request->get('notes'); + $this->blRepository->setNoteText($limit, $notes); if ($request->expectsJson()) { $array = $limit->toArray(); @@ -184,26 +207,29 @@ class BudgetLimitController extends Controller $array['spent'] = $spentArr[$currency->id]['sum'] ?? '0'; $array['left_formatted'] = app('amount')->formatAnything($limit->transactionCurrency, bcadd($array['spent'], $array['amount'])); $array['amount_formatted'] = app('amount')->formatAnything($limit->transactionCurrency, $limit['amount']); - $array['days_left'] = (string)$this->activeDaysLeft($start, $end); + $array['days_left'] = (string) $this->activeDaysLeft($start, $end); // left per day: $array['left_per_day'] = 0 === bccomp('0', $array['days_left']) ? bcadd($array['spent'], $array['amount']) : bcdiv(bcadd($array['spent'], $array['amount']), $array['days_left']); // left per day formatted. $array['left_per_day_formatted'] = app('amount')->formatAnything($limit->transactionCurrency, $array['left_per_day']); + // notes: + $array['notes'] = $this->blRepository->getNoteText($limit); + return response()->json($array); } return redirect(route('budgets.index')); } - public function update(Request $request, BudgetLimit $budgetLimit): JsonResponse + public function update(Request $request, BudgetLimit $budgetLimit): JsonResponse|RedirectResponse { - $amount = (string)$request->get('amount'); + $amount = (string) $request->get('amount'); if ('' === $amount) { $amount = '0'; } - if ((int)$amount > 268435456) { // 268 million, intentional integer + if ((int) $amount > 268435456) { // 268 million, intentional integer $amount = '268435456'; } // sanity check on amount: @@ -224,8 +250,13 @@ class BudgetLimitController extends Controller if (-1 === bccomp($amount, '0')) { $amount = bcmul($amount, '-1'); } + $notes = (string) $request->get('notes'); + if (strlen($notes) > 32768) { + $notes = substr($notes, 0, 32768); + } - $limit = $this->blRepository->update($budgetLimit, ['amount' => $amount]); + + $limit = $this->blRepository->update($budgetLimit, ['amount' => $amount, 'notes' => $notes]); app('preferences')->mark(); $array = $limit->toArray(); @@ -240,12 +271,15 @@ class BudgetLimitController extends Controller $array['spent'] = $spentArr[$budgetLimit->transactionCurrency->id]['sum'] ?? '0'; $array['left_formatted'] = app('amount')->formatAnything($limit->transactionCurrency, bcadd($array['spent'], $array['amount'])); $array['amount_formatted'] = app('amount')->formatAnything($limit->transactionCurrency, $limit['amount']); - $array['days_left'] = (string)$daysLeft; + $array['days_left'] = (string) $daysLeft; $array['left_per_day'] = 0 === $daysLeft ? bcadd($array['spent'], $array['amount']) : bcdiv(bcadd($array['spent'], $array['amount']), $array['days_left']); // left per day formatted. $array['amount'] = app('steam')->bcround($limit['amount'], $limit->transactionCurrency->decimal_places); $array['left_per_day_formatted'] = app('amount')->formatAnything($limit->transactionCurrency, $array['left_per_day']); + if ('true' === $request->get('redirect')) { + return redirect(route('budgets.index')); + } return response()->json($array); } diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php index 49548813bc..026b3629e4 100644 --- a/app/Http/Controllers/Budget/CreateController.php +++ b/app/Http/Controllers/Budget/CreateController.php @@ -52,7 +52,7 @@ class CreateController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.budgets')); + app('view')->share('title', (string) trans('firefly.budgets')); app('view')->share('mainTitleIcon', 'fa-pie-chart'); $this->repository = app(BudgetRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); @@ -73,24 +73,23 @@ class CreateController extends Controller // auto budget types $autoBudgetTypes = [ - 0 => (string)trans('firefly.auto_budget_none'), - AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), - AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), - AutoBudget::AUTO_BUDGET_ADJUSTED => (string)trans('firefly.auto_budget_adjusted'), + 0 => (string) trans('firefly.auto_budget_none'), + AutoBudget::AUTO_BUDGET_RESET => (string) trans('firefly.auto_budget_reset'), + AutoBudget::AUTO_BUDGET_ROLLOVER => (string) trans('firefly.auto_budget_rollover'), + AutoBudget::AUTO_BUDGET_ADJUSTED => (string) trans('firefly.auto_budget_adjusted'), ]; $autoBudgetPeriods = [ - 'daily' => (string)trans('firefly.auto_budget_period_daily'), - 'weekly' => (string)trans('firefly.auto_budget_period_weekly'), - 'monthly' => (string)trans('firefly.auto_budget_period_monthly'), - 'quarterly' => (string)trans('firefly.auto_budget_period_quarterly'), - 'half_year' => (string)trans('firefly.auto_budget_period_half_year'), - 'yearly' => (string)trans('firefly.auto_budget_period_yearly'), + 'daily' => (string) trans('firefly.auto_budget_period_daily'), + 'weekly' => (string) trans('firefly.auto_budget_period_weekly'), + 'monthly' => (string) trans('firefly.auto_budget_period_monthly'), + 'quarterly' => (string) trans('firefly.auto_budget_period_quarterly'), + 'half_year' => (string) trans('firefly.auto_budget_period_half_year'), + 'yearly' => (string) trans('firefly.auto_budget_period_yearly'), ]; - $currency = app('amount')->getDefaultCurrency(); $preFilled = [ - 'auto_budget_period' => $hasOldInput ? (bool)$request->old('auto_budget_period') : 'monthly', - 'auto_budget_currency_id' => $hasOldInput ? (int)$request->old('auto_budget_currency_id') : $currency->id, + 'auto_budget_period' => $hasOldInput ? (bool) $request->old('auto_budget_period') : 'monthly', + 'auto_budget_currency_id' => $hasOldInput ? (int) $request->old('auto_budget_currency_id') : $this->defaultCurrency->id, ]; $request->session()->flash('preFilled', $preFilled); @@ -100,7 +99,7 @@ class CreateController extends Controller $this->rememberPreviousUrl('budgets.create.url'); } $request->session()->forget('budgets.create.fromStore'); - $subTitle = (string)trans('firefly.create_new_budget'); + $subTitle = (string) trans('firefly.create_new_budget'); return view('budgets.create', compact('subTitle', 'autoBudgetTypes', 'autoBudgetPeriods')); } @@ -116,7 +115,7 @@ class CreateController extends Controller $budget = $this->repository->store($data); $this->repository->cleanupBudgets(); - $request->session()->flash('success', (string)trans('firefly.stored_new_budget', ['name' => $budget->name])); + $request->session()->flash('success', (string) trans('firefly.stored_new_budget', ['name' => $budget->name])); app('preferences')->mark(); Log::channel('audit')->info('Stored new budget.', $data); @@ -129,7 +128,7 @@ class CreateController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -138,7 +137,7 @@ class CreateController extends Controller $redirect = redirect($this->getPreviousUrl('budgets.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { $request->session()->put('budgets.create.fromStore', true); $redirect = redirect(route('budgets.create'))->withInput(); diff --git a/app/Http/Controllers/Budget/DeleteController.php b/app/Http/Controllers/Budget/DeleteController.php index ec9e1c538d..a938a06df4 100644 --- a/app/Http/Controllers/Budget/DeleteController.php +++ b/app/Http/Controllers/Budget/DeleteController.php @@ -50,7 +50,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.budgets')); + app('view')->share('title', (string) trans('firefly.budgets')); app('view')->share('mainTitleIcon', 'fa-pie-chart'); $this->repository = app(BudgetRepositoryInterface::class); @@ -66,7 +66,7 @@ class DeleteController extends Controller */ public function delete(Budget $budget) { - $subTitle = (string)trans('firefly.delete_budget', ['name' => $budget->name]); + $subTitle = (string) trans('firefly.delete_budget', ['name' => $budget->name]); // put previous url in session $this->rememberPreviousUrl('budgets.delete.url'); @@ -83,7 +83,7 @@ class DeleteController extends Controller { $name = $budget->name; $this->repository->destroy($budget); - $request->session()->flash('success', (string)trans('firefly.deleted_budget', ['name' => $name])); + $request->session()->flash('success', (string) trans('firefly.deleted_budget', ['name' => $name])); app('preferences')->mark(); return redirect($this->getPreviousUrl('budgets.delete.url')); diff --git a/app/Http/Controllers/Budget/EditController.php b/app/Http/Controllers/Budget/EditController.php index 72b2ff3f79..382dc3a756 100644 --- a/app/Http/Controllers/Budget/EditController.php +++ b/app/Http/Controllers/Budget/EditController.php @@ -53,7 +53,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.budgets')); + app('view')->share('title', (string) trans('firefly.budgets')); app('view')->share('mainTitleIcon', 'fa-pie-chart'); $this->repository = app(BudgetRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); @@ -70,38 +70,37 @@ class EditController extends Controller */ public function edit(Request $request, Budget $budget) { - $subTitle = (string)trans('firefly.edit_budget', ['name' => $budget->name]); + $subTitle = (string) trans('firefly.edit_budget', ['name' => $budget->name]); $autoBudget = $this->repository->getAutoBudget($budget); // auto budget types $autoBudgetTypes = [ - 0 => (string)trans('firefly.auto_budget_none'), - AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), - AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), - AutoBudget::AUTO_BUDGET_ADJUSTED => (string)trans('firefly.auto_budget_adjusted'), + 0 => (string) trans('firefly.auto_budget_none'), + AutoBudget::AUTO_BUDGET_RESET => (string) trans('firefly.auto_budget_reset'), + AutoBudget::AUTO_BUDGET_ROLLOVER => (string) trans('firefly.auto_budget_rollover'), + AutoBudget::AUTO_BUDGET_ADJUSTED => (string) trans('firefly.auto_budget_adjusted'), ]; $autoBudgetPeriods = [ - 'daily' => (string)trans('firefly.auto_budget_period_daily'), - 'weekly' => (string)trans('firefly.auto_budget_period_weekly'), - 'monthly' => (string)trans('firefly.auto_budget_period_monthly'), - 'quarterly' => (string)trans('firefly.auto_budget_period_quarterly'), - 'half_year' => (string)trans('firefly.auto_budget_period_half_year'), - 'yearly' => (string)trans('firefly.auto_budget_period_yearly'), + 'daily' => (string) trans('firefly.auto_budget_period_daily'), + 'weekly' => (string) trans('firefly.auto_budget_period_weekly'), + 'monthly' => (string) trans('firefly.auto_budget_period_monthly'), + 'quarterly' => (string) trans('firefly.auto_budget_period_quarterly'), + 'half_year' => (string) trans('firefly.auto_budget_period_half_year'), + 'yearly' => (string) trans('firefly.auto_budget_period_yearly'), ]; // code to handle active-checkboxes $hasOldInput = null !== $request->old('_token'); - $currency = app('amount')->getDefaultCurrency(); $preFilled = [ - 'active' => $hasOldInput ? (bool)$request->old('active') : $budget->active, - 'auto_budget_currency_id' => $hasOldInput ? (int)$request->old('auto_budget_currency_id') : $currency->id, + 'active' => $hasOldInput ? (bool) $request->old('active') : $budget->active, + 'auto_budget_currency_id' => $hasOldInput ? (int) $request->old('auto_budget_currency_id') : $this->defaultCurrency->id, ]; if (null !== $autoBudget) { $amount = $hasOldInput ? $request->old('auto_budget_amount') : $autoBudget->amount; if (is_array($amount)) { $amount = '0'; } - $amount = (string)$amount; + $amount = (string) $amount; $preFilled['auto_budget_amount'] = app('steam')->bcround($amount, $autoBudget->transactionCurrency->decimal_places); } @@ -123,7 +122,7 @@ class EditController extends Controller $data = $request->getBudgetData(); $this->repository->update($budget, $data); - $request->session()->flash('success', (string)trans('firefly.updated_budget', ['name' => $budget->name])); + $request->session()->flash('success', (string) trans('firefly.updated_budget', ['name' => $budget->name])); $this->repository->cleanupBudgets(); app('preferences')->mark(); @@ -139,14 +138,14 @@ class EditController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); } - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { $request->session()->put('budgets.edit.fromUpdate', true); $redirect = redirect(route('budgets.edit', [$budget->id]))->withInput(['return_to_edit' => 1]); diff --git a/app/Http/Controllers/Budget/IndexController.php b/app/Http/Controllers/Budget/IndexController.php index fd182e9212..8c67680cba 100644 --- a/app/Http/Controllers/Budget/IndexController.php +++ b/app/Http/Controllers/Budget/IndexController.php @@ -36,11 +36,13 @@ use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Controllers\DateCalculation; use Illuminate\Contracts\View\Factory; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Illuminate\View\View; /** @@ -65,7 +67,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.budgets')); + app('view')->share('title', (string) trans('firefly.budgets')); app('view')->share('mainTitleIcon', 'fa-pie-chart'); $this->repository = app(BudgetRepositoryInterface::class); $this->opsRepository = app(OperationsRepositoryInterface::class); @@ -105,7 +107,6 @@ class IndexController extends Controller $end ??= session('end', today(config('app.timezone'))->endOfMonth()); } - $defaultCurrency = app('amount')->getDefaultCurrency(); $currencies = $this->currencyRepository->get(); $budgeted = '0'; $spent = '0'; @@ -118,14 +119,14 @@ class IndexController extends Controller // get all available budgets: $availableBudgets = $this->getAllAvailableBudgets($start, $end); // get all active budgets: - $budgets = $this->getAllBudgets($start, $end, $currencies, $defaultCurrency); + $budgets = $this->getAllBudgets($start, $end, $currencies, $this->defaultCurrency); $sums = $this->getSums($budgets); // get budgeted for default currency: if (0 === count($availableBudgets)) { - $budgeted = $this->blRepository->budgeted($start, $end, $defaultCurrency); - $spentArr = $this->opsRepository->sumExpenses($start, $end, null, null, $defaultCurrency); - $spent = $spentArr[$defaultCurrency->id]['sum'] ?? '0'; + $budgeted = $this->blRepository->budgeted($start, $end, $this->defaultCurrency); + $spentArr = $this->opsRepository->sumExpenses($start, $end, null, null, $this->defaultCurrency); + $spent = $spentArr[$this->defaultCurrency->id]['sum'] ?? '0'; unset($spentArr); } @@ -135,6 +136,7 @@ class IndexController extends Controller // get all inactive budgets, and simply list them: $inactive = $this->repository->getInactiveBudgets(); + $defaultCurrency = $this->defaultCurrency; return view( 'budgets.index', @@ -161,6 +163,8 @@ class IndexController extends Controller private function getAllAvailableBudgets(Carbon $start, Carbon $end): array { + Log::debug(sprintf('Start of getAllAvailableBudgets("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); + $converter = new ExchangeRateConverter(); // get all available budgets. $ab = $this->abRepository->get($start, $end); $availableBudgets = []; @@ -168,18 +172,22 @@ class IndexController extends Controller // for each, complement with spent amount: /** @var AvailableBudget $entry */ foreach ($ab as $entry) { - $array = $entry->toArray(); - $array['start_date'] = $entry->start_date; - $array['end_date'] = $entry->end_date; + $array = $entry->toArray(); + $array['start_date'] = $entry->start_date; + $array['end_date'] = $entry->end_date; // spent in period: - $spentArr = $this->opsRepository->sumExpenses($entry->start_date, $entry->end_date, null, null, $entry->transactionCurrency); - $array['spent'] = $spentArr[$entry->transaction_currency_id]['sum'] ?? '0'; - + $spentArr = $this->opsRepository->sumExpenses($entry->start_date, $entry->end_date, null, null, $entry->transactionCurrency); + $array['spent'] = $spentArr[$entry->transaction_currency_id]['sum'] ?? '0'; + $array['native_spent'] = $this->convertToNative && $entry->transaction_currency_id !== $this->defaultCurrency->id ? $converter->convert($entry->transactionCurrency, $this->defaultCurrency, $entry->start_date, $array['spent']) : null; // budgeted in period: - $budgeted = $this->blRepository->budgeted($entry->start_date, $entry->end_date, $entry->transactionCurrency); - $array['budgeted'] = $budgeted; - $availableBudgets[] = $array; + $budgeted = $this->blRepository->budgeted($entry->start_date, $entry->end_date, $entry->transactionCurrency); + $array['budgeted'] = $budgeted; + $array['native_budgeted'] = $this->convertToNative && $entry->transaction_currency_id !== $this->defaultCurrency->id ? $converter->convert($entry->transactionCurrency, $this->defaultCurrency, $entry->start_date, $budgeted) : null; + // this time, because of complex sums, use the currency converter. + + + $availableBudgets[] = $array; unset($spentArr); } @@ -213,6 +221,7 @@ class IndexController extends Controller $array['budgeted'][] = [ 'id' => $limit->id, 'amount' => $amount, + 'notes' => $this->blRepository->getNoteText($limit), 'start_date' => $limit->start_date->isoFormat($this->monthAndDayFormat), 'end_date' => $limit->end_date->isoFormat($this->monthAndDayFormat), 'in_range' => $limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end), @@ -305,7 +314,7 @@ class IndexController extends Controller $budgetIds = $request->get('budgetIds'); foreach ($budgetIds as $index => $budgetId) { - $budgetId = (int)$budgetId; + $budgetId = (int) $budgetId; $budget = $repository->find($budgetId); if (null !== $budget) { app('log')->debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1)); diff --git a/app/Http/Controllers/Budget/ShowController.php b/app/Http/Controllers/Budget/ShowController.php index 5ed1205eaa..42f2c23ce6 100644 --- a/app/Http/Controllers/Budget/ShowController.php +++ b/app/Http/Controllers/Budget/ShowController.php @@ -59,7 +59,7 @@ class ShowController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.budgets')); + app('view')->share('title', (string) trans('firefly.budgets')); app('view')->share('mainTitleIcon', 'fa-pie-chart'); $this->journalRepos = app(JournalRepositoryInterface::class); $this->repository = app(BudgetRepositoryInterface::class); @@ -91,8 +91,8 @@ class ShowController extends Controller $first = $this->journalRepos->firstNull(); $firstDate = null !== $first ? $first->date : $start; $periods = $this->getNoBudgetPeriodOverview($firstDate, $end); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); @@ -112,12 +112,12 @@ class ShowController extends Controller */ public function noBudgetAll(Request $request) { - $subTitle = (string)trans('firefly.all_journals_without_budget'); + $subTitle = (string) trans('firefly.all_journals_without_budget'); $first = $this->journalRepos->firstNull(); $start = null === $first ? new Carbon() : $first->date; $end = today(config('app.timezone')); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); @@ -140,8 +140,8 @@ class ShowController extends Controller /** @var Carbon $allStart */ $allStart = session('first', today(config('app.timezone'))->startOfYear()); $allEnd = today(); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $limits = $this->getLimits($budget, $allStart, $allEnd); $repetition = null; $attachments = $this->repository->getAttachments($budget); @@ -156,7 +156,7 @@ class ShowController extends Controller $groups = $collector->getPaginatedGroups(); $groups->setPath(route('budgets.show', [$budget->id])); - $subTitle = (string)trans('firefly.all_journals_for_budget', ['name' => $budget->name]); + $subTitle = (string) trans('firefly.all_journals_for_budget', ['name' => $budget->name]); return view('budgets.show', compact('limits', 'attachments', 'budget', 'repetition', 'groups', 'subTitle')); } @@ -174,8 +174,8 @@ class ShowController extends Controller throw new FireflyException('This budget limit is not part of this budget.'); } - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $subTitle = trans( 'firefly.budget_in_period', [ diff --git a/app/Http/Controllers/Category/CreateController.php b/app/Http/Controllers/Category/CreateController.php index ba0fdee1eb..c9a0e58d1d 100644 --- a/app/Http/Controllers/Category/CreateController.php +++ b/app/Http/Controllers/Category/CreateController.php @@ -53,7 +53,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.categories')); + app('view')->share('title', (string) trans('firefly.categories')); app('view')->share('mainTitleIcon', 'fa-bookmark'); $this->repository = app(CategoryRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); @@ -74,7 +74,7 @@ class CreateController extends Controller $this->rememberPreviousUrl('categories.create.url'); } $request->session()->forget('categories.create.fromStore'); - $subTitle = (string)trans('firefly.create_new_category'); + $subTitle = (string) trans('firefly.create_new_category'); return view('categories.create', compact('subTitle')); } @@ -91,7 +91,7 @@ class CreateController extends Controller $data = $request->getCategoryData(); $category = $this->repository->store($data); - $request->session()->flash('success', (string)trans('firefly.stored_category', ['name' => $category->name])); + $request->session()->flash('success', (string) trans('firefly.stored_category', ['name' => $category->name])); app('preferences')->mark(); // store attachment(s): @@ -102,7 +102,7 @@ class CreateController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -110,7 +110,7 @@ class CreateController extends Controller } $redirect = redirect(route('categories.index')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { $request->session()->put('categories.create.fromStore', true); $redirect = redirect(route('categories.create'))->withInput(); diff --git a/app/Http/Controllers/Category/DeleteController.php b/app/Http/Controllers/Category/DeleteController.php index fb1c5b83b6..b4968f45ee 100644 --- a/app/Http/Controllers/Category/DeleteController.php +++ b/app/Http/Controllers/Category/DeleteController.php @@ -50,7 +50,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.categories')); + app('view')->share('title', (string) trans('firefly.categories')); app('view')->share('mainTitleIcon', 'fa-bookmark'); $this->repository = app(CategoryRepositoryInterface::class); @@ -66,7 +66,7 @@ class DeleteController extends Controller */ public function delete(Category $category) { - $subTitle = (string)trans('firefly.delete_category', ['name' => $category->name]); + $subTitle = (string) trans('firefly.delete_category', ['name' => $category->name]); // put previous url in session $this->rememberPreviousUrl('categories.delete.url'); @@ -84,7 +84,7 @@ class DeleteController extends Controller $name = $category->name; $this->repository->destroy($category); - $request->session()->flash('success', (string)trans('firefly.deleted_category', ['name' => $name])); + $request->session()->flash('success', (string) trans('firefly.deleted_category', ['name' => $name])); app('preferences')->mark(); return redirect($this->getPreviousUrl('categories.delete.url')); diff --git a/app/Http/Controllers/Category/EditController.php b/app/Http/Controllers/Category/EditController.php index a3f63400a5..cdfa19a3fc 100644 --- a/app/Http/Controllers/Category/EditController.php +++ b/app/Http/Controllers/Category/EditController.php @@ -53,7 +53,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.categories')); + app('view')->share('title', (string) trans('firefly.categories')); app('view')->share('mainTitleIcon', 'fa-bookmark'); $this->repository = app(CategoryRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); @@ -70,7 +70,7 @@ class EditController extends Controller */ public function edit(Request $request, Category $category) { - $subTitle = (string)trans('firefly.edit_category', ['name' => $category->name]); + $subTitle = (string) trans('firefly.edit_category', ['name' => $category->name]); // put previous url in session if not redirect from store (not "return_to_edit"). if (true !== session('categories.edit.fromUpdate')) { @@ -95,7 +95,7 @@ class EditController extends Controller $data = $request->getCategoryData(); $this->repository->update($category, $data); - $request->session()->flash('success', (string)trans('firefly.updated_category', ['name' => $category->name])); + $request->session()->flash('success', (string) trans('firefly.updated_category', ['name' => $category->name])); app('preferences')->mark(); // store new attachment(s): @@ -106,7 +106,7 @@ class EditController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -114,7 +114,7 @@ class EditController extends Controller } $redirect = redirect($this->getPreviousUrl('categories.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { $request->session()->put('categories.edit.fromUpdate', true); $redirect = redirect(route('categories.edit', [$category->id])); diff --git a/app/Http/Controllers/Category/IndexController.php b/app/Http/Controllers/Category/IndexController.php index 6ee7d6861c..41b85c87a7 100644 --- a/app/Http/Controllers/Category/IndexController.php +++ b/app/Http/Controllers/Category/IndexController.php @@ -50,7 +50,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.categories')); + app('view')->share('title', (string) trans('firefly.categories')); app('view')->share('mainTitleIcon', 'fa-bookmark'); $this->repository = app(CategoryRepositoryInterface::class); @@ -66,8 +66,8 @@ class IndexController extends Controller */ public function index(Request $request) { - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $collection = $this->repository->getCategories(); $total = $collection->count(); $collection = $collection->slice(($page - 1) * $pageSize, $pageSize); diff --git a/app/Http/Controllers/Category/NoCategoryController.php b/app/Http/Controllers/Category/NoCategoryController.php index 09b1d67690..d66516ec56 100644 --- a/app/Http/Controllers/Category/NoCategoryController.php +++ b/app/Http/Controllers/Category/NoCategoryController.php @@ -55,7 +55,7 @@ class NoCategoryController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.categories')); + app('view')->share('title', (string) trans('firefly.categories')); app('view')->share('mainTitleIcon', 'fa-bookmark'); $this->journalRepos = app(JournalRepositoryInterface::class); @@ -78,8 +78,8 @@ class NoCategoryController extends Controller $start ??= session('start'); // @var Carbon $end $end ??= session('end'); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $subTitle = trans( 'firefly.without_category_between', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)] @@ -113,10 +113,10 @@ class NoCategoryController extends Controller $start = null; $end = null; $periods = new Collection(); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; app('log')->debug('Start of noCategory()'); - $subTitle = (string)trans('firefly.all_journals_without_category'); + $subTitle = (string) trans('firefly.all_journals_without_category'); $first = $this->journalRepos->firstNull(); $start = null === $first ? new Carbon() : $first->date; $end = today(config('app.timezone')); diff --git a/app/Http/Controllers/Category/ShowController.php b/app/Http/Controllers/Category/ShowController.php index 24462817a3..00d6305868 100644 --- a/app/Http/Controllers/Category/ShowController.php +++ b/app/Http/Controllers/Category/ShowController.php @@ -56,7 +56,7 @@ class ShowController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.categories')); + app('view')->share('title', (string) trans('firefly.categories')); app('view')->share('mainTitleIcon', 'fa-bookmark'); $this->repository = app(CategoryRepositoryInterface::class); @@ -79,9 +79,9 @@ class ShowController extends Controller // @var Carbon $end $end ??= session('end', today(config('app.timezone'))->endOfMonth()); $subTitleIcon = 'fa-bookmark'; - $page = (int)$request->get('page'); + $page = (int) $request->get('page'); $attachments = $this->repository->getAttachments($category); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $oldest = $this->repository->firstUseDate($category) ?? today(config('app.timezone'))->startOfYear(); $periods = $this->getCategoryPeriodOverview($category, $oldest, $end); $path = route('categories.show', [$category->id, $start->format('Y-m-d'), $end->format('Y-m-d')]); @@ -116,13 +116,13 @@ class ShowController extends Controller { // default values: $subTitleIcon = 'fa-bookmark'; - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $start = null; $end = null; $periods = new Collection(); - $subTitle = (string)trans('firefly.all_journals_for_category', ['name' => $category->name]); + $subTitle = (string) trans('firefly.all_journals_for_category', ['name' => $category->name]); $first = $this->repository->firstUseDate($category); /** @var Carbon $start */ diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index aad9c2fb66..bfd43804e0 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -24,17 +24,19 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\AugumentData; use FireflyIII\Support\Http\Controllers\ChartGeneration; use FireflyIII\Support\Http\Controllers\DateCalculation; @@ -80,6 +82,8 @@ class AccountController extends Controller */ public function expenseAccounts(): JsonResponse { + Log::debug('RevenueAccounts'); + /** @var Carbon $start */ $start = clone session('start', today(config('app.timezone'))->startOfMonth()); @@ -88,6 +92,7 @@ class AccountController extends Controller $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('chart.account.expense-accounts'); if ($cache->has()) { return response()->json($cache->get()); @@ -100,36 +105,64 @@ class AccountController extends Controller $tempData = []; // grab all accounts and names - $accounts = $this->accountRepository->getAccountsByType([AccountType::EXPENSE]); + $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::EXPENSE->value]); $accountNames = $this->extractNames($accounts); // grab all balances - $startBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $start); - $endBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $end); + $startBalances = app('steam')->finalAccountsBalance($accounts, $start); + $endBalances = app('steam')->finalAccountsBalance($accounts, $end); - // loop the end balances. This is an array for each account ($expenses) - foreach ($endBalances as $accountId => $expenses) { - $accountId = (int)$accountId; - // loop each expense entry (each entry can be a different currency). - foreach ($expenses as $currencyId => $endAmount) { - $currencyId = (int)$currencyId; + // loop the accounts, then check for balance and currency info. + foreach ($accounts as $account) { + Log::debug(sprintf('Now in account #%d ("%s")', $account->id, $account->name)); + $expenses = $endBalances[$account->id] ?? false; + if (false === $expenses) { + Log::error(sprintf('Found no end balance for account #%d', $account->id)); + continue; + } + + /** + * @var string $key + * @var string $endBalance + */ + foreach ($expenses as $key => $endBalance) { + if (!$this->convertToNative && 'native_balance' === $key) { + Log::debug(sprintf('[a] Will skip expense array "%s"', $key)); + + continue; + } + if ($this->convertToNative && 'native_balance' !== $key) { + Log::debug(sprintf('[b] Will skip expense array "%s"', $key)); + + continue; + } + Log::debug(sprintf('Will process expense array "%s" with amount %s', $key, $endBalance)); + $searchCode = $this->convertToNative ? $this->defaultCurrency->code : $key; + Log::debug(sprintf('Search code is %s', $searchCode)); // see if there is an accompanying start amount. // grab the difference and find the currency. - $startAmount = (string)($startBalances[$accountId][$currencyId] ?? '0'); - $diff = bcsub((string)$endAmount, $startAmount); - $currencies[$currencyId] ??= $this->currencyRepository->find($currencyId); + $startBalance = ($startBalances[$account->id][$key] ?? '0'); + Log::debug(sprintf('Start balance is %s', $startBalance)); + $diff = bcsub($endBalance, $startBalance); + $currencies[$searchCode] ??= $this->currencyRepository->findByCode($searchCode); if (0 !== bccomp($diff, '0')) { // store the values in a temporary array. $tempData[] = [ - 'name' => $accountNames[$accountId], + 'name' => $accountNames[$account->id], 'difference' => $diff, - 'diff_float' => (float)$diff, // intentional float - 'currency_id' => $currencyId, + 'diff_float' => (float) $diff, // intentional float + 'currency_id' => $currencies[$searchCode]->id, ]; } } } + // recreate currencies, but on ID instead of code. + $newCurrencies = []; + foreach ($currencies as $currency) { + $newCurrencies[$currency->id] = $currency; + } + $currencies = $newCurrencies; // sort temp array by amount. $amounts = array_column($tempData, 'diff_float'); @@ -143,7 +176,7 @@ class AccountController extends Controller foreach ($currencies as $currencyId => $currency) { $dataSet = [ - 'label' => (string)trans('firefly.spent'), + 'label' => (string) trans('firefly.spent'), 'type' => 'bar', 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, @@ -156,7 +189,7 @@ class AccountController extends Controller foreach ($tempData as $entry) { $currencyId = $entry['currency_id']; $name = $entry['name']; - $chartData[$currencyId]['entries'][$name] = $entry['difference']; + $chartData[$currencyId]['entries'][$name] = (float) $entry['difference']; } $data = $this->generator->multiSet($chartData); @@ -200,7 +233,7 @@ class AccountController extends Controller /** @var array $journal */ foreach ($journals as $journal) { - $budgetId = (int)$journal['budget_id']; + $budgetId = (int) $journal['budget_id']; $key = sprintf('%d-%d', $budgetId, $journal['currency_id']); $budgetIds[] = $budgetId; if (!array_key_exists($key, $result)) { @@ -220,7 +253,7 @@ class AccountController extends Controller foreach ($result as $row) { $budgetId = $row['budget_id']; $name = $names[$budgetId]; - $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); + $label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } @@ -268,7 +301,7 @@ class AccountController extends Controller if (!array_key_exists($key, $result)) { $result[$key] = [ 'total' => '0', - 'category_id' => (int)$journal['category_id'], + 'category_id' => (int) $journal['category_id'], 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], @@ -281,7 +314,7 @@ class AccountController extends Controller foreach ($result as $row) { $categoryId = $row['category_id']; $name = $names[$categoryId] ?? '(unknown)'; - $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); + $label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } @@ -300,7 +333,7 @@ class AccountController extends Controller { $start = clone session('start', today(config('app.timezone'))->startOfMonth()); $end = clone session('end', today(config('app.timezone'))->endOfMonth()); - $defaultSet = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray(); + $defaultSet = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); Log::debug('Default set is ', $defaultSet); $frontpage = app('preferences')->get('frontpageAccounts', $defaultSet); $frontpageArray = !is_array($frontpage->data) ? [] : $frontpage->data; @@ -367,7 +400,7 @@ class AccountController extends Controller foreach ($result as $row) { $categoryId = $row['category_id']; $name = $names[$categoryId] ?? '(unknown)'; - $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); + $label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]); $chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']]; } $data = $this->generator->multiCurrencyPieChart($chartData); @@ -383,83 +416,111 @@ class AccountController extends Controller */ public function period(Account $account, Carbon $start, Carbon $end): JsonResponse { - $chartData = []; - $cache = new CacheProperties(); + Log::debug(sprintf('Now in period("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'))); + $chartData = []; + $cache = new CacheProperties(); $cache->addProperty('chart.account.period'); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty($account->id); if ($cache->has()) { return response()->json($cache->get()); } - $currencies = $this->accountRepository->getUsedCurrencies($account); - // if the account is not expense or revenue, just use the account's default currency. - if (!in_array($account->accountType->type, [AccountType::REVENUE, AccountType::EXPENSE], true)) { - $currencies = [$this->accountRepository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency()]; - } - - /** @var TransactionCurrency $currency */ - foreach ($currencies as $currency) { - $chartData[] = $this->periodByCurrency($start, $end, $account, $currency); - } - - $data = $this->generator->multiSet($chartData); - $cache->store($data); - - return response()->json($data); - } - - /** - * @throws FireflyException - */ - private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array - { - Log::debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code)); - $locale = app('steam')->getLocale(); - $step = $this->calculateStep($start, $end); - $result = [ - 'label' => sprintf('%s (%s)', $account->name, $currency->symbol), - 'currency_symbol' => $currency->symbol, - 'currency_code' => $currency->code, - ]; - $entries = []; - $current = clone $start; + // collect and filter balances for the entire period. + $step = $this->calculateStep($start, $end); Log::debug(sprintf('Step is %s', $step)); + $locale = app('steam')->getLocale(); + $return = []; // fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041 // have to make sure this chart is always based on the balance at the END of the period. // This period depends on the size of the chart - $current = app('navigation')->endOfX($current, $step, null); - Log::debug(sprintf('$current date is %s', $current->format('Y-m-d'))); - if ('1D' === $step) { - // per day the entire period, balance for every day. - $format = (string)trans('config.month_and_day_js', [], $locale); - $range = app('steam')->balanceInRange($account, $start, $end, $currency); - $previous = array_values($range)[0]; - while ($end >= $current) { - $theDate = $current->format('Y-m-d'); - $balance = $range[$theDate] ?? $previous; - $label = $current->isoFormat($format); - $entries[$label] = (float)$balance; - $previous = $balance; - $current->addDay(); - } - } - if ('1W' === $step || '1M' === $step || '1Y' === $step) { - while ($end >= $current) { - Log::debug(sprintf('Current is: %s', $current->format('Y-m-d'))); - $balance = (float)app('steam')->balance($account, $current, $currency); - $label = app('navigation')->periodShow($current, $step); - $entries[$label] = $balance; - $current = app('navigation')->addPeriod($current, $step, 0); - // here too, to fix #8041, the data is corrected to the end of the period. - $current = app('navigation')->endOfX($current, $step, null); - } - } - $result['entries'] = $entries; + $current = clone $start; + $current = app('navigation')->endOfX($current, $step, null); + $format = (string) trans('config.month_and_day_js', [], $locale); + $accountCurrency = $this->accountRepository->getAccountCurrency($account); - return $result; + Log::debug('One'); + $range = Steam::finalAccountBalanceInRange($account, $start, $end, $this->convertToNative); + Log::debug('Two'); + $range = Steam::filterAccountBalances($range, $account, $this->convertToNative, $accountCurrency); + Log::debug('Three'); + + // temp, get end balance. + Log::debug('temp get end balance'); + Steam::finalAccountBalance($account, $end); + Log::debug('END temp get end balance done'); + + $previous = array_values($range)[0]; + $accountCurrency ??= $this->defaultCurrency; // do this AFTER getting the balances. + Log::debug('Start chart loop.'); + + $newRange = []; + $expectedIndex = 0; + Log::debug('Balances exist at:'); + foreach ($range as $key => $value) { + $newRange[] = ['date' => $key, 'info' => $value]; + Log::debug(sprintf(' - %s', $key)); + } + $carbon = Carbon::createFromFormat('Y-m-d', $newRange[0]['date']); + while ($end->gte($current)) { + $momentBalance = $previous; + $theDate = $current->format('Y-m-d'); + while ($carbon->lte($current) && array_key_exists($expectedIndex, $newRange)) { + $momentBalance = $newRange[$expectedIndex]['info']; + Log::debug(sprintf('Expected index is %d!, date is %s, current is %s', $expectedIndex, $carbon->format('Y-m-d'), $current->format('Y-m-d'))); + $carbon = Carbon::createFromFormat('Y-m-d', $newRange[$expectedIndex]['date']); + ++$expectedIndex; + } + + $return = $this->updateChartKeys($return, $momentBalance); + $previous = $momentBalance; + + Log::debug(sprintf('Now at %s', $theDate), $momentBalance); + + + // process each balance thing. + foreach ($momentBalance as $key => $amount) { + $label = $current->isoFormat($format); + $return[$key]['entries'][$label] = $amount; + } + $current = app('navigation')->addPeriod($current, $step, 0); + // here too, to fix #8041, the data is corrected to the end of the period. + $current = app('navigation')->endOfX($current, $step, null); + Log::debug(sprintf('Next moment is %s', $current->format('Y-m-d'))); + + } + Log::debug('End of chart loop.'); + // second loop (yes) to create nice array with info! Yay! + $chartData = []; + + foreach ($return as $key => $info) { + if (3 === strlen($key)) { + // assume it's a currency: + $setCurrency = $this->currencyRepository->findByCode($key); + $info['currency_symbol'] = $setCurrency->symbol; + $info['currency_code'] = $setCurrency->code; + $info['label'] = sprintf('%s (%s)', $account->name, $setCurrency->symbol); + } + if ('balance' === $key) { + $info['currency_symbol'] = $accountCurrency->symbol; + $info['currency_code'] = $accountCurrency->code; + $info['label'] = sprintf('%s (%s)', $account->name, $accountCurrency->symbol); + } + if ('native_balance' === $key) { + $info['currency_symbol'] = $this->defaultCurrency->symbol; + $info['currency_code'] = $this->defaultCurrency->code; + $info['label'] = sprintf('%s (%s) (%s)', $account->name, (string) trans('firefly.sum'), $this->defaultCurrency->symbol); + } + $chartData[] = $info; + } + + $data = $this->generator->multiSet($chartData); + $cache->store($data); + + return response()->json($data); } /** @@ -489,6 +550,7 @@ class AccountController extends Controller $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('chart.account.revenue-accounts'); if ($cache->has()) { return response()->json($cache->get()); @@ -501,37 +563,68 @@ class AccountController extends Controller $tempData = []; // grab all accounts and names - $accounts = $this->accountRepository->getAccountsByType([AccountType::REVENUE]); + $accounts = $this->accountRepository->getAccountsByType([AccountTypeEnum::REVENUE->value]); $accountNames = $this->extractNames($accounts); // grab all balances - $startBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $start); - $endBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $end); + $startBalances = app('steam')->finalAccountsBalance($accounts, $start); + $endBalances = app('steam')->finalAccountsBalance($accounts, $end); - // loop the end balances. This is an array for each account ($expenses) - foreach ($endBalances as $accountId => $expenses) { - $accountId = (int)$accountId; - // loop each expense entry (each entry can be a different currency). - foreach ($expenses as $currencyId => $endAmount) { - $currencyId = (int)$currencyId; + // loop the accounts, then check for balance and currency info. + foreach ($accounts as $account) { + Log::debug(sprintf('Now in account #%d ("%s")', $account->id, $account->name)); + $expenses = $endBalances[$account->id] ?? false; + if (false === $expenses) { + Log::error(sprintf('Found no end balance for account #%d', $account->id)); + + continue; + } + + /** + * @var string $key + * @var string $endBalance + */ + foreach ($expenses as $key => $endBalance) { + if (!$this->convertToNative && 'native_balance' === $key) { + Log::debug(sprintf('[a] Will skip expense array "%s"', $key)); + + continue; + } + if ($this->convertToNative && 'native_balance' !== $key) { + Log::debug(sprintf('[b] Will skip expense array "%s"', $key)); + + continue; + } + Log::debug(sprintf('Will process expense array "%s" with amount %s', $key, $endBalance)); + $searchCode = $this->convertToNative ? $this->defaultCurrency->code : $key; + Log::debug(sprintf('Search code is %s', $searchCode)); // see if there is an accompanying start amount. // grab the difference and find the currency. - $startAmount = (string)($startBalances[$accountId][$currencyId] ?? '0'); - $diff = bcsub((string)$endAmount, $startAmount); - $currencies[$currencyId] ??= $this->currencyRepository->find($currencyId); + $startBalance = ($startBalances[$account->id][$key] ?? '0'); + Log::debug(sprintf('Start balance is %s', $startBalance)); + $diff = bcsub($endBalance, $startBalance); + $currencies[$searchCode] ??= $this->currencyRepository->findByCode($searchCode); if (0 !== bccomp($diff, '0')) { // store the values in a temporary array. $tempData[] = [ - 'name' => $accountNames[$accountId], + 'name' => $accountNames[$account->id], 'difference' => $diff, - 'diff_float' => (float)$diff, // intentional float - 'currency_id' => $currencyId, + 'diff_float' => (float) $diff, // intentional float + 'currency_id' => $currencies[$searchCode]->id, ]; } } } + + // recreate currencies, but on ID instead of code. + $newCurrencies = []; + foreach ($currencies as $currency) { + $newCurrencies[$currency->id] = $currency; + } + $currencies = $newCurrencies; + // sort temp array by amount. $amounts = array_column($tempData, 'diff_float'); array_multisort($amounts, SORT_ASC, $tempData); @@ -544,7 +637,7 @@ class AccountController extends Controller foreach ($currencies as $currencyId => $currency) { $dataSet = [ - 'label' => (string)trans('firefly.earned'), + 'label' => (string) trans('firefly.earned'), 'type' => 'bar', 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, @@ -565,4 +658,15 @@ class AccountController extends Controller return response()->json($data); } + + private function updateChartKeys(array $array, array $balances): array + { + foreach (array_keys($balances) as $key) { + $array[$key] ??= [ + 'key' => $key, + ]; + } + + return $array; + } } diff --git a/app/Http/Controllers/Chart/BillController.php b/app/Http/Controllers/Chart/BillController.php index 3e8c23c996..a5ab00b99b 100644 --- a/app/Http/Controllers/Chart/BillController.php +++ b/app/Http/Controllers/Chart/BillController.php @@ -72,7 +72,7 @@ class BillController extends Controller */ foreach ($paid as $info) { $amount = $info['sum']; - $label = (string)trans('firefly.paid_in_currency', ['currency' => $info['name']]); + $label = (string) trans('firefly.paid_in_currency', ['currency' => $info['name']]); $chartData[$label] = [ 'amount' => $amount, 'currency_symbol' => $info['symbol'], @@ -85,7 +85,7 @@ class BillController extends Controller */ foreach ($unpaid as $info) { $amount = $info['sum']; - $label = (string)trans('firefly.unpaid_in_currency', ['currency' => $info['name']]); + $label = (string) trans('firefly.unpaid_in_currency', ['currency' => $info['name']]); $chartData[$label] = [ 'amount' => $amount, 'currency_symbol' => $info['symbol'], @@ -109,6 +109,7 @@ class BillController extends Controller $cache = new CacheProperties(); $cache->addProperty('chart.bill.single'); $cache->addProperty($bill->id); + $cache->addProperty($this->convertToNative); if ($cache->has()) { return response()->json($cache->get()); } @@ -132,42 +133,55 @@ class BillController extends Controller return 0; } ); + $currency = $bill->transactionCurrency; + if ($this->convertToNative) { + $currency = $this->defaultCurrency; + } $chartData = [ [ 'type' => 'line', - 'label' => (string)trans('firefly.min-amount'), - 'currency_symbol' => $bill->transactionCurrency->symbol, - 'currency_code' => $bill->transactionCurrency->code, + 'label' => (string) trans('firefly.min-amount'), + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, 'entries' => [], ], [ 'type' => 'line', - 'label' => (string)trans('firefly.max-amount'), - 'currency_symbol' => $bill->transactionCurrency->symbol, - 'currency_code' => $bill->transactionCurrency->code, + 'label' => (string) trans('firefly.max-amount'), + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, 'entries' => [], ], [ 'type' => 'bar', - 'label' => (string)trans('firefly.journal-amount'), - 'currency_symbol' => $bill->transactionCurrency->symbol, - 'currency_code' => $bill->transactionCurrency->code, + 'label' => (string) trans('firefly.journal-amount'), + 'currency_symbol' => $currency->symbol, + 'currency_code' => $currency->code, 'entries' => [], ], ]; $currencyId = $bill->transaction_currency_id; + $amountMin = $bill->amount_min; + $amountMax = $bill->amount_max; + if ($this->convertToNative && $currencyId !== $this->defaultCurrency->id) { + $amountMin = $bill->native_amount_min; + $amountMax = $bill->native_amount_max; + } foreach ($journals as $journal) { - $date = $journal['date']->isoFormat((string)trans('config.month_and_day_js', [], $locale)); - $chartData[0]['entries'][$date] = $bill->amount_min; // minimum amount of bill - $chartData[1]['entries'][$date] = $bill->amount_max; // maximum amount of bill + $date = $journal['date']->isoFormat((string) trans('config.month_and_day_js', [], $locale)); + $chartData[0]['entries'][$date] = $amountMin; // minimum amount of bill + $chartData[1]['entries'][$date] = $amountMax; // maximum amount of bill // append amount because there are more than one per moment: if (!array_key_exists($date, $chartData[2]['entries'])) { $chartData[2]['entries'][$date] = '0'; } $amount = bcmul($journal['amount'], '-1'); - if ($currencyId === $journal['foreign_currency_id']) { + if ($this->convertToNative && $currencyId !== $journal['currency_id']) { + $amount = bcmul($journal['native_amount'], '-1'); + } + if ($this->convertToNative && $currencyId === $journal['foreign_currency_id']) { $amount = bcmul($journal['foreign_amount'], '-1'); } diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index 63d8051745..2ae6db7a63 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -38,10 +38,12 @@ use FireflyIII\Repositories\Budget\NoBudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Chart\Budget\FrontpageChartGenerator; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Http\Controllers\AugumentData; use FireflyIII\Support\Http\Controllers\DateCalculation; use Illuminate\Http\JsonResponse; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class BudgetController. @@ -90,6 +92,7 @@ class BudgetController extends Controller $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('chart.budget.budget'); $cache->addProperty($budget->id); @@ -106,7 +109,7 @@ class BudgetController extends Controller while ($end >= $loopStart) { /** @var Carbon $loopEnd */ $loopEnd = app('navigation')->endOfPeriod($loopStart, $step); - $spent = $this->opsRepository->sumExpenses($loopStart, $loopEnd, null, $collection); + $spent = $this->opsRepository->sumExpenses($loopStart, $loopEnd, null, $collection); // this method already converts to native. $label = trim(app('navigation')->periodShow($loopStart, $step)); foreach ($spent as $row) { @@ -155,6 +158,7 @@ class BudgetController extends Controller $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('chart.budget.budget.limit'); $cache->addProperty($budgetLimit->id); $cache->addProperty($budget->id); @@ -167,20 +171,26 @@ class BudgetController extends Controller $amount = $budgetLimit->amount; $budgetCollection = new Collection([$budget]); $currency = $budgetLimit->transactionCurrency; + if ($this->convertToNative) { + $amount = $budgetLimit->native_amount; + $currency = $this->defaultCurrency; + } + + while ($start <= $end) { $current = clone $start; - $expenses = $this->opsRepository->sumExpenses($current, $current, null, $budgetCollection, $currency); + $expenses = $this->opsRepository->sumExpenses($current, $current, null, $budgetCollection, $budgetLimit->transactionCurrency); $spent = $expenses[$currency->id]['sum'] ?? '0'; $amount = bcadd($amount, $spent); - $format = $start->isoFormat((string)trans('config.month_and_day_js', [], $locale)); + $format = $start->isoFormat((string) trans('config.month_and_day_js', [], $locale)); $entries[$format] = $amount; $start->addDay(); } - $data = $this->generator->singleSet((string)trans('firefly.left'), $entries); + $data = $this->generator->singleSet((string) trans('firefly.left'), $entries); // add currency symbol from budget limit: - $data['datasets'][0]['currency_symbol'] = $budgetLimit->transactionCurrency->symbol; - $data['datasets'][0]['currency_code'] = $budgetLimit->transactionCurrency->code; + $data['datasets'][0]['currency_symbol'] = $currency->symbol; + $data['datasets'][0]['currency_code'] = $currency->code; $cache->store($data); return response()->json($data); @@ -196,6 +206,7 @@ class BudgetController extends Controller $budgetLimitId = null === $budgetLimit ? 0 : $budgetLimit->id; $cache = new CacheProperties(); $cache->addProperty($budget->id); + $cache->addProperty($this->convertToNative); $cache->addProperty($budgetLimitId); $cache->addProperty('chart.budget.expense-asset'); $start = session('first', today(config('app.timezone'))->startOfYear()); @@ -220,20 +231,39 @@ class BudgetController extends Controller // group by asset account ID: foreach ($journals as $journal) { - $key = sprintf('%d-%d', (int)$journal['source_account_id'], $journal['currency_id']); + $key = sprintf('%d-%d', $journal['source_account_id'], $journal['currency_id']); + $amount = $journal['amount']; + + $symbol = $journal['currency_symbol']; + $code = $journal['currency_code']; + $name = $journal['currency_name']; + + // if convert to native, use the native things, unless it's the foreign amount which is in the native currency. + if ($this->convertToNative && $journal['currency_id'] !== $this->defaultCurrency->id) { + $key = sprintf('%d-%d', $journal['source_account_id'], $this->defaultCurrency->id); + $symbol = $this->defaultCurrency->symbol; + $code = $this->defaultCurrency->code; + $name = $this->defaultCurrency->name; + $amount = $journal['native_amount']; + } + + if ($journal['foreign_currency_id'] === $this->defaultCurrency->id) { + $amount = $journal['foreign_amount']; + } + $result[$key] ??= [ 'amount' => '0', - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_name' => $journal['currency_name'], + 'currency_symbol' => $symbol, + 'currency_code' => $code, + 'currency_name' => $name, ]; - $result[$key]['amount'] = bcadd($journal['amount'], $result[$key]['amount']); + $result[$key]['amount'] = bcadd($amount, $result[$key]['amount']); } $names = $this->getAccountNames(array_keys($result)); foreach ($result as $combinedId => $info) { $parts = explode('-', $combinedId); - $assetId = (int)$parts[0]; + $assetId = (int) $parts[0]; $title = sprintf('%s (%s)', $names[$assetId] ?? '(empty)', $info['currency_name']); $chartData[$title] = [ @@ -259,6 +289,7 @@ class BudgetController extends Controller $budgetLimitId = null === $budgetLimit ? 0 : $budgetLimit->id; $cache = new CacheProperties(); $cache->addProperty($budget->id); + $cache->addProperty($this->convertToNative); $cache->addProperty($budgetLimitId); $cache->addProperty('chart.budget.expense-category'); $start = session('first', today(config('app.timezone'))->startOfYear()); @@ -281,19 +312,42 @@ class BudgetController extends Controller $chartData = []; foreach ($journals as $journal) { $key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']); + $symbol = $journal['currency_symbol']; + $code = $journal['currency_code']; + $name = $journal['currency_name']; + $amount = $journal['amount']; + // if convert to native, use the native things, unless it's the foreign amount which is in the native currency. + if ($this->convertToNative && $journal['currency_id'] !== $this->defaultCurrency->id && $journal['foreign_currency_id'] !== $this->defaultCurrency->id + ) { + $key = sprintf('%d-%d', $journal['category_id'], $this->defaultCurrency->id); + $symbol = $this->defaultCurrency->symbol; + $code = $this->defaultCurrency->code; + $name = $this->defaultCurrency->name; + $amount = $journal['native_amount']; + } + + if ($this->convertToNative && $journal['currency_id'] !== $this->defaultCurrency->id && $journal['foreign_currency_id'] === $this->defaultCurrency->id + ) { + $key = sprintf('%d-%d', $journal['category_id'], $this->defaultCurrency->id); + $symbol = $this->defaultCurrency->symbol; + $code = $this->defaultCurrency->code; + $name = $this->defaultCurrency->name; + $amount = $journal['foreign_amount']; + } + $result[$key] ??= [ 'amount' => '0', - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_name' => $journal['currency_name'], + 'currency_symbol' => $symbol, + 'currency_code' => $code, + 'currency_name' => $name, ]; - $result[$key]['amount'] = bcadd($journal['amount'], $result[$key]['amount']); + $result[$key]['amount'] = bcadd($amount, $result[$key]['amount']); } $names = $this->getCategoryNames(array_keys($result)); foreach ($result as $combinedId => $info) { $parts = explode('-', $combinedId); - $categoryId = (int)$parts[0]; + $categoryId = (int) $parts[0]; $title = sprintf('%s (%s)', $names[$categoryId] ?? '(empty)', $info['currency_name']); $chartData[$title] = [ 'amount' => $info['amount'], @@ -318,6 +372,7 @@ class BudgetController extends Controller $cache = new CacheProperties(); $cache->addProperty($budget->id); $cache->addProperty($budgetLimitId); + $cache->addProperty($this->convertToNative); $cache->addProperty('chart.budget.expense-expense'); $start = session('first', today(config('app.timezone'))->startOfYear()); $end = today(); @@ -341,19 +396,38 @@ class BudgetController extends Controller /** @var array $journal */ foreach ($journals as $journal) { $key = sprintf('%d-%d', $journal['destination_account_id'], $journal['currency_id']); + $amount = $journal['amount']; + + // if convert to native, use the native things, unless it's the foreign amount which is in the native currency. + if ($this->convertToNative && $journal['currency_id'] !== $this->defaultCurrency->id && $journal['foreign_currency_id'] !== $this->defaultCurrency->id) { + $key = sprintf('%d-%d', $journal['destination_account_id'], $this->defaultCurrency->id); + $symbol = $this->defaultCurrency->symbol; + $code = $this->defaultCurrency->code; + $name = $this->defaultCurrency->name; + $amount = $journal['native_amount']; + } + + if ($this->convertToNative && $journal['currency_id'] !== $this->defaultCurrency->id && $journal['foreign_currency_id'] === $this->defaultCurrency->id) { + $key = sprintf('%d-%d', $journal['destination_account_id'], $this->defaultCurrency->id); + $symbol = $this->defaultCurrency->symbol; + $code = $this->defaultCurrency->code; + $name = $this->defaultCurrency->name; + $amount = $journal['foreign_amount']; + } + $result[$key] ??= [ 'amount' => '0', - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_name' => $journal['currency_name'], + 'currency_symbol' => $symbol, + 'currency_code' => $code, + 'currency_name' => $name, ]; - $result[$key]['amount'] = bcadd($journal['amount'], $result[$key]['amount']); + $result[$key]['amount'] = bcadd($amount, $result[$key]['amount']); } $names = $this->getAccountNames(array_keys($result)); foreach ($result as $combinedId => $info) { $parts = explode('-', $combinedId); - $opposingId = (int)$parts[0]; + $opposingId = (int) $parts[0]; $name = $names[$opposingId] ?? 'no name'; $title = sprintf('%s (%s)', $name, $info['currency_name']); $chartData[$title] = [ @@ -374,25 +448,27 @@ class BudgetController extends Controller */ public function frontpage(): JsonResponse { - $start = session('start', today(config('app.timezone'))->startOfMonth()); - $end = session('end', today(config('app.timezone'))->endOfMonth()); - + $start = session('start', today(config('app.timezone'))->startOfMonth()); + $end = session('end', today(config('app.timezone'))->endOfMonth()); // chart properties for cache: - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('chart.budget.frontpage'); if ($cache->has()) { return response()->json($cache->get()); } - - $chartGenerator = app(FrontpageChartGenerator::class); + Log::debug('Regenerate frontpage chart from scratch.'); + $chartGenerator = app(FrontpageChartGenerator::class); $chartGenerator->setUser(auth()->user()); $chartGenerator->setStart($start); $chartGenerator->setEnd($end); + $chartGenerator->convertToNative = $this->convertToNative; + $chartGenerator->default = $this->defaultCurrency; - $chartData = $chartGenerator->generate(); - $data = $this->generator->multiSet($chartData); + $chartData = $chartGenerator->generate(); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); @@ -422,14 +498,14 @@ class BudgetController extends Controller $preferredRange = app('navigation')->preferredRangeFormat($start, $end); $chartData = [ [ - 'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $currency->name]), + 'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $currency->name]), 'type' => 'bar', 'entries' => [], 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, ], [ - 'label' => (string)trans('firefly.box_budgeted_in_currency', ['currency' => $currency->name]), + 'label' => (string) trans('firefly.box_budgeted_in_currency', ['currency' => $currency->name]), 'type' => 'bar', 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, @@ -500,7 +576,7 @@ class BudgetController extends Controller $currentStart = app('navigation')->addPeriod($currentStart, $preferredRange, 0); } - $data = $this->generator->singleSet((string)trans('firefly.spent'), $chartData); + $data = $this->generator->singleSet((string) trans('firefly.spent'), $chartData); $cache->store($data); return response()->json($data); diff --git a/app/Http/Controllers/Chart/BudgetReportController.php b/app/Http/Controllers/Chart/BudgetReportController.php index c4ea407bb9..89d24d1e02 100644 --- a/app/Http/Controllers/Chart/BudgetReportController.php +++ b/app/Http/Controllers/Chart/BudgetReportController.php @@ -171,7 +171,7 @@ class BudgetReportController extends Controller $chartData[$spentKey] ??= [ 'label' => sprintf( '%s (%s)', - (string)trans('firefly.spent_in_specific_budget', ['budget' => $budget->name]), + (string) trans('firefly.spent_in_specific_budget', ['budget' => $budget->name]), $currency['currency_name'] ), 'type' => 'bar', diff --git a/app/Http/Controllers/Chart/CategoryController.php b/app/Http/Controllers/Chart/CategoryController.php index bef78246ba..79979e6a70 100644 --- a/app/Http/Controllers/Chart/CategoryController.php +++ b/app/Http/Controllers/Chart/CategoryController.php @@ -49,8 +49,7 @@ class CategoryController extends Controller use ChartGeneration; use DateCalculation; - /** @var GeneratorInterface Chart generation methods. */ - protected $generator; + protected GeneratorInterface $generator; /** * CategoryController constructor. @@ -71,24 +70,27 @@ class CategoryController extends Controller public function all(Category $category): JsonResponse { // cache results: - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty('chart.category.all'); $cache->addProperty($category->id); + $cache->addProperty($this->convertToNative); if ($cache->has()) { return response()->json($cache->get()); } /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class); - $start = $repository->firstUseDate($category) ?? $this->getDate(); - $range = app('navigation')->getViewRange(false); - $start = app('navigation')->startOfPeriod($start, $range); - $end = $this->getDate(); + $repository = app(CategoryRepositoryInterface::class); + $start = $repository->firstUseDate($category) ?? $this->getDate(); + $range = app('navigation')->getViewRange(false); + $start = app('navigation')->startOfPeriod($start, $range); + $end = $this->getDate(); /** @var WholePeriodChartGenerator $chartGenerator */ - $chartGenerator = app(WholePeriodChartGenerator::class); - $chartData = $chartGenerator->generate($category, $start, $end); - $data = $this->generator->multiSet($chartData); + $chartGenerator = app(WholePeriodChartGenerator::class); + $chartGenerator->convertToNative = $this->convertToNative; + + $chartData = $chartGenerator->generate($category, $start, $end); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); @@ -111,6 +113,7 @@ class CategoryController extends Controller $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('chart.category.frontpage'); if ($cache->has()) { return response()->json($cache->get()); @@ -136,6 +139,7 @@ class CategoryController extends Controller $cache->addProperty('chart.category.period'); $cache->addProperty($accounts->pluck('id')->toArray()); $cache->addProperty($category); + $cache->addProperty($this->convertToNative); if ($cache->has()) { return response()->json($cache->get()); } @@ -185,7 +189,7 @@ class CategoryController extends Controller $inKey = sprintf('%d-in', $currencyId); $chartData[$outKey] = [ - 'label' => sprintf('%s (%s)', (string)trans('firefly.spent'), $currencyInfo['currency_name']), + 'label' => sprintf('%s (%s)', (string) trans('firefly.spent'), $currencyInfo['currency_name']), 'entries' => [], 'type' => 'bar', 'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red @@ -193,7 +197,7 @@ class CategoryController extends Controller $chartData[$inKey] = [ - 'label' => sprintf('%s (%s)', (string)trans('firefly.earned'), $currencyInfo['currency_name']), + 'label' => sprintf('%s (%s)', (string) trans('firefly.earned'), $currencyInfo['currency_name']), 'entries' => [], 'type' => 'bar', 'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green diff --git a/app/Http/Controllers/Chart/CategoryReportController.php b/app/Http/Controllers/Chart/CategoryReportController.php index 6c90ed0462..238065a1fc 100644 --- a/app/Http/Controllers/Chart/CategoryReportController.php +++ b/app/Http/Controllers/Chart/CategoryReportController.php @@ -218,7 +218,7 @@ class CategoryReportController extends Controller $chartData[$spentKey] ??= [ 'label' => sprintf( '%s (%s)', - (string)trans('firefly.spent_in_specific_category', ['category' => $category->name]), + (string) trans('firefly.spent_in_specific_category', ['category' => $category->name]), $currency['currency_name'] ), 'type' => 'bar', @@ -245,7 +245,7 @@ class CategoryReportController extends Controller $chartData[$spentKey] ??= [ 'label' => sprintf( '%s (%s)', - (string)trans('firefly.earned_in_specific_category', ['category' => $category->name]), + (string) trans('firefly.earned_in_specific_category', ['category' => $category->name]), $currency['currency_name'] ), 'type' => 'bar', diff --git a/app/Http/Controllers/Chart/DoubleReportController.php b/app/Http/Controllers/Chart/DoubleReportController.php index fd367bcd7c..159b6dfefa 100644 --- a/app/Http/Controllers/Chart/DoubleReportController.php +++ b/app/Http/Controllers/Chart/DoubleReportController.php @@ -164,7 +164,7 @@ class DoubleReportController extends Controller $chartData[$spentKey] ??= [ 'label' => sprintf( '%s (%s)', - (string)trans('firefly.spent_in_specific_double', ['account' => $name]), + (string) trans('firefly.spent_in_specific_double', ['account' => $name]), $currency['currency_name'] ), 'type' => 'bar', @@ -190,7 +190,7 @@ class DoubleReportController extends Controller $chartData[$earnedKey] ??= [ 'label' => sprintf( '%s (%s)', - (string)trans('firefly.earned_in_specific_double', ['account' => $name]), + (string) trans('firefly.earned_in_specific_double', ['account' => $name]), $currency['currency_name'] ), 'type' => 'bar', diff --git a/app/Http/Controllers/Chart/ExpenseReportController.php b/app/Http/Controllers/Chart/ExpenseReportController.php index ec1ff4e84f..7b3997759a 100644 --- a/app/Http/Controllers/Chart/ExpenseReportController.php +++ b/app/Http/Controllers/Chart/ExpenseReportController.php @@ -103,27 +103,27 @@ class ExpenseReportController extends Controller /** @var Account $exp */ $exp = $combination->first(); $chartData[$exp->id.'-in'] = [ - 'label' => sprintf('%s (%s)', $name, (string)trans('firefly.income')), + 'label' => sprintf('%s (%s)', $name, (string) trans('firefly.income')), 'type' => 'bar', 'yAxisID' => 'y-axis-0', 'entries' => [], ]; $chartData[$exp->id.'-out'] = [ - 'label' => sprintf('%s (%s)', $name, (string)trans('firefly.expenses')), + 'label' => sprintf('%s (%s)', $name, (string) trans('firefly.expenses')), 'type' => 'bar', 'yAxisID' => 'y-axis-0', 'entries' => [], ]; // total in, total out: $chartData[$exp->id.'-total-in'] = [ - 'label' => sprintf('%s (%s)', $name, (string)trans('firefly.sum_of_income')), + 'label' => sprintf('%s (%s)', $name, (string) trans('firefly.sum_of_income')), 'type' => 'line', 'fill' => false, 'yAxisID' => 'y-axis-1', 'entries' => [], ]; $chartData[$exp->id.'-total-out'] = [ - 'label' => sprintf('%s (%s)', $name, (string)trans('firefly.sum_of_expenses')), + 'label' => sprintf('%s (%s)', $name, (string) trans('firefly.sum_of_expenses')), 'type' => 'line', 'fill' => false, 'yAxisID' => 'y-axis-1', diff --git a/app/Http/Controllers/Chart/PiggyBankController.php b/app/Http/Controllers/Chart/PiggyBankController.php index 4bf27e2748..661c362394 100644 --- a/app/Http/Controllers/Chart/PiggyBankController.php +++ b/app/Http/Controllers/Chart/PiggyBankController.php @@ -75,7 +75,7 @@ class PiggyBankController extends Controller $locale = app('steam')->getLocale(); // get first event or start date of piggy bank or today - $startDate = $piggyBank->startdate ?? today(config('app.timezone')); + $startDate = $piggyBank->start_date ?? today(config('app.timezone')); /** @var null|PiggyBankEvent $firstEvent */ $firstEvent = $set->first(); @@ -95,7 +95,7 @@ class PiggyBankController extends Controller } ); $currentSum = $filtered->sum('amount'); - $label = $oldest->isoFormat((string)trans('config.month_and_day_js', [], $locale)); + $label = $oldest->isoFormat((string) trans('config.month_and_day_js', [], $locale)); $chartData[$label] = $currentSum; $oldest = app('navigation')->addPeriod($oldest, $step, 0); } @@ -105,7 +105,7 @@ class PiggyBankController extends Controller } ); $finalSum = $finalFiltered->sum('amount'); - $finalLabel = $today->isoFormat((string)trans('config.month_and_day_js', [], $locale)); + $finalLabel = $today->isoFormat((string) trans('config.month_and_day_js', [], $locale)); $chartData[$finalLabel] = $finalSum; $data = $this->generator->singleSet($piggyBank->name, $chartData); diff --git a/app/Http/Controllers/Chart/ReportController.php b/app/Http/Controllers/Chart/ReportController.php index 7aef5e0a4c..cecdde2826 100644 --- a/app/Http/Controllers/Chart/ReportController.php +++ b/app/Http/Controllers/Chart/ReportController.php @@ -109,7 +109,7 @@ class ReportController extends Controller continue; } $currencyId = $netWorthItem['currency_id']; - $label = $current->isoFormat((string)trans('config.month_and_day_js', [], $locale)); + $label = $current->isoFormat((string) trans('config.month_and_day_js', [], $locale)); if (!array_key_exists($currencyId, $chartData)) { $chartData[$currencyId] = [ 'label' => 'Net worth in '.$netWorthItem['currency_name'], @@ -145,7 +145,7 @@ class ReportController extends Controller $cache->addProperty($accounts); $cache->addProperty($end); if ($cache->has()) { - // return response()->json($cache->get()); + return response()->json($cache->get()); } Log::debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray()); @@ -175,13 +175,13 @@ class ReportController extends Controller /** @var array $journal */ foreach ($journals as $journal) { $period = $journal['date']->format($format); - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $data[$currencyId] ??= [ 'currency_id' => $currencyId, 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], 'currency_name' => $journal['currency_name'], - 'currency_decimal_places' => (int)$journal['currency_decimal_places'], + 'currency_decimal_places' => (int) $journal['currency_decimal_places'], ]; $data[$currencyId][$period] ??= [ 'period' => $period, @@ -198,8 +198,8 @@ class ReportController extends Controller TransactionTypeEnum::DEPOSIT->value === $journal['transaction_type_type'] || (( TransactionTypeEnum::TRANSFER->value === $journal['transaction_type_type'] - || TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type'] - || TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] + || TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type'] + || TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] ) && in_array($journal['destination_account_id'], $ids, true))) { $key = 'earned'; @@ -214,7 +214,7 @@ class ReportController extends Controller foreach ($data as $currency) { Log::debug(sprintf('Now processing currency "%s"', $currency['currency_name'])); $income = [ - 'label' => (string)trans('firefly.box_earned_in_currency', ['currency' => $currency['currency_name']]), + 'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $currency['currency_name']]), 'type' => 'bar', 'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green 'currency_id' => $currency['currency_id'], @@ -223,7 +223,7 @@ class ReportController extends Controller 'entries' => [], ]; $expense = [ - 'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $currency['currency_name']]), + 'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $currency['currency_name']]), 'type' => 'bar', 'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red 'currency_id' => $currency['currency_id'], diff --git a/app/Http/Controllers/Chart/TagReportController.php b/app/Http/Controllers/Chart/TagReportController.php index 2d12c01539..5bea78116c 100644 --- a/app/Http/Controllers/Chart/TagReportController.php +++ b/app/Http/Controllers/Chart/TagReportController.php @@ -222,7 +222,7 @@ class TagReportController extends Controller $chartData[$spentKey] ??= [ 'label' => sprintf( '%s (%s)', - (string)trans('firefly.spent_in_specific_tag', ['tag' => $tag->tag]), + (string) trans('firefly.spent_in_specific_tag', ['tag' => $tag->tag]), $currency['currency_name'] ), 'type' => 'bar', @@ -249,7 +249,7 @@ class TagReportController extends Controller $chartData[$spentKey] ??= [ 'label' => sprintf( '%s (%s)', - (string)trans('firefly.earned_in_specific_tag', ['tag' => $tag->tag]), + (string) trans('firefly.earned_in_specific_tag', ['tag' => $tag->tag]), $currency['currency_name'] ), 'type' => 'bar', diff --git a/app/Http/Controllers/Chart/TransactionController.php b/app/Http/Controllers/Chart/TransactionController.php index d587d58024..fad9dc67fe 100644 --- a/app/Http/Controllers/Chart/TransactionController.php +++ b/app/Http/Controllers/Chart/TransactionController.php @@ -74,7 +74,7 @@ class TransactionController extends Controller // group by category. /** @var array $journal */ foreach ($result as $journal) { - $budget = $journal['budget_name'] ?? (string)trans('firefly.no_budget'); + $budget = $journal['budget_name'] ?? (string) trans('firefly.no_budget'); $title = sprintf('%s (%s)', $budget, $journal['currency_symbol']); $data[$title] ??= [ 'amount' => '0', @@ -124,7 +124,7 @@ class TransactionController extends Controller // group by category. /** @var array $journal */ foreach ($result as $journal) { - $category = $journal['category_name'] ?? (string)trans('firefly.no_category'); + $category = $journal['category_name'] ?? (string) trans('firefly.no_category'); $title = sprintf('%s (%s)', $category, $journal['currency_symbol']); $data[$title] ??= [ 'amount' => '0', diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 772020f672..237b02d55a 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -23,12 +23,17 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\RequestInformation; use FireflyIII\Support\Http\Controllers\UserNavigation; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; +use Illuminate\Support\Facades\View; +use Route; /** * Class Controller. @@ -44,8 +49,13 @@ abstract class Controller extends BaseController use UserNavigation; use ValidatesRequests; + // fails on PHP < 8.4 + public protected(set) string $name; + protected string $dateTimeFormat; protected string $monthAndDayFormat; + protected bool $convertToNative = false; + protected ?TransactionCurrency $defaultCurrency; protected string $monthFormat; protected string $redirectUrl = '/'; @@ -56,69 +66,73 @@ abstract class Controller extends BaseController { // is site a demo site? $isDemoSiteConfig = app('fireflyconfig')->get('is_demo_site', config('firefly.configuration.is_demo_site', false)); - $isDemoSite = (bool)$isDemoSiteConfig->data; - app('view')->share('IS_DEMO_SITE', $isDemoSite); - app('view')->share('DEMO_USERNAME', config('firefly.demo_username')); - app('view')->share('DEMO_PASSWORD', config('firefly.demo_password')); - app('view')->share('FF_VERSION', config('firefly.version')); + $isDemoSite = (bool) $isDemoSiteConfig->data; + View::share('IS_DEMO_SITE', $isDemoSite); + View::share('DEMO_USERNAME', config('firefly.demo_username')); + View::share('DEMO_PASSWORD', config('firefly.demo_password')); + View::share('FF_VERSION', config('firefly.version')); // is webhooks enabled? - app('view')->share('featuringWebhooks', true === config('firefly.feature_flags.webhooks') && true === config('firefly.allow_webhooks')); + View::share('featuringWebhooks', true === config('firefly.feature_flags.webhooks') && true === config('firefly.allow_webhooks')); // share custom auth guard info. - $authGuard = config('firefly.authentication_guard'); - $logoutUrl = config('firefly.custom_logout_url'); + $authGuard = config('firefly.authentication_guard'); + $logoutUrl = config('firefly.custom_logout_url'); // overrule v2 layout back to v1. if ('true' === request()->get('force_default_layout') && 'v2' === config('view.layout')) { - app('view')->getFinder()->setPaths([realpath(base_path('resources/views'))]); // @phpstan-ignore-line + View::getFinder()->setPaths([realpath(base_path('resources/views'))]); // @phpstan-ignore-line } - app('view')->share('authGuard', $authGuard); - app('view')->share('logoutUrl', $logoutUrl); + View::share('authGuard', $authGuard); + View::share('logoutUrl', $logoutUrl); // upload size - $maxFileSize = app('steam')->phpBytes((string)ini_get('upload_max_filesize')); - $maxPostSize = app('steam')->phpBytes((string)ini_get('post_max_size')); - $uploadSize = min($maxFileSize, $maxPostSize); - app('view')->share('uploadSize', $uploadSize); + $maxFileSize = Steam::phpBytes((string) ini_get('upload_max_filesize')); + $maxPostSize = Steam::phpBytes((string) ini_get('post_max_size')); + $uploadSize = min($maxFileSize, $maxPostSize); + View::share('uploadSize', $uploadSize); // share is alpha, is beta - $isAlpha = false; + $isAlpha = false; if (str_contains(config('firefly.version'), 'alpha')) { $isAlpha = true; } - $isBeta = false; + $isBeta = false; if (str_contains(config('firefly.version'), 'beta')) { $isBeta = true; } - app('view')->share('FF_IS_ALPHA', $isAlpha); - app('view')->share('FF_IS_BETA', $isBeta); + View::share('FF_IS_ALPHA', $isAlpha); + View::share('FF_IS_BETA', $isBeta); $this->middleware( function ($request, $next): mixed { - $locale = app('steam')->getLocale(); + $locale = Steam::getLocale(); // translations for specific strings: - $this->monthFormat = (string)trans('config.month_js', [], $locale); - $this->monthAndDayFormat = (string)trans('config.month_and_day_js', [], $locale); - $this->dateTimeFormat = (string)trans('config.date_time_js', [], $locale); + $this->monthFormat = (string) trans('config.month_js', [], $locale); + $this->monthAndDayFormat = (string) trans('config.month_and_day_js', [], $locale); + $this->dateTimeFormat = (string) trans('config.date_time_js', [], $locale); $darkMode = 'browser'; + $this->defaultCurrency =null; // get shown-intro-preference: if (auth()->check()) { - $language = app('steam')->getLanguage(); - $locale = app('steam')->getLocale(); + $this->defaultCurrency = Amount::getDefaultCurrency(); + $language = Steam::getLanguage(); + $locale = Steam::getLocale(); $darkMode = app('preferences')->get('darkMode', 'browser')->data; + $this->convertToNative =Amount::convertToNative(); $page = $this->getPageName(); $shownDemo = $this->hasSeenDemo(); - app('view')->share('language', $language); - app('view')->share('locale', $locale); - app('view')->share('shownDemo', $shownDemo); - app('view')->share('current_route_name', $page); - app('view')->share('original_route_name', \Route::currentRouteName()); + View::share('language', $language); + View::share('locale', $locale); + View::share('convertToNative', $this->convertToNative); + View::share('shownDemo', $shownDemo); + View::share('current_route_name', $page); + View::share('original_route_name', Route::currentRouteName()); } - app('view')->share('darkMode', $darkMode); + View::share('darkMode', $darkMode); return $next($request); } diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php index 350136fac7..db80fd18b4 100644 --- a/app/Http/Controllers/DebugController.php +++ b/app/Http/Controllers/DebugController.php @@ -25,11 +25,11 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; use Carbon\Carbon; -use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Middleware\IsDemoUser; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Support\Http\Controllers\GetConfigurationData; use FireflyIII\Support\Models\AccountBalanceCalculator; use FireflyIII\User; @@ -39,8 +39,10 @@ use Illuminate\Http\Request; use Illuminate\Routing\Redirector; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Route; use Illuminate\View\View; use Monolog\Handler\RotatingFileHandler; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class DebugController @@ -58,6 +60,69 @@ class DebugController extends Controller $this->middleware(IsDemoUser::class)->except(['displayError']); } + public function routes(): never + { + if (!auth()->user()->hasRole('owner')) { + throw new NotFoundHttpException(); + } + $routes = Route::getRoutes(); + $return = []; + + /** @var \Illuminate\Routing\Route $route */ + foreach ($routes as $route) { + // skip API and other routes. + if ( + str_starts_with($route->uri(), 'api') + || str_starts_with($route->uri(), '_debugbar') + || str_starts_with($route->uri(), '_ignition') + || str_starts_with($route->uri(), 'oauth') + || str_starts_with($route->uri(), 'chart') + || str_starts_with($route->uri(), 'v1/jscript') + || str_starts_with($route->uri(), 'v2/jscript') + || str_starts_with($route->uri(), 'json') + || str_starts_with($route->uri(), 'sanctum') + ) { + continue; + } + // skip non GET routes + if (!in_array('GET', $route->methods(), true)) { + continue; + } + // no name route: + if (null === $route->getName()) { + var_dump($route); + + exit; + } + if (!str_contains($route->uri(), '{')) { + + $return[$route->getName()] = route($route->getName()); + + continue; + } + $params = []; + foreach ($route->parameterNames() as $name) { + $params[] = $this->getParameter($name); + } + $return[$route->getName()] = route($route->getName(), $params); + } + $count = 0; + echo '
'; + echo '

Routes

'; + echo sprintf('

%s

', $count); + foreach ($return as $name => $path) { + echo sprintf('%2$s
', $path, $name).PHP_EOL; + ++$count; + if (0 === $count % 10) { + echo '
'; + echo sprintf('

%s

', $count); + } + } + + exit; + var_dump($return); + } + /** * Show all possible errors. * @@ -95,8 +160,8 @@ class DebugController extends Controller Artisan::call('view:clear'); // also do some recalculations. - Artisan::call('firefly-iii:trigger-credit-recalculation'); - AccountBalanceCalculator::recalculateAll(true); + Artisan::call('correction:recalculates-liabilities'); + AccountBalanceCalculator::recalculateAll(false); try { Artisan::call('twig:clean'); @@ -137,7 +202,7 @@ class DebugController extends Controller } if ('' !== $logContent) { // last few lines - $logContent = 'Truncated from this point <----|'.substr((string)$logContent, -16384); + $logContent = 'Truncated from this point <----|'.substr((string) $logContent, -16384); } return view('debug', compact('table', 'now', 'logContent')); @@ -151,13 +216,13 @@ class DebugController extends Controller $app = $this->getAppInfo(); $user = $this->getuserInfo(); - return (string)view('partials.debug-table', compact('system', 'docker', 'app', 'user')); + return (string) view('partials.debug-table', compact('system', 'docker', 'app', 'user')); } private function getSystemInformation(): array { - $maxFileSize = app('steam')->phpBytes((string)ini_get('upload_max_filesize')); - $maxPostSize = app('steam')->phpBytes((string)ini_get('post_max_size')); + $maxFileSize = app('steam')->phpBytes((string) ini_get('upload_max_filesize')); + $maxPostSize = app('steam')->phpBytes((string) ini_get('post_max_size')); $drivers = \DB::availableDrivers(); $currentDriver = \DB::getDriverName(); @@ -170,7 +235,7 @@ class DebugController extends Controller 'bits' => \PHP_INT_SIZE * 8, 'bcscale' => bcscale(), 'display_errors' => ini_get('display_errors'), - 'error_reporting' => $this->errorReporting((int)ini_get('error_reporting')), + 'error_reporting' => $this->errorReporting((int) ini_get('error_reporting')), 'upload_size' => min($maxFileSize, $maxPostSize), 'all_drivers' => $drivers, 'current_driver' => $currentDriver, @@ -189,7 +254,7 @@ class DebugController extends Controller try { if (file_exists('/var/www/counter-main.txt')) { - $return['build'] = trim((string)file_get_contents('/var/www/counter-main.txt')); + $return['build'] = trim((string) file_get_contents('/var/www/counter-main.txt')); app('log')->debug(sprintf('build is now "%s"', $return['build'])); } } catch (\Exception $e) { // @phpstan-ignore-line @@ -199,16 +264,16 @@ class DebugController extends Controller try { if (file_exists('/var/www/build-date-main.txt')) { - $return['build_date'] = trim((string)file_get_contents('/var/www/build-date-main.txt')); + $return['build_date'] = trim((string) file_get_contents('/var/www/build-date-main.txt')); } } catch (\Exception $e) { // @phpstan-ignore-line app('log')->debug('Could not check build date, but thats ok.'); app('log')->warning($e->getMessage()); } - if ('' !== (string)env('BASE_IMAGE_BUILD')) { + if ('' !== (string) env('BASE_IMAGE_BUILD')) { $return['base_build'] = env('BASE_IMAGE_BUILD'); } - if ('' !== (string)env('BASE_IMAGE_DATE')) { + if ('' !== (string) env('BASE_IMAGE_DATE')) { $return['base_build_date'] = env('BASE_IMAGE_DATE'); } @@ -220,7 +285,7 @@ class DebugController extends Controller $userGuard = config('auth.defaults.guard'); $config = app('fireflyconfig')->get('last_rt_job', 0); - $lastTime = (int)$config->data; + $lastTime = (int) $config->data; $lastCronjob = 'never'; $lastCronjobAgo = 'never'; if ($lastTime > 0) { @@ -232,8 +297,8 @@ class DebugController extends Controller return [ 'debug' => var_export(config('app.debug'), true), 'audit_log_channel' => envNonEmpty('AUDIT_LOG_CHANNEL', '(empty)'), - 'default_language' => (string)config('firefly.default_language'), - 'default_locale' => (string)config('firefly.default_locale'), + 'default_language' => (string) config('firefly.default_language'), + 'default_locale' => (string) config('firefly.default_locale'), 'remote_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_header') : 'N/A', 'remote_mail_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_email') : 'N/A', 'stateful_domains' => implode(', ', config('sanctum.stateful')), @@ -264,7 +329,7 @@ class DebugController extends Controller $result = setlocale(LC_ALL, $code); $localeAttempts[$code] = $result === $code; } - setlocale(LC_ALL, (string)$original); + setlocale(LC_ALL, (string) $original); return [ 'user_id' => auth()->user()->id, @@ -280,10 +345,10 @@ class DebugController extends Controller private function getUserFlags(): string { - $flags = []; + $flags = []; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); // has liabilities if ($user->accounts()->accountTypeIn([AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE])->count() > 0) { @@ -291,12 +356,15 @@ class DebugController extends Controller } // has piggies - if ($user->piggyBanks()->count() > 0) { + $repository = app(PiggyBankRepositoryInterface::class); + $repository->setUser($user); + + if ($repository->getPiggyBanks()->count() > 0) { $flags[] = ':pig:'; } // has stored reconciliations - $type = TransactionType::whereType(TransactionType::RECONCILIATION)->first(); + $type = TransactionType::whereType(TransactionType::RECONCILIATION)->first(); if ($user->transactionJournals()->where('transaction_type_id', $type->id)->count() > 0) { $flags[] = ':ledger:'; } @@ -340,4 +408,130 @@ class DebugController extends Controller return redirect(route('home')); } + + private function getParameter(string $name): string + { + switch ($name) { + default: + throw new FireflyException(sprintf('Unknown parameter "%s"', $name)); + + case 'cliToken': + case 'token': + case 'code': + case 'oldAddressHash': + return 'fake-token'; + + case 'objectType': + return 'asset'; + + case 'account': + return '1'; + + case 'start_date': + return '20241201'; + + case 'end_date': + return '20241231'; + + case 'attachment': + return '1'; + + case 'bill': + return '1'; + + case 'budget': + return '1'; + + case 'budgetLimit': + return '1'; + + case 'category': + return '1'; + + case 'currency': + return '1'; + + case 'fromCurrencyCode': + return 'EUR'; + + case 'toCurrencyCode': + return 'USD'; + + case 'accountList': + return '1,6'; + + case 'budgetList': + return '1,2'; + + case 'categoryList': + return '1,2'; + + case 'doubleList': + return '1,2'; + + case 'tagList': + return '1,2'; + + case 'tag': + return '1'; + + case 'piggyBank': + return '1'; + + case 'objectGroup': + return '1'; + + case 'route': + return 'accounts'; + + case 'specificPage': + return 'show'; + + case 'recurrence': + return '1'; + + case 'tj': + return '1'; + + case 'reportType': + return 'default'; + + case 'ruleGroup': + return '1'; + + case 'rule': + return '1'; + + case 'tagOrId': + return '1'; + + case 'transactionGroup': + return '1'; + + case 'journalList': + return '1,2'; + + case 'transactionType': + return 'withdrawal'; + + case 'journalLink': + return '1'; + + case 'webhook': + return '1'; + + case 'user': + return '1'; + + case 'linkType': + return '1'; + + case 'userGroup': + return '1'; + + case 'date': + return '20241201'; + + } + } } diff --git a/app/Http/Controllers/ExchangeRates/IndexController.php b/app/Http/Controllers/ExchangeRates/IndexController.php new file mode 100644 index 0000000000..4cff4418e2 --- /dev/null +++ b/app/Http/Controllers/ExchangeRates/IndexController.php @@ -0,0 +1,64 @@ +middleware( + function ($request, $next) { + app('view')->share('mainTitleIcon', 'fa-exchange'); + app('view')->share('title', (string) trans('firefly.header_exchange_rates')); + + return $next($request); + } + ); + if (!config('cer.enabled')) { + throw new NotFoundHttpException(); + } + } + + public function index(): View + { + return view('exchange-rates.index'); + } + + public function rates(TransactionCurrency $from, TransactionCurrency $to): View + { + return view('exchange-rates.rates', compact('from', 'to')); + } +} diff --git a/app/Http/Controllers/Export/IndexController.php b/app/Http/Controllers/Export/IndexController.php index 91da4e515e..267f827a27 100644 --- a/app/Http/Controllers/Export/IndexController.php +++ b/app/Http/Controllers/Export/IndexController.php @@ -52,7 +52,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-life-bouy'); - app('view')->share('title', (string)trans('firefly.export_data_title')); + app('view')->share('title', (string) trans('firefly.export_data_title')); $this->journalRepository = app(JournalRepositoryInterface::class); $this->middleware(IsDemoUser::class)->except(['index']); @@ -67,7 +67,7 @@ class IndexController extends Controller public function export(): LaravelResponse|RedirectResponse { if (auth()->user()->hasRole('demo')) { - session()->flash('info', (string)trans('firefly.demo_user_export')); + session()->flash('info', (string) trans('firefly.demo_user_export')); return redirect(route('export.index')); } @@ -103,7 +103,7 @@ class IndexController extends Controller ->header('Expires', '0') ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') ->header('Pragma', 'public') - ->header('Content-Length', (string)strlen($result['transactions'])) + ->header('Content-Length', (string) strlen($result['transactions'])) ; // return CSV file made from 'transactions' array. diff --git a/app/Http/Controllers/JavascriptController.php b/app/Http/Controllers/JavascriptController.php index ec31f0cdb2..7a0b271d14 100644 --- a/app/Http/Controllers/JavascriptController.php +++ b/app/Http/Controllers/JavascriptController.php @@ -49,14 +49,13 @@ class JavascriptController extends Controller $accounts = $repository->getAccountsByType( [AccountType::DEFAULT, AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD] ); - $default = app('amount')->getDefaultCurrency(); $data = ['accounts' => []]; /** @var Account $account */ foreach ($accounts as $account) { $accountId = $account->id; - $currency = (int)$repository->getMetaValue($account, 'currency_id'); - $currency = 0 === $currency ? $default->id : $currency; + $currency = (int) $repository->getMetaValue($account, 'currency_id'); + $currency = 0 === $currency ? $this->defaultCurrency->id : $currency; $entry = ['preferredCurrency' => $currency, 'name' => $account->name]; $data['accounts'][$accountId] = $entry; } @@ -95,10 +94,10 @@ class JavascriptController extends Controller * */ public function variables(Request $request, AccountRepositoryInterface $repository): Response { - $account = $repository->find((int)$request->get('account')); - $currency = app('amount')->getDefaultCurrency(); + $account = $repository->find((int) $request->get('account')); + $currency = $this->defaultCurrency; if (null !== $account) { - $currency = $repository->getAccountCurrency($account) ?? $currency; + $currency = $repository->getAccountCurrency($account) ?? $this->defaultCurrency; } $locale = app('steam')->getLocale(); $accounting = app('amount')->getJsConfig(); @@ -106,7 +105,7 @@ class JavascriptController extends Controller $pref = app('preferences')->get('language', config('firefly.default_language', 'en_US')); $lang = $pref->data; $dateRange = $this->getDateRangeConfig(); - $uid = substr(hash('sha256', sprintf('%s-%s-%s', (string)config('app.key'), auth()->user()->id, auth()->user()->email)), 0, 12); + $uid = substr(hash('sha256', sprintf('%s-%s-%s', (string) config('app.key'), auth()->user()->id, auth()->user()->email)), 0, 12); $data = [ 'currencyCode' => $currency->code, 'currencySymbol' => $currency->symbol, diff --git a/app/Http/Controllers/Json/BoxController.php b/app/Http/Controllers/Json/BoxController.php index 4e9ec22d24..5bd00fb5f4 100644 --- a/app/Http/Controllers/Json/BoxController.php +++ b/app/Http/Controllers/Json/BoxController.php @@ -24,18 +24,17 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Json; use Carbon\Carbon; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Report\NetWorthInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; -use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface; -use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Http\Controllers\DateCalculation; use Illuminate\Http\JsonResponse; @@ -47,108 +46,13 @@ class BoxController extends Controller use DateCalculation; /** - * This box has three types of info to display: - * 0) If the user has available amount this period and has overspent: overspent box. - * 1) If the user has available amount this period and has NOT overspent: left to spend box. - * 2) if the user has no available amount set this period: spent per day + * Deprecated method, no longer in use. * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @deprecated */ public function available(): JsonResponse { - app('log')->debug('Now in available()'); - - /** @var OperationsRepositoryInterface $opsRepository */ - $opsRepository = app(OperationsRepositoryInterface::class); - - /** @var AvailableBudgetRepositoryInterface $abRepository */ - $abRepository = app(AvailableBudgetRepositoryInterface::class); - $abRepository->cleanup(); - - /** @var Carbon $start */ - $start = session('start', today(config('app.timezone'))->startOfMonth()); - - /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); - $today = today(config('app.timezone')); - $display = 2; // see method docs. - $boxTitle = (string)trans('firefly.spent'); - - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($today); - $cache->addProperty('box-available'); - if ($cache->has()) { - return response()->json($cache->get()); - } - $leftPerDayAmount = '0'; - $leftToSpendAmount = '0'; - - $currency = app('amount')->getDefaultCurrency(); - app('log')->debug(sprintf('Default currency is %s', $currency->code)); - $availableBudgets = $abRepository->getAvailableBudgetsByExactDate($start, $end); - app('log')->debug(sprintf('Found %d available budget(s)', $availableBudgets->count())); - $availableBudgets = $availableBudgets->filter( - static function (AvailableBudget $availableBudget) use ($currency) { // @phpstan-ignore-line - if ($availableBudget->transaction_currency_id === $currency->id) { - app('log')->debug(sprintf( - 'Will include AB #%d: from %s-%s amount %s', - $availableBudget->id, - $availableBudget->start_date->format('Y-m-d'), - $availableBudget->end_date->format('Y-m-d'), - $availableBudget->amount - )); - - return $availableBudget; - } - - return null; - } - ); - app('log')->debug(sprintf('Filtered back to %d available budgets', $availableBudgets->count())); - // spent in this period, in budgets, for default currency. - // also calculate spent per day. - $spent = $opsRepository->sumExpenses($start, $end, null, null, $currency); - $spentAmount = $spent[$currency->id]['sum'] ?? '0'; - app('log')->debug(sprintf('Spent for default currency for all budgets in this period: %s', $spentAmount)); - - $days = (int)($today->between($start, $end) ? $today->diffInDays($start, true) + 1 : $end->diffInDays($start, true) + 1); - app('log')->debug(sprintf('Number of days left: %d', $days)); - $spentPerDay = bcdiv($spentAmount, (string)$days); - app('log')->debug(sprintf('Available to spend per day: %s', $spentPerDay)); - if ($availableBudgets->count() > 0) { - $display = 0; // assume user overspent - $boxTitle = (string)trans('firefly.overspent'); - $totalAvailableSum = (string)$availableBudgets->sum('amount'); - app('log')->debug(sprintf('Total available sum is %s', $totalAvailableSum)); - // calculate with available budget. - $leftToSpendAmount = bcadd($totalAvailableSum, $spentAmount); - app('log')->debug(sprintf('So left to spend is %s', $leftToSpendAmount)); - if (bccomp($leftToSpendAmount, '0') >= 0) { - app('log')->debug('Left to spend is positive or zero!'); - $boxTitle = (string)trans('firefly.left_to_spend'); - $activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description. - $display = 1; // not overspent - $leftPerDayAmount = 0 === $activeDaysLeft ? $leftToSpendAmount : bcdiv($leftToSpendAmount, (string)$activeDaysLeft); - app('log')->debug(sprintf('Left to spend per day is %s', $leftPerDayAmount)); - } - } - - $return = [ - 'display' => $display, - 'spent_total' => app('amount')->formatAnything($currency, $spentAmount, false), - 'spent_per_day' => app('amount')->formatAnything($currency, $spentPerDay, false), - 'left_to_spend' => app('amount')->formatAnything($currency, $leftToSpendAmount, false), - 'left_per_day' => app('amount')->formatAnything($currency, $leftPerDayAmount, false), - 'title' => $boxTitle, - ]; - app('log')->debug('Final output', $return); - - $cache->store($return); - app('log')->debug('Now done with available()'); - - return response()->json($return); + return response()->json([]); } /** @@ -165,6 +69,7 @@ class BoxController extends Controller $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('box-balance'); if ($cache->has()) { return response()->json($cache->get()); @@ -173,7 +78,7 @@ class BoxController extends Controller $incomes = []; $expenses = []; $sums = []; - $currency = app('amount')->getDefaultCurrency(); + $currency = $this->defaultCurrency; // collect income of user: /** @var GroupCollectorInterface $collector */ @@ -185,8 +90,8 @@ class BoxController extends Controller /** @var array $journal */ foreach ($set as $journal) { - $currencyId = (int)$journal['currency_id']; - $amount = $journal['amount'] ?? '0'; + $currencyId = $this->convertToNative && $this->defaultCurrency->id !== (int) $journal['currency_id'] ? $this->defaultCurrency->id : (int) $journal['currency_id']; + $amount = Amount::getAmountFromJournal($journal); $incomes[$currencyId] ??= '0'; $incomes[$currencyId] = bcadd($incomes[$currencyId], app('steam')->positive($amount)); $sums[$currencyId] ??= '0'; @@ -197,17 +102,18 @@ class BoxController extends Controller /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end) - ->setTypes([TransactionType::WITHDRAWAL]) + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]) ; $set = $collector->getExtractedJournals(); /** @var array $journal */ foreach ($set as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = $this->convertToNative ? $this->defaultCurrency->id : (int) $journal['currency_id']; + $amount = Amount::getAmountFromJournal($journal); $expenses[$currencyId] ??= '0'; - $expenses[$currencyId] = bcadd($expenses[$currencyId], $journal['amount'] ?? '0'); + $expenses[$currencyId] = bcadd($expenses[$currencyId], $amount); $sums[$currencyId] ??= '0'; - $sums[$currencyId] = bcadd($sums[$currencyId], $journal['amount']); + $sums[$currencyId] = bcadd($sums[$currencyId], $amount); } // format amounts: @@ -219,10 +125,10 @@ class BoxController extends Controller $expenses[$currencyId] = app('amount')->formatAnything($currency, $expenses[$currencyId] ?? '0', false); } if (0 === count($sums)) { - $currency = app('amount')->getDefaultCurrency(); - $sums[$currency->id] = app('amount')->formatAnything($currency, '0', false); - $incomes[$currency->id] = app('amount')->formatAnything($currency, '0', false); - $expenses[$currency->id] = app('amount')->formatAnything($currency, '0', false); + $currency = $this->defaultCurrency; + $sums[$this->defaultCurrency->id] = app('amount')->formatAnything($this->defaultCurrency, '0', false); + $incomes[$this->defaultCurrency->id] = app('amount')->formatAnything($this->defaultCurrency, '0', false); + $expenses[$this->defaultCurrency->id] = app('amount')->formatAnything($this->defaultCurrency, '0', false); } $response = [ diff --git a/app/Http/Controllers/Json/BudgetController.php b/app/Http/Controllers/Json/BudgetController.php index 10479402f3..ca72b59698 100644 --- a/app/Http/Controllers/Json/BudgetController.php +++ b/app/Http/Controllers/Json/BudgetController.php @@ -53,7 +53,7 @@ class BudgetController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.budgets')); + app('view')->share('title', (string) trans('firefly.budgets')); app('view')->share('mainTitleIcon', 'fa-pie-chart'); $this->repository = app(BudgetRepositoryInterface::class); $this->abRepository = app(AvailableBudgetRepositoryInterface::class); diff --git a/app/Http/Controllers/Json/FrontpageController.php b/app/Http/Controllers/Json/FrontpageController.php index a8ab134e96..8a42fbcf16 100644 --- a/app/Http/Controllers/Json/FrontpageController.php +++ b/app/Http/Controllers/Json/FrontpageController.php @@ -50,15 +50,15 @@ class FrontpageController extends Controller if (1 === bccomp($amount, '0')) { // percentage! $pct = 0; - if (0 !== bccomp($piggyBank->targetamount, '0')) { - $pct = (int)bcmul(bcdiv($amount, $piggyBank->targetamount), '100'); + if (0 !== bccomp($piggyBank->target_amount, '0')) { + $pct = (int) bcmul(bcdiv($amount, $piggyBank->target_amount), '100'); } $entry = [ 'id' => $piggyBank->id, 'name' => $piggyBank->name, 'amount' => $amount, - 'target' => $piggyBank->targetamount, + 'target' => $piggyBank->target_amount, 'percentage' => $pct, ]; diff --git a/app/Http/Controllers/Json/IntroController.php b/app/Http/Controllers/Json/IntroController.php index df92e6cf0a..6ea2ca32d2 100644 --- a/app/Http/Controllers/Json/IntroController.php +++ b/app/Http/Controllers/Json/IntroController.php @@ -103,7 +103,7 @@ class IntroController extends Controller app('preferences')->set($key, false); app('preferences')->mark(); - return response()->json(['message' => (string)trans('firefly.intro_boxes_after_refresh')]); + return response()->json(['message' => (string) trans('firefly.intro_boxes_after_refresh')]); } /** diff --git a/app/Http/Controllers/Json/ReconcileController.php b/app/Http/Controllers/Json/ReconcileController.php index d6f2408772..38c29896f3 100644 --- a/app/Http/Controllers/Json/ReconcileController.php +++ b/app/Http/Controllers/Json/ReconcileController.php @@ -32,6 +32,7 @@ use FireflyIII\Models\Account; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; @@ -55,7 +56,7 @@ class ReconcileController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-credit-card'); - app('view')->share('title', (string)trans('firefly.accounts')); + app('view')->share('title', (string) trans('firefly.accounts')); $this->accountRepos = app(AccountRepositoryInterface::class); return $next($request); @@ -72,7 +73,7 @@ class ReconcileController extends Controller { $startBalance = $request->get('startBalance'); $endBalance = $request->get('endBalance'); - $accountCurrency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); + $accountCurrency = $this->accountRepos->getAccountCurrency($account) ?? $this->defaultCurrency; $amount = '0'; $clearedAmount = '0'; @@ -122,10 +123,10 @@ class ReconcileController extends Controller Log::debug(sprintf('End balance: "%s"', $endBalance)); Log::debug(sprintf('Cleared amount: "%s"', $clearedAmount)); Log::debug(sprintf('Amount: "%s"', $amount)); - $difference = bcadd(bcadd(bcsub($startBalance, $endBalance), $clearedAmount), $amount); + $difference = bcadd(bcadd(bcsub($startBalance ?? '0', $endBalance ?? '0'), $clearedAmount ?? '0'), $amount); $diffCompare = bccomp($difference, '0'); $countCleared = count($clearedJournals); - $reconSum = bcadd(bcadd($startBalance, $amount), $clearedAmount); + $reconSum = bcadd(bcadd($startBalance ?? '0', $amount ?? '0'), $clearedAmount ?? '0'); try { $view = view('accounts.reconcile.overview', compact('account', 'start', 'diffCompare', 'difference', 'end', 'clearedAmount', 'startBalance', 'endBalance', 'amount', 'route', 'countCleared', 'reconSum', 'selectedIds'))->render(); @@ -192,9 +193,9 @@ class ReconcileController extends Controller $startDate->subDay(); $end->endOfDay(); - $currency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); - $startBalance = app('steam')->bcround(app('steam')->balance($account, $startDate), $currency->decimal_places); - $endBalance = app('steam')->bcround(app('steam')->balance($account, $end), $currency->decimal_places); + $currency = $this->accountRepos->getAccountCurrency($account) ?? $this->defaultCurrency; + $startBalance = Steam::finalAccountBalance($account, $startDate)['balance']; + $endBalance = Steam::finalAccountBalance($account, $end)['balance']; // get the transactions $selectionStart = clone $start; diff --git a/app/Http/Controllers/Json/RecurrenceController.php b/app/Http/Controllers/Json/RecurrenceController.php index 41369d0825..b423ec53ea 100644 --- a/app/Http/Controllers/Json/RecurrenceController.php +++ b/app/Http/Controllers/Json/RecurrenceController.php @@ -72,13 +72,13 @@ class RecurrenceController extends Controller $start = Carbon::createFromFormat('Y-m-d', $request->get('start')); $end = Carbon::createFromFormat('Y-m-d', $request->get('end')); $firstDate = Carbon::createFromFormat('Y-m-d', $request->get('first_date')); - $endDate = '' !== (string)$request->get('end_date') ? Carbon::createFromFormat('Y-m-d', $request->get('end_date')) : null; - $endsAt = (string)$request->get('ends'); + $endDate = '' !== (string) $request->get('end_date') ? Carbon::createFromFormat('Y-m-d', $request->get('end_date')) : null; + $endsAt = (string) $request->get('ends'); $repetitionType = explode(',', $request->get('type'))[0]; - $repetitions = (int)$request->get('reps'); - $weekend = (int)$request->get('weekend'); + $repetitions = (int) $request->get('reps'); + $weekend = (int) $request->get('weekend'); $repetitionMoment = ''; - $skip = (int)$request->get('skip'); + $skip = (int) $request->get('skip'); $skip = $skip < 0 || $skip > 31 ? 0 : $skip; $weekend = $weekend < 1 || $weekend > 4 ? 1 : $weekend; @@ -153,7 +153,7 @@ class RecurrenceController extends Controller */ public function suggest(Request $request): JsonResponse { - $string = '' === (string)$request->get('date') ? date('Y-m-d') : (string)$request->get('date'); + $string = '' === (string) $request->get('date') ? date('Y-m-d') : (string) $request->get('date'); $today = today(config('app.timezone'))->startOfDay(); try { @@ -165,37 +165,37 @@ class RecurrenceController extends Controller return response()->json(); } $date->startOfDay(); - $preSelected = (string)$request->get('pre_select'); + $preSelected = (string) $request->get('pre_select'); $locale = app('steam')->getLocale(); app('log')->debug(sprintf('date = %s, today = %s. date > today? %s', $date->toAtomString(), $today->toAtomString(), var_export($date > $today, true))); - app('log')->debug(sprintf('past = true? %s', var_export('true' === (string)$request->get('past'), true))); + app('log')->debug(sprintf('past = true? %s', var_export('true' === (string) $request->get('past'), true))); $result = []; - if ($date > $today || 'true' === (string)$request->get('past')) { + if ($date > $today || 'true' === (string) $request->get('past')) { app('log')->debug('Will fill dropdown.'); $weekly = sprintf('weekly,%s', $date->dayOfWeekIso); $monthly = sprintf('monthly,%s', $date->day); - $dayOfWeek = (string)trans(sprintf('config.dow_%s', $date->dayOfWeekIso)); + $dayOfWeek = (string) trans(sprintf('config.dow_%s', $date->dayOfWeekIso)); $ndom = sprintf('ndom,%s,%s', $date->weekOfMonth, $date->dayOfWeekIso); $yearly = sprintf('yearly,%s', $date->format('Y-m-d')); - $yearlyDate = $date->isoFormat((string)trans('config.month_and_day_no_year_js', [], $locale)); + $yearlyDate = $date->isoFormat((string) trans('config.month_and_day_no_year_js', [], $locale)); $result = [ - 'daily' => ['label' => (string)trans('firefly.recurring_daily'), 'selected' => str_starts_with($preSelected, 'daily')], + 'daily' => ['label' => (string) trans('firefly.recurring_daily'), 'selected' => str_starts_with($preSelected, 'daily')], $weekly => [ - 'label' => (string)trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek]), + 'label' => (string) trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek]), 'selected' => str_starts_with($preSelected, 'weekly'), ], $monthly => [ - 'label' => (string)trans('firefly.recurring_monthly', ['dayOfMonth' => $date->day]), + 'label' => (string) trans('firefly.recurring_monthly', ['dayOfMonth' => $date->day]), 'selected' => str_starts_with($preSelected, 'monthly'), ], $ndom => [ - 'label' => (string)trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $date->weekOfMonth]), + 'label' => (string) trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $date->weekOfMonth]), 'selected' => str_starts_with($preSelected, 'ndom'), ], $yearly => [ - 'label' => (string)trans('firefly.recurring_yearly', ['date' => $yearlyDate]), + 'label' => (string) trans('firefly.recurring_yearly', ['date' => $yearlyDate]), 'selected' => str_starts_with($preSelected, 'yearly'), ], ]; diff --git a/app/Http/Controllers/Json/RuleController.php b/app/Http/Controllers/Json/RuleController.php index dab72c3cb1..c91fb1c1e3 100644 --- a/app/Http/Controllers/Json/RuleController.php +++ b/app/Http/Controllers/Json/RuleController.php @@ -41,11 +41,11 @@ class RuleController extends Controller */ public function action(Request $request): JsonResponse { - $count = (int)$request->get('count') > 0 ? (int)$request->get('count') : 1; + $count = (int) $request->get('count') > 0 ? (int) $request->get('count') : 1; $keys = array_keys(config('firefly.rule-actions')); $actions = []; foreach ($keys as $key) { - $actions[$key] = (string)trans('firefly.rule_action_'.$key.'_choice'); + $actions[$key] = (string) trans('firefly.rule_action_'.$key.'_choice'); } try { @@ -68,12 +68,12 @@ class RuleController extends Controller */ public function trigger(Request $request): JsonResponse { - $count = (int)$request->get('count') > 0 ? (int)$request->get('count') : 1; + $count = (int) $request->get('count') > 0 ? (int) $request->get('count') : 1; $operators = config('search.operators'); $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { - $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); + $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key)); } } asort($triggers); diff --git a/app/Http/Controllers/NewUserController.php b/app/Http/Controllers/NewUserController.php index 2fd5e1e70b..08ebad3a43 100644 --- a/app/Http/Controllers/NewUserController.php +++ b/app/Http/Controllers/NewUserController.php @@ -66,7 +66,7 @@ class NewUserController extends Controller */ public function index() { - app('view')->share('title', (string)trans('firefly.welcome')); + app('view')->share('title', (string) trans('firefly.welcome')); app('view')->share('mainTitleIcon', 'fa-fire'); $types = config('firefly.accountTypesByIdentifier.asset'); @@ -98,7 +98,7 @@ class NewUserController extends Controller // set language preference: app('preferences')->set('language', $language); // Store currency preference from input: - $currency = $currencyRepository->find((int)$request->input('amount_currency_id_bank_balance')); + $currency = $currencyRepository->find((int) $request->input('amount_currency_id_bank_balance')); // if is null, set to EUR: if (null === $currency) { @@ -134,7 +134,7 @@ class NewUserController extends Controller ]; app('preferences')->set('transaction_journal_optional_fields', $visibleFields); - session()->flash('success', (string)trans('firefly.stored_new_accounts_new_user')); + session()->flash('success', (string) trans('firefly.stored_new_accounts_new_user')); app('preferences')->mark(); return redirect(route('index')); diff --git a/app/Http/Controllers/ObjectGroup/DeleteController.php b/app/Http/Controllers/ObjectGroup/DeleteController.php index f9115ec7f4..4a6c97181c 100644 --- a/app/Http/Controllers/ObjectGroup/DeleteController.php +++ b/app/Http/Controllers/ObjectGroup/DeleteController.php @@ -48,7 +48,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-envelope-o'); - app('view')->share('title', (string)trans('firefly.object_groups_page_title')); + app('view')->share('title', (string) trans('firefly.object_groups_page_title')); $this->repository = app(ObjectGroupRepositoryInterface::class); @@ -64,7 +64,7 @@ class DeleteController extends Controller */ public function delete(ObjectGroup $objectGroup) { - $subTitle = (string)trans('firefly.delete_object_group', ['title' => $objectGroup->title]); + $subTitle = (string) trans('firefly.delete_object_group', ['title' => $objectGroup->title]); $piggyBanks = $objectGroup->piggyBanks()->count(); // put previous url in session @@ -78,7 +78,7 @@ class DeleteController extends Controller */ public function destroy(ObjectGroup $objectGroup): RedirectResponse { - session()->flash('success', (string)trans('firefly.deleted_object_group', ['title' => $objectGroup->title])); + session()->flash('success', (string) trans('firefly.deleted_object_group', ['title' => $objectGroup->title])); app('preferences')->mark(); $this->repository->destroy($objectGroup); diff --git a/app/Http/Controllers/ObjectGroup/EditController.php b/app/Http/Controllers/ObjectGroup/EditController.php index e85ffac6a6..62be1e1edb 100644 --- a/app/Http/Controllers/ObjectGroup/EditController.php +++ b/app/Http/Controllers/ObjectGroup/EditController.php @@ -51,7 +51,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-envelope-o'); - app('view')->share('title', (string)trans('firefly.object_groups_page_title')); + app('view')->share('title', (string) trans('firefly.object_groups_page_title')); $this->repository = app(ObjectGroupRepositoryInterface::class); @@ -67,7 +67,7 @@ class EditController extends Controller */ public function edit(ObjectGroup $objectGroup) { - $subTitle = (string)trans('firefly.edit_object_group', ['title' => $objectGroup->title]); + $subTitle = (string) trans('firefly.edit_object_group', ['title' => $objectGroup->title]); $subTitleIcon = 'fa-pencil'; if (true !== session('object-groups.edit.fromUpdate')) { @@ -88,12 +88,12 @@ class EditController extends Controller $data = $request->getObjectGroupData(); $piggyBank = $this->repository->update($objectGroup, $data); - session()->flash('success', (string)trans('firefly.updated_object_group', ['title' => $objectGroup->title])); + session()->flash('success', (string) trans('firefly.updated_object_group', ['title' => $objectGroup->title])); app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('object-groups.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { session()->put('object-groups.edit.fromUpdate', true); $redirect = redirect(route('object-groups.edit', [$piggyBank->id])); diff --git a/app/Http/Controllers/ObjectGroup/IndexController.php b/app/Http/Controllers/ObjectGroup/IndexController.php index 168b3ea601..6cddb20792 100644 --- a/app/Http/Controllers/ObjectGroup/IndexController.php +++ b/app/Http/Controllers/ObjectGroup/IndexController.php @@ -50,7 +50,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-envelope-o'); - app('view')->share('title', (string)trans('firefly.object_groups_page_title')); + app('view')->share('title', (string) trans('firefly.object_groups_page_title')); $this->repository = app(ObjectGroupRepositoryInterface::class); return $next($request); @@ -65,7 +65,7 @@ class IndexController extends Controller { $this->repository->deleteEmpty(); $this->repository->resetOrder(); - $subTitle = (string)trans('firefly.object_groups_index'); + $subTitle = (string) trans('firefly.object_groups_index'); $objectGroups = $this->repository->get(); return view('object-groups.index', compact('subTitle', 'objectGroups')); @@ -77,7 +77,7 @@ class IndexController extends Controller public function setOrder(Request $request, ObjectGroup $objectGroup) { app('log')->debug(sprintf('Found object group #%d "%s"', $objectGroup->id, $objectGroup->title)); - $newOrder = (int)$request->get('order'); + $newOrder = (int) $request->get('order'); $this->repository->setOrder($objectGroup, $newOrder); return response()->json([]); diff --git a/app/Http/Controllers/PiggyBank/AmountController.php b/app/Http/Controllers/PiggyBank/AmountController.php index 28085daa06..b9d4bc29a8 100644 --- a/app/Http/Controllers/PiggyBank/AmountController.php +++ b/app/Http/Controllers/PiggyBank/AmountController.php @@ -26,12 +26,14 @@ namespace FireflyIII\Http\Controllers\PiggyBank; use Carbon\Carbon; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Account; use FireflyIII\Models\PiggyBank; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use Illuminate\View\View; /** @@ -51,7 +53,7 @@ class AmountController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.piggyBanks')); + app('view')->share('title', (string) trans('firefly.piggyBanks')); app('view')->share('mainTitleIcon', 'fa-bullseye'); $this->piggyRepos = app(PiggyBankRepositoryInterface::class); @@ -69,16 +71,26 @@ class AmountController extends Controller */ public function add(PiggyBank $piggyBank) { - $leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, today(config('app.timezone'))); - $savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank); - $maxAmount = $leftOnAccount; - if (0 !== bccomp($piggyBank->targetamount, '0')) { - $leftToSave = bcsub($piggyBank->targetamount, $savedSoFar); - $maxAmount = min($leftOnAccount, $leftToSave); + $accounts = []; + $total = '0'; + $totalSaved = $this->piggyRepos->getCurrentAmount($piggyBank); + $leftToSave = bcsub($piggyBank->target_amount, $totalSaved); + foreach ($piggyBank->accounts as $account) { + $leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $account, today(config('app.timezone'))->endOfDay()); + $savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank, $account); + $maxAmount = 0 === bccomp($piggyBank->target_amount, '0') ? $leftToSave : min($leftOnAccount, $leftToSave); + $accounts[] = [ + 'account' => $account, + 'left_on_account' => $leftOnAccount, + 'saved_so_far' => $savedSoFar, + 'left_to_save' => $leftToSave, + 'max_amount' => $maxAmount, + ]; + $total = bcadd($total, $leftOnAccount); } - $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); + $total = (float) $total; // intentional float. - return view('piggy-banks.add', compact('piggyBank', 'maxAmount', 'currency')); + return view('piggy-banks.add', compact('piggyBank', 'accounts', 'total')); } /** @@ -89,18 +101,24 @@ class AmountController extends Controller public function addMobile(PiggyBank $piggyBank) { /** @var Carbon $date */ - $date = session('end', today(config('app.timezone'))); - $leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $date); - $savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank); - $maxAmount = $leftOnAccount; - - if (0 !== bccomp($piggyBank->targetamount, '0')) { - $leftToSave = bcsub($piggyBank->targetamount, $savedSoFar); - $maxAmount = min($leftOnAccount, $leftToSave); + $date = session('end', today(config('app.timezone'))); + $accounts = []; + $total = '0'; + foreach ($piggyBank->accounts as $account) { + $leftOnAccount = $this->piggyRepos->leftOnAccount($piggyBank, $account, $date); + $savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank, $account); + $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); + $accounts[] = [ + 'account' => $account, + 'left_on_account' => $leftOnAccount, + 'saved_so_far' => $savedSoFar, + 'left_to_save' => $leftToSave, + 'max_amount' => 0 === bccomp($piggyBank->target_amount, '0') ? $leftOnAccount : min($leftOnAccount, $leftToSave), + ]; + $total = bcadd($total, $leftOnAccount); } - $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); - return view('piggy-banks.add-mobile', compact('piggyBank', 'maxAmount', 'currency')); + return view('piggy-banks.add-mobile', compact('piggyBank', 'total', 'accounts')); } /** @@ -108,32 +126,48 @@ class AmountController extends Controller */ public function postAdd(Request $request, PiggyBank $piggyBank): RedirectResponse { - $amount = $request->get('amount') ?? '0'; - $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); - // if amount is negative, make positive and continue: - if (-1 === bccomp($amount, '0')) { - $amount = bcmul($amount, '-1'); + $data = $request->all(); + $amounts = $data['amount'] ?? []; + $total = '0'; + Log::debug('Start with loop.'); + + /** @var Account $account */ + foreach ($piggyBank->accounts as $account) { + $amount = (string) ($amounts[$account->id] ?? '0'); + if ('' === $amount || 0 === bccomp($amount, '0')) { + continue; + } + if (-1 === bccomp($amount, '0')) { + $amount = bcmul($amount, '-1'); + } + + // small check to see if the $amount is not more than the total "left to save" value + $currentAmount = $this->piggyRepos->getCurrentAmount($piggyBank); + $leftToSave = 0 === bccomp($piggyBank->target_amount, '0') ? '0' : bcsub($piggyBank->target_amount, $currentAmount); + if (bccomp($amount, $leftToSave) > 0 && 0 !== bccomp($leftToSave, '0')) { + Log::debug(sprintf('Amount "%s" is more than left to save "%s". Using left to save.', $amount, $leftToSave)); + $amount = $leftToSave; + } + + $canAddAmount = $this->piggyRepos->canAddAmount($piggyBank, $account, $amount); + if ($canAddAmount) { + $this->piggyRepos->addAmount($piggyBank, $account, $amount); + $total = bcadd($total, $amount); + } + $piggyBank->refresh(); } - if ($this->piggyRepos->canAddAmount($piggyBank, $amount)) { - $this->piggyRepos->addAmount($piggyBank, $amount); - session()->flash( - 'success', - (string)trans( - 'firefly.added_amount_to_piggy', - ['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => $piggyBank->name] - ) - ); + if (0 !== bccomp($total, '0')) { + session()->flash('success', (string) trans('firefly.added_amount_to_piggy', ['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => $piggyBank->name])); app('preferences')->mark(); return redirect(route('piggy-banks.index')); } - - app('log')->error('Cannot add '.$amount.' because canAddAmount returned false.'); + app('log')->error(sprintf('Cannot add %s because canAddAmount returned false.', $total)); session()->flash( 'error', - (string)trans( + (string) trans( 'firefly.cannot_add_amount_piggy', - ['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)] + ['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => e($piggyBank->name)] ) ); @@ -145,32 +179,44 @@ class AmountController extends Controller */ public function postRemove(Request $request, PiggyBank $piggyBank): RedirectResponse { - $amount = $request->get('amount') ?? '0'; - $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); - // if amount is negative, make positive and continue: - if (-1 === bccomp($amount, '0')) { - $amount = bcmul($amount, '-1'); + $amounts = $request->get('amount') ?? []; + if (!is_array($amounts)) { + $amounts = []; } - if ($this->piggyRepos->canRemoveAmount($piggyBank, $amount)) { - $this->piggyRepos->removeAmount($piggyBank, $amount); + $total = '0'; + + /** @var Account $account */ + foreach ($piggyBank->accounts as $account) { + $amount = (string) ($amounts[$account->id] ?? '0'); + if ('' === $amount || 0 === bccomp($amount, '0')) { + continue; + } + if (-1 === bccomp($amount, '0')) { + $amount = bcmul($amount, '-1'); + } + if ($this->piggyRepos->canRemoveAmount($piggyBank, $account, $amount)) { + $this->piggyRepos->removeAmount($piggyBank, $account, $amount); + $total = bcadd($total, $amount); + } + } + if (0 !== bccomp($total, '0')) { session()->flash( 'success', - (string)trans( + (string) trans( 'firefly.removed_amount_from_piggy', - ['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => $piggyBank->name] + ['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => $piggyBank->name] ) ); app('preferences')->mark(); return redirect(route('piggy-banks.index')); } - $amount = (string)$request->get('amount'); session()->flash( 'error', - (string)trans( + (string) trans( 'firefly.cannot_remove_from_piggy', - ['amount' => app('amount')->formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)] + ['amount' => app('amount')->formatAnything($piggyBank->transactionCurrency, $total, false), 'name' => e($piggyBank->name)] ) ); @@ -184,10 +230,15 @@ class AmountController extends Controller */ public function remove(PiggyBank $piggyBank) { - $repetition = $this->piggyRepos->getRepetition($piggyBank); - $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); + $accounts = []; + foreach ($piggyBank->accounts as $account) { + $accounts[] = [ + 'account' => $account, + 'saved_so_far' => $this->piggyRepos->getCurrentAmount($piggyBank, $account), + ]; + } - return view('piggy-banks.remove', compact('piggyBank', 'repetition', 'currency')); + return view('piggy-banks.remove', compact('piggyBank', 'accounts')); } /** @@ -197,9 +248,14 @@ class AmountController extends Controller */ public function removeMobile(PiggyBank $piggyBank) { - $repetition = $this->piggyRepos->getRepetition($piggyBank); - $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); + $accounts = []; + foreach ($piggyBank->accounts as $account) { + $accounts[] = [ + 'account' => $account, + 'saved_so_far' => $this->piggyRepos->getCurrentAmount($piggyBank, $account), + ]; + } - return view('piggy-banks.remove-mobile', compact('piggyBank', 'repetition', 'currency')); + return view('piggy-banks.remove-mobile', compact('piggyBank', 'accounts')); } } diff --git a/app/Http/Controllers/PiggyBank/CreateController.php b/app/Http/Controllers/PiggyBank/CreateController.php index 92aac833d9..0edd81cbcc 100644 --- a/app/Http/Controllers/PiggyBank/CreateController.php +++ b/app/Http/Controllers/PiggyBank/CreateController.php @@ -31,6 +31,7 @@ use FireflyIII\Http\Requests\PiggyBankStoreRequest; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; use Illuminate\Routing\Redirector; use Illuminate\Support\Facades\Log; use Illuminate\View\View; @@ -52,7 +53,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.piggyBanks')); + app('view')->share('title', (string) trans('firefly.piggyBanks')); app('view')->share('mainTitleIcon', 'fa-bullseye'); $this->attachments = app(AttachmentHelperInterface::class); @@ -68,10 +69,12 @@ class CreateController extends Controller * * @return Factory|View */ - public function create() + public function create(Request $request) { - $subTitle = (string)trans('firefly.new_piggy_bank'); + $subTitle = (string) trans('firefly.new_piggy_bank'); $subTitleIcon = 'fa-plus'; + $hasOldInput = null !== $request->old('_token'); + $preFilled = $request->old(); // put previous url in session if not redirect from store (not "create another"). if (true !== session('piggy-banks.create.fromStore')) { @@ -79,7 +82,7 @@ class CreateController extends Controller } session()->forget('piggy-banks.create.fromStore'); - return view('piggy-banks.create', compact('subTitle', 'subTitleIcon')); + return view('piggy-banks.create', compact('subTitle', 'subTitleIcon', 'preFilled')); } /** @@ -92,12 +95,14 @@ class CreateController extends Controller public function store(PiggyBankStoreRequest $request) { $data = $request->getPiggyBankData(); - if (null === $data['startdate']) { - $data['startdate'] = today(config('app.timezone')); + + if (null === $data['start_date']) { + $data['start_date'] = today(config('app.timezone')); } $piggyBank = $this->piggyRepos->store($data); - session()->flash('success', (string)trans('firefly.stored_piggy_bank', ['name' => $piggyBank->name])); + session()->flash('success', (string) trans('firefly.stored_piggy_bank', ['name' => $piggyBank->name])); + session()->flash('success_url', route('piggy-banks.show', [$piggyBank->id])); app('preferences')->mark(); // store attachment(s): @@ -108,7 +113,7 @@ class CreateController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -116,7 +121,7 @@ class CreateController extends Controller } $redirect = redirect($this->getPreviousUrl('piggy-banks.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { session()->put('piggy-banks.create.fromStore', true); $redirect = redirect(route('piggy-banks.create'))->withInput(); diff --git a/app/Http/Controllers/PiggyBank/DeleteController.php b/app/Http/Controllers/PiggyBank/DeleteController.php index 754b62f01d..2ac0855b85 100644 --- a/app/Http/Controllers/PiggyBank/DeleteController.php +++ b/app/Http/Controllers/PiggyBank/DeleteController.php @@ -47,7 +47,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.piggyBanks')); + app('view')->share('title', (string) trans('firefly.piggyBanks')); app('view')->share('mainTitleIcon', 'fa-bullseye'); $this->piggyRepos = app(PiggyBankRepositoryInterface::class); @@ -64,7 +64,7 @@ class DeleteController extends Controller */ public function delete(PiggyBank $piggyBank) { - $subTitle = (string)trans('firefly.delete_piggy_bank', ['name' => $piggyBank->name]); + $subTitle = (string) trans('firefly.delete_piggy_bank', ['name' => $piggyBank->name]); // put previous url in session $this->rememberPreviousUrl('piggy-banks.delete.url'); @@ -77,7 +77,7 @@ class DeleteController extends Controller */ public function destroy(PiggyBank $piggyBank): RedirectResponse { - session()->flash('success', (string)trans('firefly.deleted_piggy_bank', ['name' => $piggyBank->name])); + session()->flash('success', (string) trans('firefly.deleted_piggy_bank', ['name' => $piggyBank->name])); app('preferences')->mark(); $this->piggyRepos->destroy($piggyBank); diff --git a/app/Http/Controllers/PiggyBank/EditController.php b/app/Http/Controllers/PiggyBank/EditController.php index c0a2de083e..f7574e59d9 100644 --- a/app/Http/Controllers/PiggyBank/EditController.php +++ b/app/Http/Controllers/PiggyBank/EditController.php @@ -54,7 +54,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.piggyBanks')); + app('view')->share('title', (string) trans('firefly.piggyBanks')); app('view')->share('mainTitleIcon', 'fa-bullseye'); $this->attachments = app(AttachmentHelperInterface::class); @@ -73,28 +73,27 @@ class EditController extends Controller */ public function edit(PiggyBank $piggyBank) { - $subTitle = (string)trans('firefly.update_piggy_title', ['name' => $piggyBank->name]); + $subTitle = (string) trans('firefly.update_piggy_title', ['name' => $piggyBank->name]); $subTitleIcon = 'fa-pencil'; $note = $piggyBank->notes()->first(); // Flash some data to fill the form. - $targetDate = $piggyBank->targetdate?->format('Y-m-d'); - $startDate = $piggyBank->startdate?->format('Y-m-d'); - $currency = $this->accountRepository->getAccountCurrency($piggyBank->account); - if (null === $currency) { - $currency = app('amount')->getDefaultCurrency(); - } + $targetDate = $piggyBank->target_date?->format('Y-m-d'); + $startDate = $piggyBank->start_date?->format('Y-m-d'); $preFilled = [ - 'name' => $piggyBank->name, - 'account_id' => $piggyBank->account_id, - 'targetamount' => app('steam')->bcround($piggyBank->targetamount, $currency->decimal_places), - 'targetdate' => $targetDate, - 'startdate' => $startDate, - 'object_group' => null !== $piggyBank->objectGroups->first() ? $piggyBank->objectGroups->first()->title : '', - 'notes' => null === $note ? '' : $note->text, + 'name' => $piggyBank->name, + 'target_amount' => app('steam')->bcround($piggyBank->target_amount, $piggyBank->transactionCurrency->decimal_places), + 'target_date' => $targetDate, + 'start_date' => $startDate, + 'accounts' => [], + 'object_group' => null !== $piggyBank->objectGroups->first() ? $piggyBank->objectGroups->first()->title : '', + 'notes' => null === $note ? '' : $note->text, ]; - if (0 === bccomp($piggyBank->targetamount, '0')) { - $preFilled['targetamount'] = ''; + foreach ($piggyBank->accounts as $account) { + $preFilled['accounts'][] = $account->id; + } + if (0 === bccomp($piggyBank->target_amount, '0')) { + $preFilled['target_amount'] = ''; } session()->flash('preFilled', $preFilled); @@ -117,7 +116,7 @@ class EditController extends Controller $data = $request->getPiggyBankData(); $piggyBank = $this->piggyRepos->update($piggyBank, $data); - session()->flash('success', (string)trans('firefly.updated_piggy_bank', ['name' => $piggyBank->name])); + session()->flash('success', (string) trans('firefly.updated_piggy_bank', ['name' => $piggyBank->name])); app('preferences')->mark(); // store new attachment(s): @@ -128,7 +127,7 @@ class EditController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -136,7 +135,7 @@ class EditController extends Controller } $redirect = redirect($this->getPreviousUrl('piggy-banks.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { session()->put('piggy-banks.edit.fromUpdate', true); $redirect = redirect(route('piggy-banks.edit', [$piggyBank->id])); diff --git a/app/Http/Controllers/PiggyBank/IndexController.php b/app/Http/Controllers/PiggyBank/IndexController.php index 72396eb20d..d461ec49b5 100644 --- a/app/Http/Controllers/PiggyBank/IndexController.php +++ b/app/Http/Controllers/PiggyBank/IndexController.php @@ -27,6 +27,7 @@ namespace FireflyIII\Http\Controllers\PiggyBank; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Account; use FireflyIII\Models\PiggyBank; use FireflyIII\Repositories\ObjectGroup\OrganisesObjectGroups; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; @@ -35,6 +36,7 @@ use FireflyIII\Transformers\PiggyBankTransformer; use Illuminate\Contracts\View\Factory; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Collection; use Illuminate\View\View; use Symfony\Component\HttpFoundation\ParameterBag; @@ -56,7 +58,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.piggyBanks')); + app('view')->share('title', (string) trans('firefly.piggyBanks')); app('view')->share('mainTitleIcon', 'fa-bullseye'); $this->piggyRepos = app(PiggyBankRepositoryInterface::class); @@ -80,7 +82,6 @@ class IndexController extends Controller $this->cleanupObjectGroups(); $this->piggyRepos->resetOrder(); $collection = $this->piggyRepos->getPiggyBanks(); - $accounts = []; /** @var Carbon $end */ $end = session('end', today(config('app.timezone'))->endOfMonth()); @@ -89,51 +90,15 @@ class IndexController extends Controller $parameters = new ParameterBag(); $parameters->set('end', $end); - // make piggy bank groups: - $piggyBanks = []; - - /** @var PiggyBankTransformer $transformer */ - $transformer = app(PiggyBankTransformer::class); - $transformer->setParameters(new ParameterBag()); /** @var AccountTransformer $accountTransformer */ $accountTransformer = app(AccountTransformer::class); $accountTransformer->setParameters($parameters); - /** @var PiggyBank $piggy */ - foreach ($collection as $piggy) { - $array = $transformer->transform($piggy); - $groupOrder = (int)$array['object_group_order']; - // make group array if necessary: - $piggyBanks[$groupOrder] ??= [ - 'object_group_id' => $array['object_group_id'] ?? 0, - 'object_group_title' => $array['object_group_title'] ?? trans('firefly.default_group_title_name'), - 'piggy_banks' => [], - ]; - - $account = $accountTransformer->transform($piggy->account); - $accountId = (int)$account['id']; - $array['attachments'] = $this->piggyRepos->getAttachments($piggy); - if (!array_key_exists($accountId, $accounts)) { - // create new: - $accounts[$accountId] = $account; - - // add some interesting details: - $accounts[$accountId]['left'] = $accounts[$accountId]['current_balance']; - $accounts[$accountId]['saved'] = 0; - $accounts[$accountId]['target'] = 0; - $accounts[$accountId]['to_save'] = 0; - } - - // calculate new interesting fields: - $accounts[$accountId]['left'] -= $array['current_amount']; - $accounts[$accountId]['saved'] += $array['current_amount']; - $accounts[$accountId]['target'] += $array['target_amount']; - $accounts[$accountId]['to_save'] += ($array['target_amount'] - $array['current_amount']); - $array['account_name'] = $account['name']; - $piggyBanks[$groupOrder]['piggy_banks'][] = $array; - } - // do a bunch of summaries. + // data + $piggyBanks = $this->groupPiggyBanks($collection); + $accounts = $this->collectAccounts($collection); + $accounts = $this->mergeAccountsAndPiggies($piggyBanks, $accounts); $piggyBanks = $this->makeSums($piggyBanks); ksort($piggyBanks); @@ -141,6 +106,99 @@ class IndexController extends Controller return view('piggy-banks.index', compact('piggyBanks', 'accounts')); } + private function groupPiggyBanks(Collection $collection): array + { + /** @var PiggyBankTransformer $transformer */ + $transformer = app(PiggyBankTransformer::class); + $transformer->setParameters(new ParameterBag()); + $piggyBanks = []; + + /** @var PiggyBank $piggy */ + foreach ($collection as $piggy) { + $array = $transformer->transform($piggy); + $groupOrder = (int) $array['object_group_order']; + $piggyBanks[$groupOrder] ??= [ + 'object_group_id' => $array['object_group_id'] ?? 0, + 'object_group_title' => $array['object_group_title'] ?? trans('firefly.default_group_title_name'), + 'piggy_banks' => [], + ]; + $array['attachments'] = $this->piggyRepos->getAttachments($piggy); + + // sum the total amount for the index. + $piggyBanks[$groupOrder]['piggy_banks'][] = $array; + } + + return $piggyBanks; + } + + private function collectAccounts(Collection $collection): array + { + /** @var Carbon $end */ + $end = session('end', today(config('app.timezone'))->endOfMonth()); + + // transform piggies using the transformer: + $parameters = new ParameterBag(); + $parameters->set('end', $end); + + /** @var AccountTransformer $accountTransformer */ + $accountTransformer = app(AccountTransformer::class); + $accountTransformer->setParameters($parameters); + + $return = []; + + /** @var PiggyBank $piggy */ + foreach ($collection as $piggy) { + $accounts = $piggy->accounts; + + /** @var Account $account */ + foreach ($accounts as $account) { + $array = $accountTransformer->transform($account); + $accountId = (int) $array['id']; + if (!array_key_exists($accountId, $return)) { + $return[$accountId] = $array; + + // add some interesting details: + $return[$accountId]['left'] = $return[$accountId]['current_balance']; + $return[$accountId]['saved'] = '0'; + $return[$accountId]['target'] = '0'; + $return[$accountId]['to_save'] = '0'; + } + + // calculate new interesting fields: + // $return[$accountId]['left'] -= $array['current_amount']; + // $return[$accountId]['saved'] += $array['current_amount']; + // $return[$accountId]['target'] += $array['target_amount']; + // $return[$accountId]['to_save'] += ($array['target_amount'] - $array['current_amount']); + // $return['account_name'] = $account['name']; + + } + } + + return $return; + } + + private function mergeAccountsAndPiggies(array $piggyBanks, array $accounts): array + { + // @var array $piggyBank + foreach ($piggyBanks as $group) { + foreach ($group['piggy_banks'] as $piggyBank) { + // loop all accounts in this piggy bank subtract the current amount from "left to save" in the $accounts array. + /** @var array $piggyAccount */ + foreach ($piggyBank['accounts'] as $piggyAccount) { + $accountId = $piggyAccount['id']; + if (array_key_exists($accountId, $accounts)) { + $accounts[$accountId]['left'] = bcsub($accounts[$accountId]['left'], $piggyAccount['current_amount']); + $accounts[$accountId]['saved'] = bcadd($accounts[$accountId]['saved'], $piggyAccount['current_amount']); + $accounts[$accountId]['target'] = bcadd($accounts[$accountId]['target'], $piggyBank['target_amount']); + $accounts[$accountId]['to_save'] = bcadd($accounts[$accountId]['to_save'], bcsub($piggyBank['target_amount'], $piggyAccount['current_amount'])); + } + } + } + } + + return $accounts; + } + private function makeSums(array $piggyBanks): array { $sums = []; @@ -162,10 +220,10 @@ class IndexController extends Controller // current_amount // left_to_save // save_per_month - $sums[$groupId][$currencyId]['target'] = bcadd($sums[$groupId][$currencyId]['target'], (string)$piggy['target_amount']); - $sums[$groupId][$currencyId]['saved'] = bcadd($sums[$groupId][$currencyId]['saved'], (string)$piggy['current_amount']); - $sums[$groupId][$currencyId]['left_to_save'] = bcadd($sums[$groupId][$currencyId]['left_to_save'], (string)$piggy['left_to_save']); - $sums[$groupId][$currencyId]['save_per_month'] = bcadd($sums[$groupId][$currencyId]['save_per_month'], (string)$piggy['save_per_month']); + $sums[$groupId][$currencyId]['target'] = bcadd($sums[$groupId][$currencyId]['target'], (string) $piggy['target_amount']); + $sums[$groupId][$currencyId]['saved'] = bcadd($sums[$groupId][$currencyId]['saved'], (string) $piggy['current_amount']); + $sums[$groupId][$currencyId]['left_to_save'] = bcadd($sums[$groupId][$currencyId]['left_to_save'], (string) $piggy['left_to_save']); + $sums[$groupId][$currencyId]['save_per_month'] = bcadd($sums[$groupId][$currencyId]['save_per_month'], (string) $piggy['save_per_month']); } } foreach ($piggyBanks as $groupOrder => $group) { @@ -181,8 +239,8 @@ class IndexController extends Controller */ public function setOrder(Request $request, PiggyBank $piggyBank): JsonResponse { - $objectGroupTitle = (string)$request->get('objectGroupTitle'); - $newOrder = (int)$request->get('order'); + $objectGroupTitle = (string) $request->get('objectGroupTitle'); + $newOrder = (int) $request->get('order'); $this->piggyRepos->setOrder($piggyBank, $newOrder); if ('' !== $objectGroupTitle) { $this->piggyRepos->setObjectGroup($piggyBank, $objectGroupTitle); diff --git a/app/Http/Controllers/PiggyBank/ShowController.php b/app/Http/Controllers/PiggyBank/ShowController.php index 2fd69286ae..d1a69a6c1c 100644 --- a/app/Http/Controllers/PiggyBank/ShowController.php +++ b/app/Http/Controllers/PiggyBank/ShowController.php @@ -50,7 +50,7 @@ class ShowController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.piggyBanks')); + app('view')->share('title', (string) trans('firefly.piggyBanks')); app('view')->share('mainTitleIcon', 'fa-bullseye'); $this->piggyRepos = app(PiggyBankRepositoryInterface::class); @@ -83,6 +83,7 @@ class ShowController extends Controller $subTitle = $piggyBank->name; $attachments = $this->piggyRepos->getAttachments($piggyBank); + return view('piggy-banks.show', compact('piggyBank', 'events', 'subTitle', 'piggy', 'attachments')); } } diff --git a/app/Http/Controllers/PreferencesController.php b/app/Http/Controllers/PreferencesController.php index 1206cde540..702eb0bd62 100644 --- a/app/Http/Controllers/PreferencesController.php +++ b/app/Http/Controllers/PreferencesController.php @@ -23,16 +23,20 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; +use FireflyIII\Events\Preferences\UserGroupChangedDefaultCurrency; +use FireflyIII\Events\Test\UserTestNotificationChannel; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Http\Requests\PreferencesRequest; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Preference; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\User; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; +use Illuminate\Support\Facades\Log; use Illuminate\View\View; /** @@ -49,7 +53,7 @@ class PreferencesController extends Controller $this->middleware( static function ($request, $next) { - app('view')->share('title', (string)trans('firefly.preferences')); + app('view')->share('title', (string) trans('firefly.preferences')); app('view')->share('mainTitleIcon', 'fa-gear'); return $next($request); @@ -66,14 +70,14 @@ class PreferencesController extends Controller */ public function index(AccountRepositoryInterface $repository) { - $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); - $isDocker = env('IS_DOCKER', false); - $groupedAccounts = []; + $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); + $isDocker = env('IS_DOCKER', false); + $groupedAccounts = []; /** @var Account $account */ foreach ($accounts as $account) { - $type = $account->accountType->type; - $role = sprintf('opt_group_%s', $repository->getMetaValue($account, 'account_role')); + $type = $account->accountType->type; + $role = sprintf('opt_group_%s', $repository->getMetaValue($account, 'account_role')); if (in_array($type, [AccountType::MORTGAGE, AccountType::DEBT, AccountType::LOAN], true)) { $role = sprintf('opt_group_l_%s', $type); @@ -82,64 +86,121 @@ class PreferencesController extends Controller if ('opt_group_' === $role) { $role = 'opt_group_defaultAsset'; } - $groupedAccounts[(string)trans(sprintf('firefly.%s', $role))][$account->id] = $account->name; + $groupedAccounts[(string) trans(sprintf('firefly.%s', $role))][$account->id] = $account->name; } ksort($groupedAccounts); /** @var array $accountIds */ - $accountIds = $accounts->pluck('id')->toArray(); - $viewRange = app('navigation')->getViewRange(false); - $frontpageAccountsPref = app('preferences')->get('frontpageAccounts', $accountIds); - $frontpageAccounts = $frontpageAccountsPref->data; + $accountIds = $accounts->pluck('id')->toArray(); + $viewRange = app('navigation')->getViewRange(false); + $frontpageAccountsPref = app('preferences')->get('frontpageAccounts', $accountIds); + $frontpageAccounts = $frontpageAccountsPref->data; if (!is_array($frontpageAccounts)) { $frontpageAccounts = $accountIds; } - $language = app('steam')->getLanguage(); - $languages = config('firefly.languages'); - $locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data; - $listPageSize = app('preferences')->get('listPageSize', 50)->data; - $darkMode = app('preferences')->get('darkMode', 'browser')->data; - $slackUrl = app('preferences')->get('slack_webhook_url', '')->data; - $customFiscalYear = app('preferences')->get('customFiscalYear', 0)->data; - $fiscalYearStartStr = app('preferences')->get('fiscalYearStart', '01-01')->data; + $language = app('steam')->getLanguage(); + $languages = config('firefly.languages'); + $locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data; + $listPageSize = app('preferences')->get('listPageSize', 50)->data; + $darkMode = app('preferences')->get('darkMode', 'browser')->data; + $customFiscalYear = app('preferences')->get('customFiscalYear', 0)->data; + $fiscalYearStartStr = app('preferences')->get('fiscalYearStart', '01-01')->data; + $convertToNative = $this->convertToNative; if (is_array($fiscalYearStartStr)) { $fiscalYearStartStr = '01-01'; } - $fiscalYearStart = sprintf('%s-%s', date('Y'), (string)$fiscalYearStartStr); - $tjOptionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data; - $availableDarkModes = config('firefly.available_dark_modes'); + $fiscalYearStart = sprintf('%s-%s', date('Y'), (string) $fiscalYearStartStr); + $tjOptionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data; + $availableDarkModes = config('firefly.available_dark_modes'); - // notification preferences (single value for each): - $notifications = []; - foreach (config('firefly.available_notifications') as $notification) { - $notifications[$notification] = app('preferences')->get(sprintf('notification_%s', $notification), true)->data; + // notifications settings + $slackUrl = app('preferences')->getEncrypted('slack_webhook_url', '')->data; + $pushoverAppToken = (string) app('preferences')->getEncrypted('pushover_app_token', '')->data; + $pushoverUserToken = (string) app('preferences')->getEncrypted('pushover_user_token', '')->data; + $ntfyServer = app('preferences')->getEncrypted('ntfy_server', 'https://ntfy.sh')->data; + $ntfyTopic = (string) app('preferences')->getEncrypted('ntfy_topic', '')->data; + $ntfyAuth = app('preferences')->get('ntfy_auth', false)->data; + $ntfyUser = app('preferences')->getEncrypted('ntfy_user', '')->data; + $ntfyPass = (string) app('preferences')->getEncrypted('ntfy_pass', '')->data; + $channels = config('notifications.channels'); + $forcedAvailability = []; + + // notification preferences + $notifications = []; + foreach (config('notifications.notifications.user') as $key => $info) { + if ($info['enabled']) { + $notifications[$key] + = [ + 'enabled' => app('preferences')->get(sprintf('notification_%s', $key), true)->data, + 'configurable' => $info['configurable'], + ]; + } } + // loop all channels to see if they are available. + foreach ($channels as $channel => $info) { + $forcedAvailability[$channel] = true; + } + $forcedAvailability['ntfy'] = '' !== $ntfyTopic; + $forcedAvailability['pushover'] = '' !== $pushoverAppToken && '' !== $pushoverUserToken; ksort($languages); // list of locales also has "equal" which makes it equal to whatever the language is. try { - $locales = json_decode((string)file_get_contents(resource_path(sprintf('locales/%s/locales.json', $language))), true, 512, JSON_THROW_ON_ERROR); + $locales = json_decode((string) file_get_contents(resource_path(sprintf('locales/%s/locales.json', $language))), true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException $e) { app('log')->error($e->getMessage()); $locales = []; } - $locales = ['equal' => (string)trans('firefly.equal_to_language')] + $locales; + $locales = ['equal' => (string) trans('firefly.equal_to_language')] + $locales; // an important fallback is that the frontPageAccount array gets refilled automatically // when it turns up empty. if (0 === count($frontpageAccounts)) { $frontpageAccounts = $accountIds; } - // for the demo user, the slackUrl is automatically emptied. - // this isn't really secure, but it means that the demo site has a semi-secret - // slackUrl. + // for the demo user, the notification settings are automatically emptied. + // this isn't really secure, but it means that the demo site has semi-secret notification settings. if (auth()->user()->hasRole('demo')) { - $slackUrl = ''; + $slackUrl = ''; + $pushoverAppToken = ''; + $pushoverUserToken = ''; + $ntfyServer = ''; + $ntfyTopic = ''; + $ntfyAuth = false; + $ntfyUser = ''; + $ntfyPass = ''; } - return view('preferences.index', compact('language', 'groupedAccounts', 'isDocker', 'frontpageAccounts', 'languages', 'darkMode', 'availableDarkModes', 'notifications', 'slackUrl', 'locales', 'locale', 'tjOptionalFields', 'viewRange', 'customFiscalYear', 'listPageSize', 'fiscalYearStart')); + return view('preferences.index', compact( + 'language', + 'pushoverAppToken', + 'pushoverUserToken', + 'ntfyServer', + 'ntfyTopic', + 'ntfyAuth', + 'channels', + 'ntfyUser', + 'forcedAvailability', + 'ntfyPass', + 'groupedAccounts', + 'isDocker', + 'frontpageAccounts', + 'languages', + 'darkMode', + 'availableDarkModes', + 'notifications', + 'convertToNative', + 'slackUrl', + 'locales', + 'locale', + 'tjOptionalFields', + 'viewRange', + 'customFiscalYear', + 'listPageSize', + 'fiscalYearStart' + )); } /** @@ -152,21 +213,21 @@ class PreferencesController extends Controller * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function postIndex(Request $request) + public function postIndex(PreferencesRequest $request) { // front page accounts $frontpageAccounts = []; if (is_array($request->get('frontpageAccounts')) && count($request->get('frontpageAccounts')) > 0) { foreach ($request->get('frontpageAccounts') as $id) { - $frontpageAccounts[] = (int)$id; + $frontpageAccounts[] = (int) $id; } app('preferences')->set('frontpageAccounts', $frontpageAccounts); } // extract notifications: $all = $request->all(); - foreach (config('firefly.available_notifications') as $option) { - $key = sprintf('notification_%s', $option); + foreach (config('notifications.notifications.user') as $key => $info) { + $key = sprintf('notification_%s', $key); if (array_key_exists($key, $all)) { app('preferences')->set($key, true); } @@ -182,20 +243,33 @@ class PreferencesController extends Controller session()->forget('end'); session()->forget('range'); - // slack URL: + // notification settings, cannot be set by the demo user. if (!auth()->user()->hasRole('demo')) { - $url = (string)$request->get('slackUrl'); - if (UrlValidator::isValidWebhookURL($url)) { - app('preferences')->set('slack_webhook_url', $url); - } - if ('' === $url) { - app('preferences')->delete('slack_webhook_url'); + + $variables = ['slack_webhook_url', 'pushover_app_token', 'pushover_user_token', 'ntfy_server', 'ntfy_topic', 'ntfy_user', 'ntfy_pass']; + foreach ($variables as $variable) { + if ('' === $all[$variable]) { + app('preferences')->delete($variable); + } + if ('' !== $all[$variable]) { + app('preferences')->setEncrypted($variable, $all[$variable]); + } } + app('preferences')->set('ntfy_auth', $all['ntfy_auth'] ?? false); } + // convert native + $convertToNative = 1 === (int) $request->get('convertToNative'); + if ($convertToNative && !$this->convertToNative) { + // set to true! + Log::debug('User sets convertToNative to true.'); + event(new UserGroupChangedDefaultCurrency(auth()->user()->userGroup)); + } + app('preferences')->set('convert_to_native', $convertToNative); + // custom fiscal year - $customFiscalYear = 1 === (int)$request->get('customFiscalYear'); - $string = strtotime((string)$request->get('fiscalYearStart')); + $customFiscalYear = 1 === (int) $request->get('customFiscalYear'); + $string = strtotime((string) $request->get('fiscalYearStart')); if (false !== $string) { $fiscalYearStart = date('m-d', $string); app('preferences')->set('customFiscalYear', $customFiscalYear); @@ -204,7 +278,7 @@ class PreferencesController extends Controller // save page size: app('preferences')->set('listPageSize', 50); - $listPageSize = (int)$request->get('listPageSize'); + $listPageSize = (int) $request->get('listPageSize'); if ($listPageSize > 0 && $listPageSize < 1337) { app('preferences')->set('listPageSize', $listPageSize); } @@ -252,9 +326,35 @@ class PreferencesController extends Controller app('preferences')->set('darkMode', $darkMode); } - session()->flash('success', (string)trans('firefly.saved_preferences')); + session()->flash('success', (string) trans('firefly.saved_preferences')); app('preferences')->mark(); return redirect(route('preferences.index')); } + + public function testNotification(Request $request): mixed + { + + $all = $request->all(); + $channel = $all['channel'] ?? ''; + + switch ($channel) { + default: + session()->flash('error', (string) trans('firefly.notification_test_failed', ['channel' => $channel])); + + break; + + case 'email': + case 'slack': + case 'pushover': + case 'ntfy': + /** @var User $user */ + $user = auth()->user(); + app('log')->debug(sprintf('Now in testNotification("%s") controller.', $channel)); + event(new UserTestNotificationChannel($channel, $user)); + session()->flash('success', (string) trans('firefly.notification_test_executed', ['channel' => $channel])); + } + + return ''; + } } diff --git a/app/Http/Controllers/Profile/MfaController.php b/app/Http/Controllers/Profile/MfaController.php index cba791f4fc..340d7240ef 100644 --- a/app/Http/Controllers/Profile/MfaController.php +++ b/app/Http/Controllers/Profile/MfaController.php @@ -80,19 +80,59 @@ class MfaController extends Controller } - public function index(): Factory|RedirectResponse|View + /** + * @throws FireflyException + */ + public function backupCodes(Request $request): Factory|RedirectResponse|View { if (!$this->internalAuth) { - request()->session()->flash('error', trans('firefly.external_user_mgt_disabled')); + $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); + + return redirect(route('profile.index')); + } + $enabledMFA = null !== auth()->user()->mfa_secret; + if (false === $enabledMFA) { + request()->session()->flash('info', trans('firefly.mfa_not_enabled')); return redirect(route('profile.index')); } - $subTitle = (string)trans('firefly.mfa_index_title'); - $subTitleIcon = 'fa-calculator'; - $enabledMFA = null !== auth()->user()->mfa_secret; + return view('profile.mfa.backup-codes-intro'); + } + + public function backupCodesPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse|View + { + if (!$this->internalAuth) { + $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); + + return redirect(route('profile.index')); + } + $enabledMFA = null !== auth()->user()->mfa_secret; + if (false === $enabledMFA) { + request()->session()->flash('info', trans('firefly.mfa_not_enabled')); + + return redirect(route('profile.index')); + } + // generate recovery codes: + $recovery = app(Recovery::class); + $recoveryCodes = $recovery->lowercase() + ->setCount(8) // Generate 8 codes + ->setBlocks(2) // Every code must have 2 blocks + ->setChars(6) // Each block must have 6 chars + ->toArray() + ; + $codes = implode("\r\n", $recoveryCodes); + + app('preferences')->set('mfa_recovery', $recoveryCodes); + app('preferences')->mark(); + + // send user notification. + $user = auth()->user(); + Log::channel('audit')->info(sprintf('User "%s" has generated new backup codes.', $user->email)); + event(new MFANewBackupCodes($user)); + + return view('profile.mfa.backup-codes-post')->with(compact('codes')); - return view('profile.mfa.index')->with(compact('subTitle', 'subTitleIcon', 'enabledMFA')); } public function disableMFA(Request $request): Factory|RedirectResponse|View @@ -108,7 +148,7 @@ class MfaController extends Controller return redirect(route('profile.index')); } - $subTitle = (string)trans('firefly.mfa_index_title'); + $subTitle = (string) trans('firefly.mfa_index_title'); $subTitleIcon = 'fa-calculator'; return view('profile.mfa.disable-mfa')->with(compact('subTitle', 'subTitleIcon', 'enabledMFA')); @@ -180,66 +220,10 @@ class MfaController extends Controller app('preferences')->set('temp-mfa-secret', $secret); - return view('profile.mfa.enable-mfa', compact('image', 'secret')); } - public function backupCodesPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse|View - { - if (!$this->internalAuth) { - $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); - - return redirect(route('profile.index')); - } - $enabledMFA = null !== auth()->user()->mfa_secret; - if (false === $enabledMFA) { - request()->session()->flash('info', trans('firefly.mfa_not_enabled')); - - return redirect(route('profile.index')); - } - // generate recovery codes: - $recovery = app(Recovery::class); - $recoveryCodes = $recovery->lowercase() - ->setCount(8) // Generate 8 codes - ->setBlocks(2) // Every code must have 2 blocks - ->setChars(6) // Each block must have 6 chars - ->toArray() - ; - $codes = implode("\r\n", $recoveryCodes); - - app('preferences')->set('mfa_recovery', $recoveryCodes); - app('preferences')->mark(); - - // send user notification. - $user = auth()->user(); - Log::channel('audit')->info(sprintf('User "%s" has generated new backup codes.', $user->email)); - event(new MFANewBackupCodes($user)); - - return view('profile.mfa.backup-codes-post')->with(compact('codes')); - - } - - /** - * @throws FireflyException - */ - public function backupCodes(Request $request): Factory|RedirectResponse|View - { - if (!$this->internalAuth) { - $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); - - return redirect(route('profile.index')); - } - $enabledMFA = null !== auth()->user()->mfa_secret; - if (false === $enabledMFA) { - request()->session()->flash('info', trans('firefly.mfa_not_enabled')); - - return redirect(route('profile.index')); - } - - return view('profile.mfa.backup-codes-intro'); - } - /** * Submit 2FA for the first time. * @@ -340,4 +324,19 @@ class MfaController extends Controller } app('preferences')->set('mfa_history', $newHistory); } + + public function index(): Factory|RedirectResponse|View + { + if (!$this->internalAuth) { + request()->session()->flash('error', trans('firefly.external_user_mgt_disabled')); + + return redirect(route('profile.index')); + } + + $subTitle = (string) trans('firefly.mfa_index_title'); + $subTitleIcon = 'fa-calculator'; + $enabledMFA = null !== auth()->user()->mfa_secret; + + return view('profile.mfa.index')->with(compact('subTitle', 'subTitleIcon', 'enabledMFA')); + } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index b270c86905..95136a1018 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -65,7 +65,7 @@ class ProfileController extends Controller $this->middleware( static function ($request, $next) { - app('view')->share('title', (string)trans('firefly.profile')); + app('view')->share('title', (string) trans('firefly.profile')); app('view')->share('mainTitleIcon', 'fa-user'); return $next($request); @@ -107,7 +107,7 @@ class ProfileController extends Controller $repository->unblockUser($user); // return to log in. - session()->flash('success', (string)trans('firefly.login_with_new_email')); + session()->flash('success', (string) trans('firefly.login_with_new_email')); return redirect(route('login')); } @@ -123,7 +123,7 @@ class ProfileController extends Controller return redirect(route('profile.index')); } $title = auth()->user()->email; - $subTitle = (string)trans('firefly.delete_account'); + $subTitle = (string) trans('firefly.delete_account'); $subTitleIcon = 'fa-trash'; return view('profile.delete-account', compact('title', 'subTitle', 'subTitleIcon')); @@ -171,7 +171,7 @@ class ProfileController extends Controller public function logoutOtherSessions(): Factory|RedirectResponse|View { if (!$this->internalAuth) { - session()->flash('info', (string)trans('firefly.external_auth_disabled')); + session()->flash('info', (string) trans('firefly.external_auth_disabled')); return redirect(route('profile.index')); } @@ -195,7 +195,7 @@ class ProfileController extends Controller $newEmail = $request->convertString('email'); $oldEmail = $user->email; if ($newEmail === $user->email) { - session()->flash('error', (string)trans('firefly.email_not_changed')); + session()->flash('error', (string) trans('firefly.email_not_changed')); return redirect(route('profile.change-email'))->withInput(); } @@ -205,7 +205,7 @@ class ProfileController extends Controller \Auth::guard()->logout(); // @phpstan-ignore-line (does not recognize function) $request->session()->invalidate(); - session()->flash('success', (string)trans('firefly.email_changed')); + session()->flash('success', (string) trans('firefly.email_changed')); return redirect(route('index')); } @@ -218,7 +218,7 @@ class ProfileController extends Controller // force user logout. \Auth::guard()->logout(); // @phpstan-ignore-line (does not recognize function) $request->session()->invalidate(); - session()->flash('success', (string)trans('firefly.email_changed')); + session()->flash('success', (string) trans('firefly.email_changed')); return redirect(route('index')); } @@ -236,7 +236,7 @@ class ProfileController extends Controller $title = auth()->user()->email; $email = auth()->user()->email; - $subTitle = (string)trans('firefly.change_your_email'); + $subTitle = (string) trans('firefly.change_your_email'); $subTitleIcon = 'fa-envelope'; return view('profile.change-email', compact('title', 'subTitle', 'subTitleIcon', 'email')); @@ -271,7 +271,7 @@ class ProfileController extends Controller } $repository->changePassword($user, $request->get('new_password')); - session()->flash('success', (string)trans('firefly.password_changed')); + session()->flash('success', (string) trans('firefly.password_changed')); return redirect(route('profile.index')); } @@ -290,7 +290,7 @@ class ProfileController extends Controller } $title = auth()->user()->email; - $subTitle = (string)trans('firefly.change_your_password'); + $subTitle = (string) trans('firefly.change_your_password'); $subTitleIcon = 'fa-key'; return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon')); @@ -310,7 +310,7 @@ class ProfileController extends Controller } if (!\Hash::check($request->get('password'), auth()->user()->password)) { - session()->flash('error', (string)trans('firefly.invalid_password')); + session()->flash('error', (string) trans('firefly.invalid_password')); return redirect(route('profile.delete-account')); } @@ -334,7 +334,7 @@ class ProfileController extends Controller public function postLogoutOtherSessions(Request $request) { if (!$this->internalAuth) { - session()->flash('info', (string)trans('firefly.external_auth_disabled')); + session()->flash('info', (string) trans('firefly.external_auth_disabled')); return redirect(route('profile.index')); } @@ -344,11 +344,11 @@ class ProfileController extends Controller ]; if (\Auth::once($creds)) { \Auth::logoutOtherDevices($request->get('password')); - session()->flash('info', (string)trans('firefly.other_sessions_logged_out')); + session()->flash('info', (string) trans('firefly.other_sessions_logged_out')); return redirect(route('profile.index')); } - session()->flash('error', (string)trans('auth.failed')); + session()->flash('error', (string) trans('auth.failed')); return redirect(route('profile.index')); } @@ -372,7 +372,7 @@ class ProfileController extends Controller $user = auth()->user(); $token = $user->generateAccessToken(); app('preferences')->set('access_token', $token); - session()->flash('success', (string)trans('firefly.token_regenerated')); + session()->flash('success', (string) trans('firefly.token_regenerated')); return redirect(route('profile.index')); } @@ -410,7 +410,7 @@ class ProfileController extends Controller /** @var string $match */ $match = null; foreach ($set as $entry) { - $hashed = hash('sha256', sprintf('%s%s', (string)config('app.key'), $entry->data)); + $hashed = hash('sha256', sprintf('%s%s', (string) config('app.key'), $entry->data)); if ($hashed === $hash) { $match = $entry->data; @@ -426,7 +426,7 @@ class ProfileController extends Controller $repository->unblockUser($user); // return to login page. - session()->flash('success', (string)trans('firefly.login_with_old_email')); + session()->flash('success', (string) trans('firefly.login_with_old_email')); return redirect(route('login')); } diff --git a/app/Http/Controllers/Recurring/CreateController.php b/app/Http/Controllers/Recurring/CreateController.php index 1faed7d0c5..2a79a90092 100644 --- a/app/Http/Controllers/Recurring/CreateController.php +++ b/app/Http/Controllers/Recurring/CreateController.php @@ -62,8 +62,8 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-paint-brush'); - app('view')->share('title', (string)trans('firefly.recurrences')); - app('view')->share('subTitle', (string)trans('firefly.create_new_recurrence')); + app('view')->share('title', (string) trans('firefly.recurrences')); + app('view')->share('subTitle', (string) trans('firefly.create_new_recurrence')); $this->recurring = app(RecurringRepositoryInterface::class); $this->budgetRepos = app(BudgetRepositoryInterface::class); @@ -84,7 +84,7 @@ class CreateController extends Controller { $budgets = app('expandedform')->makeSelectListWithEmpty($this->budgetRepos->getActiveBudgets()); $bills = app('expandedform')->makeSelectListWithEmpty($this->billRepository->getActiveBills()); - $defaultCurrency = app('amount')->getDefaultCurrency(); + $defaultCurrency = $this->defaultCurrency; $tomorrow = today(config('app.timezone')); $oldRepetitionType = $request->old('repetition_type'); $tomorrow->addDay(); @@ -95,22 +95,22 @@ class CreateController extends Controller } $request->session()->forget('recurring.create.fromStore'); $repetitionEnds = [ - 'forever' => (string)trans('firefly.repeat_forever'), - 'until_date' => (string)trans('firefly.repeat_until_date'), - 'times' => (string)trans('firefly.repeat_times'), + 'forever' => (string) trans('firefly.repeat_forever'), + 'until_date' => (string) trans('firefly.repeat_until_date'), + 'times' => (string) trans('firefly.repeat_times'), ]; $weekendResponses = [ - RecurrenceRepetition::WEEKEND_DO_NOTHING => (string)trans('firefly.do_nothing'), - RecurrenceRepetition::WEEKEND_SKIP_CREATION => (string)trans('firefly.skip_transaction'), - RecurrenceRepetition::WEEKEND_TO_FRIDAY => (string)trans('firefly.jump_to_friday'), - RecurrenceRepetition::WEEKEND_TO_MONDAY => (string)trans('firefly.jump_to_monday'), + RecurrenceRepetition::WEEKEND_DO_NOTHING => (string) trans('firefly.do_nothing'), + RecurrenceRepetition::WEEKEND_SKIP_CREATION => (string) trans('firefly.skip_transaction'), + RecurrenceRepetition::WEEKEND_TO_FRIDAY => (string) trans('firefly.jump_to_friday'), + RecurrenceRepetition::WEEKEND_TO_MONDAY => (string) trans('firefly.jump_to_monday'), ]; $hasOldInput = null !== $request->old('_token'); // flash some data $preFilled = [ 'first_date' => $tomorrow->format('Y-m-d'), 'transaction_type' => $hasOldInput ? $request->old('transaction_type') : 'withdrawal', - 'active' => $hasOldInput ? (bool)$request->old('active') : true, - 'apply_rules' => $hasOldInput ? (bool)$request->old('apply_rules') : true, + 'active' => $hasOldInput ? (bool) $request->old('active') : true, + 'apply_rules' => $hasOldInput ? (bool) $request->old('apply_rules') : true, ]; $request->session()->flash('preFilled', $preFilled); @@ -129,7 +129,7 @@ class CreateController extends Controller { $budgets = app('expandedform')->makeSelectListWithEmpty($this->budgetRepos->getActiveBudgets()); $bills = app('expandedform')->makeSelectListWithEmpty($this->billRepository->getActiveBills()); - $defaultCurrency = app('amount')->getDefaultCurrency(); + $defaultCurrency = $this->defaultCurrency; $tomorrow = today(config('app.timezone')); $oldRepetitionType = $request->old('repetition_type'); $tomorrow->addDay(); @@ -140,15 +140,15 @@ class CreateController extends Controller } $request->session()->forget('recurring.create.fromStore'); $repetitionEnds = [ - 'forever' => (string)trans('firefly.repeat_forever'), - 'until_date' => (string)trans('firefly.repeat_until_date'), - 'times' => (string)trans('firefly.repeat_times'), + 'forever' => (string) trans('firefly.repeat_forever'), + 'until_date' => (string) trans('firefly.repeat_until_date'), + 'times' => (string) trans('firefly.repeat_times'), ]; $weekendResponses = [ - RecurrenceRepetition::WEEKEND_DO_NOTHING => (string)trans('firefly.do_nothing'), - RecurrenceRepetition::WEEKEND_SKIP_CREATION => (string)trans('firefly.skip_transaction'), - RecurrenceRepetition::WEEKEND_TO_FRIDAY => (string)trans('firefly.jump_to_friday'), - RecurrenceRepetition::WEEKEND_TO_MONDAY => (string)trans('firefly.jump_to_monday'), + RecurrenceRepetition::WEEKEND_DO_NOTHING => (string) trans('firefly.do_nothing'), + RecurrenceRepetition::WEEKEND_SKIP_CREATION => (string) trans('firefly.skip_transaction'), + RecurrenceRepetition::WEEKEND_TO_FRIDAY => (string) trans('firefly.jump_to_friday'), + RecurrenceRepetition::WEEKEND_TO_MONDAY => (string) trans('firefly.jump_to_monday'), ]; // fill prefilled with journal info @@ -181,8 +181,8 @@ class CreateController extends Controller 'category' => $request->old('category'), 'budget_id' => $request->old('budget_id'), 'bill_id' => $request->old('bill_id'), - 'active' => (bool)$request->old('active'), - 'apply_rules' => (bool)$request->old('apply_rules'), + 'active' => (bool) $request->old('active'), + 'apply_rules' => (bool) $request->old('apply_rules'), ]; } if (false === $hasOldInput) { @@ -234,7 +234,7 @@ class CreateController extends Controller } Log::channel('audit')->info('Stored new recurrence.', $data); - $request->session()->flash('success', (string)trans('firefly.stored_new_recurrence', ['title' => $recurrence->title])); + $request->session()->flash('success', (string) trans('firefly.stored_new_recurrence', ['title' => $recurrence->title])); app('preferences')->mark(); // store attachment(s): @@ -245,7 +245,7 @@ class CreateController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -253,7 +253,7 @@ class CreateController extends Controller } $redirect = redirect($this->getPreviousUrl('recurring.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { // set value so create routine will not overwrite URL: $request->session()->put('recurring.create.fromStore', true); diff --git a/app/Http/Controllers/Recurring/DeleteController.php b/app/Http/Controllers/Recurring/DeleteController.php index e70a261e6e..4cdc4c971f 100644 --- a/app/Http/Controllers/Recurring/DeleteController.php +++ b/app/Http/Controllers/Recurring/DeleteController.php @@ -52,7 +52,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-paint-brush'); - app('view')->share('title', (string)trans('firefly.recurrences')); + app('view')->share('title', (string) trans('firefly.recurrences')); $this->recurring = app(RecurringRepositoryInterface::class); @@ -68,7 +68,7 @@ class DeleteController extends Controller */ public function delete(Recurrence $recurrence) { - $subTitle = (string)trans('firefly.delete_recurring', ['title' => $recurrence->title]); + $subTitle = (string) trans('firefly.delete_recurring', ['title' => $recurrence->title]); // put previous url in session $this->rememberPreviousUrl('recurrences.delete.url'); @@ -85,7 +85,7 @@ class DeleteController extends Controller public function destroy(RecurringRepositoryInterface $repository, Request $request, Recurrence $recurrence) { $repository->destroy($recurrence); - $request->session()->flash('success', (string)trans('firefly.recurrence_deleted', ['title' => $recurrence->title])); + $request->session()->flash('success', (string) trans('firefly.recurrence_deleted', ['title' => $recurrence->title])); app('preferences')->mark(); return redirect($this->getPreviousUrl('recurrences.delete.url')); diff --git a/app/Http/Controllers/Recurring/EditController.php b/app/Http/Controllers/Recurring/EditController.php index 2a3a37342e..c662ed3d04 100644 --- a/app/Http/Controllers/Recurring/EditController.php +++ b/app/Http/Controllers/Recurring/EditController.php @@ -63,8 +63,8 @@ class EditController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-paint-brush'); - app('view')->share('title', (string)trans('firefly.recurrences')); - app('view')->share('subTitle', (string)trans('firefly.recurrences')); + app('view')->share('title', (string) trans('firefly.recurrences')); + app('view')->share('subTitle', (string) trans('firefly.recurrences')); $this->recurring = app(RecurringRepositoryInterface::class); $this->budgetRepos = app(BudgetRepositoryInterface::class); @@ -114,9 +114,9 @@ class EditController extends Controller $repetitionEnd = 'forever'; $repetitionEnds = [ - 'forever' => (string)trans('firefly.repeat_forever'), - 'until_date' => (string)trans('firefly.repeat_until_date'), - 'times' => (string)trans('firefly.repeat_times'), + 'forever' => (string) trans('firefly.repeat_forever'), + 'until_date' => (string) trans('firefly.repeat_until_date'), + 'times' => (string) trans('firefly.repeat_times'), ]; if (null !== $recurrence->repeat_until) { $repetitionEnd = 'until_date'; @@ -126,22 +126,22 @@ class EditController extends Controller } $weekendResponses = [ - RecurrenceRepetition::WEEKEND_DO_NOTHING => (string)trans('firefly.do_nothing'), - RecurrenceRepetition::WEEKEND_SKIP_CREATION => (string)trans('firefly.skip_transaction'), - RecurrenceRepetition::WEEKEND_TO_FRIDAY => (string)trans('firefly.jump_to_friday'), - RecurrenceRepetition::WEEKEND_TO_MONDAY => (string)trans('firefly.jump_to_monday'), + RecurrenceRepetition::WEEKEND_DO_NOTHING => (string) trans('firefly.do_nothing'), + RecurrenceRepetition::WEEKEND_SKIP_CREATION => (string) trans('firefly.skip_transaction'), + RecurrenceRepetition::WEEKEND_TO_FRIDAY => (string) trans('firefly.jump_to_friday'), + RecurrenceRepetition::WEEKEND_TO_MONDAY => (string) trans('firefly.jump_to_monday'), ]; $hasOldInput = null !== $request->old('_token'); $preFilled = [ 'transaction_type' => strtolower($recurrence->transactionType->type), - 'active' => $hasOldInput ? (bool)$request->old('active') : $recurrence->active, - 'apply_rules' => $hasOldInput ? (bool)$request->old('apply_rules') : $recurrence->apply_rules, + 'active' => $hasOldInput ? (bool) $request->old('active') : $recurrence->active, + 'apply_rules' => $hasOldInput ? (bool) $request->old('apply_rules') : $recurrence->apply_rules, 'deposit_source_id' => $array['transactions'][0]['source_id'], 'withdrawal_destination_id' => $array['transactions'][0]['destination_id'], ]; - $array['first_date'] = substr((string)$array['first_date'], 0, 10); - $array['repeat_until'] = substr((string)$array['repeat_until'], 0, 10); + $array['first_date'] = substr((string) $array['first_date'], 0, 10); + $array['repeat_until'] = substr((string) $array['repeat_until'], 0, 10); $array['transactions'][0]['tags'] = implode(',', $array['transactions'][0]['tags'] ?? []); return view( @@ -172,7 +172,7 @@ class EditController extends Controller $data = $request->getAll(); $this->recurring->update($recurrence, $data); - $request->session()->flash('success', (string)trans('firefly.updated_recurrence', ['title' => $recurrence->title])); + $request->session()->flash('success', (string) trans('firefly.updated_recurrence', ['title' => $recurrence->title])); Log::channel('audit')->info(sprintf('Updated recurrence #%d.', $recurrence->id), $data); // store new attachment(s): @@ -183,7 +183,7 @@ class EditController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachments->getMessages()->get('attachments')) > 0) { @@ -191,7 +191,7 @@ class EditController extends Controller } app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('recurrences.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { // set value so edit routine will not overwrite URL: $request->session()->put('recurrences.edit.fromUpdate', true); diff --git a/app/Http/Controllers/Recurring/IndexController.php b/app/Http/Controllers/Recurring/IndexController.php index ed2e3775ba..982e35e878 100644 --- a/app/Http/Controllers/Recurring/IndexController.php +++ b/app/Http/Controllers/Recurring/IndexController.php @@ -56,7 +56,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-paint-brush'); - app('view')->share('title', (string)trans('firefly.recurrences')); + app('view')->share('title', (string) trans('firefly.recurrences')); $this->recurringRepos = app(RecurringRepositoryInterface::class); @@ -75,8 +75,8 @@ class IndexController extends Controller */ public function index(Request $request) { - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $collection = $this->recurringRepos->get(); $today = today(config('app.timezone')); $year = today(config('app.timezone')); diff --git a/app/Http/Controllers/Recurring/ShowController.php b/app/Http/Controllers/Recurring/ShowController.php index bb4c3d82ed..ef4beccd79 100644 --- a/app/Http/Controllers/Recurring/ShowController.php +++ b/app/Http/Controllers/Recurring/ShowController.php @@ -60,7 +60,7 @@ class ShowController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-paint-brush'); - app('view')->share('title', (string)trans('firefly.recurrences')); + app('view')->share('title', (string) trans('firefly.recurrences')); $this->recurring = app(RecurringRepositoryInterface::class); @@ -125,7 +125,7 @@ class ShowController extends Controller } } - $subTitle = (string)trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]); + $subTitle = (string) trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]); return view('recurring.show', compact('recurrence', 'subTitle', 'array', 'groups', 'today')); } diff --git a/app/Http/Controllers/Recurring/TriggerController.php b/app/Http/Controllers/Recurring/TriggerController.php index e1a4c3be79..818f353037 100644 --- a/app/Http/Controllers/Recurring/TriggerController.php +++ b/app/Http/Controllers/Recurring/TriggerController.php @@ -74,11 +74,11 @@ class TriggerController extends Controller app('preferences')->mark(); if (0 === $groups->count()) { - $request->session()->flash('info', (string)trans('firefly.no_new_transaction_in_recurrence')); + $request->session()->flash('info', (string) trans('firefly.no_new_transaction_in_recurrence')); } if (1 === $groups->count()) { $first = $groups->first(); - $request->session()->flash('success', (string)trans('firefly.stored_journal_no_descr')); + $request->session()->flash('success', (string) trans('firefly.stored_journal_no_descr')); $request->session()->flash('success_url', route('transactions.show', [$first->id])); } diff --git a/app/Http/Controllers/Report/BudgetController.php b/app/Http/Controllers/Report/BudgetController.php index 9e95e2ba43..286978f049 100644 --- a/app/Http/Controllers/Report/BudgetController.php +++ b/app/Http/Controllers/Report/BudgetController.php @@ -164,8 +164,8 @@ class BudgetController extends Controller ]; ++$result[$key]['transactions']; $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); - $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); - $result[$key]['avg_float'] = (float)$result[$key]['avg']; // intentional float + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string) $result[$key]['transactions']); + $result[$key]['avg_float'] = (float) $result[$key]['avg']; // intentional float } } } @@ -242,7 +242,7 @@ class BudgetController extends Controller $total = $sums[$currencyId]['sum'] ?? '0'; $pct = '0'; if (0 !== bccomp($sum, '0') && 0 !== bccomp($total, '9')) { - $pct = round((float)bcmul(bcdiv($sum, $total), '100')); // intentional float + $pct = round((float) bcmul(bcdiv($sum, $total), '100')); // intentional float } $report[$budgetId]['currencies'][$currencyId]['sum_pct'] = $pct; } @@ -319,7 +319,7 @@ class BudgetController extends Controller $report[$key]['entries'][$dateKey] ??= '0'; $report[$key]['entries'][$dateKey] = bcadd($journal['amount'], $report[$key]['entries'][$dateKey]); $report[$key]['sum'] = bcadd($report[$key]['sum'], $journal['amount']); - $report[$key]['avg'] = bcdiv($report[$key]['sum'], (string)count($periods)); + $report[$key]['avg'] = bcdiv($report[$key]['sum'], (string) count($periods)); } } } @@ -354,7 +354,7 @@ class BudgetController extends Controller $result[] = [ 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], - 'amount_float' => (float)$journal['amount'], // intentional float + 'amount_float' => (float) $journal['amount'], // intentional float 'amount' => $journal['amount'], 'date' => $journal['date']->isoFormat($this->monthAndDayFormat), 'date_sort' => $journal['date']->format('Y-m-d'), diff --git a/app/Http/Controllers/Report/CategoryController.php b/app/Http/Controllers/Report/CategoryController.php index c3560c2219..907525f85f 100644 --- a/app/Http/Controllers/Report/CategoryController.php +++ b/app/Http/Controllers/Report/CategoryController.php @@ -283,8 +283,8 @@ class CategoryController extends Controller ]; ++$result[$key]['transactions']; $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); - $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); - $result[$key]['avg_float'] = (float)$result[$key]['avg']; // intentional float + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string) $result[$key]['transactions']); + $result[$key]['avg_float'] = (float) $result[$key]['avg']; // intentional float } } } @@ -333,8 +333,8 @@ class CategoryController extends Controller ]; ++$result[$key]['transactions']; $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); - $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); - $result[$key]['avg_float'] = (float)$result[$key]['avg']; + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string) $result[$key]['transactions']); + $result[$key]['avg_float'] = (float) $result[$key]['avg']; } } } @@ -664,7 +664,7 @@ class CategoryController extends Controller $result[] = [ 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], - 'amount_float' => (float)$journal['amount'], + 'amount_float' => (float) $journal['amount'], 'amount' => $journal['amount'], 'date' => $journal['date']->isoFormat($this->monthAndDayFormat), 'date_sort' => $journal['date']->format('Y-m-d'), @@ -712,7 +712,7 @@ class CategoryController extends Controller $result[] = [ 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], - 'amount_float' => (float)$journal['amount'], + 'amount_float' => (float) $journal['amount'], 'amount' => $journal['amount'], 'date' => $journal['date']->isoFormat($this->monthAndDayFormat), 'date_sort' => $journal['date']->format('Y-m-d'), diff --git a/app/Http/Controllers/Report/DoubleController.php b/app/Http/Controllers/Report/DoubleController.php index 9477589868..75840fcef4 100644 --- a/app/Http/Controllers/Report/DoubleController.php +++ b/app/Http/Controllers/Report/DoubleController.php @@ -91,8 +91,8 @@ class DoubleController extends Controller ]; ++$result[$key]['transactions']; $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); - $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); - $result[$key]['avg_float'] = (float)$result[$key]['avg']; + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string) $result[$key]['transactions']); + $result[$key]['avg_float'] = (float) $result[$key]['avg']; } } // sort by amount_float @@ -141,8 +141,8 @@ class DoubleController extends Controller ]; ++$result[$key]['transactions']; $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); - $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); - $result[$key]['avg_float'] = (float)$result[$key]['avg']; + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string) $result[$key]['transactions']); + $result[$key]['avg_float'] = (float) $result[$key]['avg']; } } // sort by amount_float @@ -407,7 +407,7 @@ class DoubleController extends Controller $result[] = [ 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], - 'amount_float' => (float)$journal['amount'], + 'amount_float' => (float) $journal['amount'], 'amount' => $journal['amount'], 'date' => $journal['date']->isoFormat($this->monthAndDayFormat), 'date_sort' => $journal['date']->format('Y-m-d'), @@ -455,7 +455,7 @@ class DoubleController extends Controller $result[] = [ 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], - 'amount_float' => (float)$journal['amount'], + 'amount_float' => (float) $journal['amount'], 'amount' => $journal['amount'], 'date' => $journal['date']->isoFormat($this->monthAndDayFormat), 'date_sort' => $journal['date']->format('Y-m-d'), diff --git a/app/Http/Controllers/Report/TagController.php b/app/Http/Controllers/Report/TagController.php index 32ff779948..fdae8faec2 100644 --- a/app/Http/Controllers/Report/TagController.php +++ b/app/Http/Controllers/Report/TagController.php @@ -280,8 +280,8 @@ class TagController extends Controller ]; ++$result[$key]['transactions']; $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); - $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); - $result[$key]['avg_float'] = (float)$result[$key]['avg']; + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string) $result[$key]['transactions']); + $result[$key]['avg_float'] = (float) $result[$key]['avg']; } } } @@ -330,8 +330,8 @@ class TagController extends Controller ]; ++$result[$key]['transactions']; $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); - $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); - $result[$key]['avg_float'] = (float)$result[$key]['avg']; + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string) $result[$key]['transactions']); + $result[$key]['avg_float'] = (float) $result[$key]['avg']; } } } @@ -467,7 +467,7 @@ class TagController extends Controller $result[] = [ 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], - 'amount_float' => (float)$journal['amount'], + 'amount_float' => (float) $journal['amount'], 'amount' => $journal['amount'], 'date' => $journal['date']->isoFormat($this->monthAndDayFormat), 'date_sort' => $journal['date']->format('Y-m-d'), @@ -515,7 +515,7 @@ class TagController extends Controller $result[] = [ 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], - 'amount_float' => (float)$journal['amount'], // intentional float. + 'amount_float' => (float) $journal['amount'], // intentional float. 'amount' => $journal['amount'], 'date' => $journal['date']->isoFormat($this->monthAndDayFormat), 'date_sort' => $journal['date']->format('Y-m-d'), diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 67d8344fee..eabef2d1c4 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -59,7 +59,7 @@ class ReportController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.reports')); + app('view')->share('title', (string) trans('firefly.reports')); app('view')->share('mainTitleIcon', 'fa-bar-chart'); app('view')->share('subTitleIcon', 'fa-calendar'); $this->helper = app(ReportHelperInterface::class); @@ -80,7 +80,7 @@ class ReportController extends Controller public function auditReport(Collection $accounts, Carbon $start, Carbon $end) { if ($end < $start) { - return view('error')->with('message', (string)trans('firefly.end_after_start_date')); + return view('error')->with('message', (string) trans('firefly.end_after_start_date')); } $this->repository->cleanupBudgets(); @@ -111,7 +111,7 @@ class ReportController extends Controller public function budgetReport(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end) { if ($end < $start) { - return view('error')->with('message', (string)trans('firefly.end_after_start_date')); + return view('error')->with('message', (string) trans('firefly.end_after_start_date')); } $this->repository->cleanupBudgets(); @@ -143,7 +143,7 @@ class ReportController extends Controller public function categoryReport(Collection $accounts, Collection $categories, Carbon $start, Carbon $end) { if ($end < $start) { - return view('error')->with('message', (string)trans('firefly.end_after_start_date')); + return view('error')->with('message', (string) trans('firefly.end_after_start_date')); } $this->repository->cleanupBudgets(); @@ -175,7 +175,7 @@ class ReportController extends Controller public function defaultReport(Collection $accounts, Carbon $start, Carbon $end) { if ($end < $start) { - return view('error')->with('message', (string)trans('firefly.end_after_start_date')); + return view('error')->with('message', (string) trans('firefly.end_after_start_date')); } $this->repository->cleanupBudgets(); @@ -250,8 +250,8 @@ class ReportController extends Controller /** @var Account $account */ foreach ($accounts as $account) { - $type = $account->accountType->type; - $role = sprintf('opt_group_%s', $repository->getMetaValue($account, 'account_role')); + $type = $account->accountType->type; + $role = sprintf('opt_group_%s', $repository->getMetaValue($account, 'account_role')); if (in_array($type, [AccountType::MORTGAGE, AccountType::DEBT, AccountType::LOAN], true)) { $role = sprintf('opt_group_l_%s', $type); @@ -260,7 +260,7 @@ class ReportController extends Controller if ('opt_group_' === $role) { $role = 'opt_group_defaultAsset'; } - $groupedAccounts[(string)trans(sprintf('firefly.%s', $role))][$account->id] = $account; + $groupedAccounts[(string) trans(sprintf('firefly.%s', $role))][$account->id] = $account; } ksort($groupedAccounts); @@ -309,37 +309,37 @@ class ReportController extends Controller if (0 === $request->getAccountList()->count()) { app('log')->debug('Account count is zero'); - session()->flash('error', (string)trans('firefly.select_at_least_one_account')); + session()->flash('error', (string) trans('firefly.select_at_least_one_account')); return redirect(route('reports.index')); } if ('category' === $reportType && 0 === $request->getCategoryList()->count()) { - session()->flash('error', (string)trans('firefly.select_at_least_one_category')); + session()->flash('error', (string) trans('firefly.select_at_least_one_category')); return redirect(route('reports.index')); } if ('budget' === $reportType && 0 === $request->getBudgetList()->count()) { - session()->flash('error', (string)trans('firefly.select_at_least_one_budget')); + session()->flash('error', (string) trans('firefly.select_at_least_one_budget')); return redirect(route('reports.index')); } if ('tag' === $reportType && 0 === $request->getTagList()->count()) { - session()->flash('error', (string)trans('firefly.select_at_least_one_tag')); + session()->flash('error', (string) trans('firefly.select_at_least_one_tag')); return redirect(route('reports.index')); } if ('double' === $reportType && 0 === $request->getDoubleList()->count()) { - session()->flash('error', (string)trans('firefly.select_at_least_one_expense')); + session()->flash('error', (string) trans('firefly.select_at_least_one_expense')); return redirect(route('reports.index')); } if ($request->getEndDate() < $request->getStartDate()) { - return view('error')->with('message', (string)trans('firefly.end_after_start_date')); + return view('error')->with('message', (string) trans('firefly.end_after_start_date')); } $url = match ($reportType) { @@ -364,7 +364,7 @@ class ReportController extends Controller public function tagReport(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) { if ($end < $start) { - return view('error')->with('message', (string)trans('firefly.end_after_start_date')); + return view('error')->with('message', (string) trans('firefly.end_after_start_date')); } $this->repository->cleanupBudgets(); diff --git a/app/Http/Controllers/Rule/CreateController.php b/app/Http/Controllers/Rule/CreateController.php index e9468d2ca9..9729dfe015 100644 --- a/app/Http/Controllers/Rule/CreateController.php +++ b/app/Http/Controllers/Rule/CreateController.php @@ -60,7 +60,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->ruleRepos = app(RuleRepositoryInterface::class); @@ -87,7 +87,7 @@ class CreateController extends Controller $oldActions = []; // build triggers from query, if present. - $query = (string)$request->get('from_query'); + $query = (string) $request->get('from_query'); if ('' !== $query) { $search = app(SearchInterface::class); $search->parseQuery($query); @@ -115,9 +115,9 @@ class CreateController extends Controller $subTitleIcon = 'fa-clone'; // title depends on whether or not there is a rule group: - $subTitle = (string)trans('firefly.make_new_rule_no_group'); + $subTitle = (string) trans('firefly.make_new_rule_no_group'); if (null !== $ruleGroup) { - $subTitle = (string)trans('firefly.make_new_rule', ['title' => $ruleGroup->title]); + $subTitle = (string) trans('firefly.make_new_rule', ['title' => $ruleGroup->title]); } // flash old data @@ -144,13 +144,13 @@ class CreateController extends Controller */ public function createFromBill(Request $request, Bill $bill) { - $request->session()->flash('info', (string)trans('firefly.instructions_rule_from_bill', ['name' => e($bill->name)])); + $request->session()->flash('info', (string) trans('firefly.instructions_rule_from_bill', ['name' => e($bill->name)])); $this->createDefaultRuleGroup(); $preFilled = [ 'strict' => true, - 'title' => (string)trans('firefly.new_rule_for_bill_title', ['name' => $bill->name]), - 'description' => (string)trans('firefly.new_rule_for_bill_description', ['name' => $bill->name]), + 'title' => (string) trans('firefly.new_rule_for_bill_title', ['name' => $bill->name]), + 'description' => (string) trans('firefly.new_rule_for_bill_description', ['name' => $bill->name]), ]; // make triggers and actions from the bill itself. @@ -170,7 +170,7 @@ class CreateController extends Controller $subTitleIcon = 'fa-clone'; // title depends on whether there is a rule group: - $subTitle = (string)trans('firefly.make_new_rule_no_group'); + $subTitle = (string) trans('firefly.make_new_rule_no_group'); // flash old data $request->session()->flash('preFilled', $preFilled); @@ -194,10 +194,10 @@ class CreateController extends Controller */ public function createFromJournal(Request $request, TransactionJournal $journal) { - $request->session()->flash('info', (string)trans('firefly.instructions_rule_from_journal', ['name' => e($journal->description)])); + $request->session()->flash('info', (string) trans('firefly.instructions_rule_from_journal', ['name' => e($journal->description)])); $subTitleIcon = 'fa-clone'; - $subTitle = (string)trans('firefly.make_new_rule_no_group'); + $subTitle = (string) trans('firefly.make_new_rule_no_group'); // get triggers and actions for journal. $oldTriggers = $this->getTriggersForJournal($journal); @@ -208,8 +208,8 @@ class CreateController extends Controller // collect pre-filled information: $preFilled = [ 'strict' => true, - 'title' => (string)trans('firefly.new_rule_for_journal_title', ['description' => $journal->description]), - 'description' => (string)trans('firefly.new_rule_for_journal_description', ['description' => $journal->description]), + 'title' => (string) trans('firefly.new_rule_for_journal_title', ['description' => $journal->description]), + 'description' => (string) trans('firefly.new_rule_for_journal_description', ['description' => $journal->description]), ]; // restore actions and triggers from old input: @@ -238,7 +238,7 @@ class CreateController extends Controller public function duplicate(Request $request): JsonResponse { - $ruleId = (int)$request->get('id'); + $ruleId = (int) $request->get('id'); $rule = $this->ruleRepos->find($ruleId); if (null !== $rule) { $this->ruleRepos->duplicate($rule); @@ -257,22 +257,22 @@ class CreateController extends Controller $data = $request->getRuleData(); $rule = $this->ruleRepos->store($data); - session()->flash('success', (string)trans('firefly.stored_new_rule', ['title' => $rule->title])); + session()->flash('success', (string) trans('firefly.stored_new_rule', ['title' => $rule->title])); app('preferences')->mark(); // redirect to show bill. - if ('true' === $request->get('return_to_bill') && (int)$request->get('bill_id') > 0) { - return redirect(route('bills.show', [(int)$request->get('bill_id')])); + if ('true' === $request->get('return_to_bill') && (int) $request->get('bill_id') > 0) { + return redirect(route('bills.show', [(int) $request->get('bill_id')])); } // redirect to new bill creation. - if ((int)$request->get('bill_id') > 0) { + if ((int) $request->get('bill_id') > 0) { return redirect($this->getPreviousUrl('bills.create.url')); } $redirect = redirect($this->getPreviousUrl('rules.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { session()->put('rules.create.fromStore', true); $redirect = redirect(route('rules.create', [$data['rule_group_id']]))->withInput(); } diff --git a/app/Http/Controllers/Rule/DeleteController.php b/app/Http/Controllers/Rule/DeleteController.php index 9c4fd816b3..d4c6ceb05d 100644 --- a/app/Http/Controllers/Rule/DeleteController.php +++ b/app/Http/Controllers/Rule/DeleteController.php @@ -48,7 +48,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->ruleRepos = app(RuleRepositoryInterface::class); @@ -65,7 +65,7 @@ class DeleteController extends Controller */ public function delete(Rule $rule) { - $subTitle = (string)trans('firefly.delete_rule', ['title' => $rule->title]); + $subTitle = (string) trans('firefly.delete_rule', ['title' => $rule->title]); // put previous url in session $this->rememberPreviousUrl('rules.delete.url'); @@ -81,7 +81,7 @@ class DeleteController extends Controller $title = $rule->title; $this->ruleRepos->destroy($rule); - session()->flash('success', (string)trans('firefly.deleted_rule', ['title' => $title])); + session()->flash('success', (string) trans('firefly.deleted_rule', ['title' => $title])); app('preferences')->mark(); return redirect($this->getPreviousUrl('rules.delete.url')); diff --git a/app/Http/Controllers/Rule/EditController.php b/app/Http/Controllers/Rule/EditController.php index daa7b8bf15..a00df8ef4c 100644 --- a/app/Http/Controllers/Rule/EditController.php +++ b/app/Http/Controllers/Rule/EditController.php @@ -58,7 +58,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->ruleRepos = app(RuleRepositoryInterface::class); @@ -83,7 +83,7 @@ class EditController extends Controller $oldTriggers = []; // build triggers from query, if present. - $query = (string)$request->get('from_query'); + $query = (string) $request->get('from_query'); if ('' !== $query) { $search = app(SearchInterface::class); $search->parseQuery($query); @@ -113,14 +113,14 @@ class EditController extends Controller $hasOldInput = null !== $request->old('_token'); $preFilled = [ - 'active' => $hasOldInput ? (bool)$request->old('active') : $rule->active, - 'stop_processing' => $hasOldInput ? (bool)$request->old('stop_processing') : $rule->stop_processing, - 'strict' => $hasOldInput ? (bool)$request->old('strict') : $rule->strict, + 'active' => $hasOldInput ? (bool) $request->old('active') : $rule->active, + 'stop_processing' => $hasOldInput ? (bool) $request->old('stop_processing') : $rule->stop_processing, + 'strict' => $hasOldInput ? (bool) $request->old('strict') : $rule->strict, ]; // get rule trigger for update / store-journal: $primaryTrigger = $this->ruleRepos->getPrimaryTrigger($rule); - $subTitle = (string)trans('firefly.edit_rule', ['title' => $rule->title]); + $subTitle = (string) trans('firefly.edit_rule', ['title' => $rule->title]); // put previous url in session if not redirect from store (not "return_to_edit"). if (true !== session('rules.edit.fromUpdate')) { @@ -144,7 +144,7 @@ class EditController extends Controller $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { - $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); + $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key)); } } asort($triggers); @@ -186,10 +186,10 @@ class EditController extends Controller $this->ruleRepos->update($rule, $data); - session()->flash('success', (string)trans('firefly.updated_rule', ['title' => $rule->title])); + session()->flash('success', (string) trans('firefly.updated_rule', ['title' => $rule->title])); app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('rules.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { session()->put('rules.edit.fromUpdate', true); $redirect = redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]); diff --git a/app/Http/Controllers/Rule/IndexController.php b/app/Http/Controllers/Rule/IndexController.php index d86f902c36..5d53c38e9c 100644 --- a/app/Http/Controllers/Rule/IndexController.php +++ b/app/Http/Controllers/Rule/IndexController.php @@ -53,7 +53,7 @@ class IndexController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->ruleGroupRepos = app(RuleGroupRepositoryInterface::class); $this->ruleRepos = app(RuleRepositoryInterface::class); @@ -79,7 +79,7 @@ class IndexController extends Controller public function moveRule(Request $request, Rule $rule, RuleGroup $ruleGroup): JsonResponse { - $order = (int)$request->get('order'); + $order = (int) $request->get('order'); $this->ruleRepos->moveRule($rule, $ruleGroup, $order); return response()->json([]); diff --git a/app/Http/Controllers/Rule/SelectController.php b/app/Http/Controllers/Rule/SelectController.php index a362e955fe..d6dae9d5fa 100644 --- a/app/Http/Controllers/Rule/SelectController.php +++ b/app/Http/Controllers/Rule/SelectController.php @@ -56,7 +56,7 @@ class SelectController extends Controller $this->middleware( static function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); return $next($request); @@ -108,7 +108,7 @@ class SelectController extends Controller // does the user have shared accounts? $first = session('first', today(config('app.timezone'))->subYear())->format('Y-m-d'); $today = today(config('app.timezone'))->format('Y-m-d'); - $subTitle = (string)trans('firefly.apply_rule_selection', ['title' => $rule->title]); + $subTitle = (string) trans('firefly.apply_rule_selection', ['title' => $rule->title]); return view('rules.rule.select-transactions', compact('first', 'today', 'rule', 'subTitle')); } @@ -133,7 +133,7 @@ class SelectController extends Controller // warn if nothing. if (0 === count($textTriggers)) { - return response()->json(['html' => '', 'warning' => (string)trans('firefly.warning_no_valid_triggers')]); + return response()->json(['html' => '', 'warning' => (string) trans('firefly.warning_no_valid_triggers')]); } foreach ($textTriggers as $textTrigger) { @@ -166,7 +166,7 @@ class SelectController extends Controller // Warn the user if only a subset of transactions is returned $warning = ''; if (0 === count($collection)) { - $warning = (string)trans('firefly.warning_no_matching_transactions'); + $warning = (string) trans('firefly.warning_no_matching_transactions'); } // Return json response @@ -196,7 +196,7 @@ class SelectController extends Controller $triggers = $rule->ruleTriggers; if (0 === count($triggers)) { - return response()->json(['html' => '', 'warning' => (string)trans('firefly.warning_no_valid_triggers')]); + return response()->json(['html' => '', 'warning' => (string) trans('firefly.warning_no_valid_triggers')]); } // create new rule engine: $newRuleEngine = app(RuleEngineInterface::class); @@ -208,7 +208,7 @@ class SelectController extends Controller $warning = ''; if (0 === count($collection)) { - $warning = (string)trans('firefly.warning_no_matching_transactions'); + $warning = (string) trans('firefly.warning_no_matching_transactions'); } // Return json response diff --git a/app/Http/Controllers/RuleGroup/CreateController.php b/app/Http/Controllers/RuleGroup/CreateController.php index 383ece3c70..c269849308 100644 --- a/app/Http/Controllers/RuleGroup/CreateController.php +++ b/app/Http/Controllers/RuleGroup/CreateController.php @@ -49,7 +49,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->repository = app(RuleGroupRepositoryInterface::class); @@ -67,7 +67,7 @@ class CreateController extends Controller public function create() { $subTitleIcon = 'fa-clone'; - $subTitle = (string)trans('firefly.make_new_rule_group'); + $subTitle = (string) trans('firefly.make_new_rule_group'); // put previous url in session if not redirect from store (not "create another"). if (true !== session('rule-groups.create.fromStore')) { @@ -88,11 +88,11 @@ class CreateController extends Controller $data = $request->getRuleGroupData(); $ruleGroup = $this->repository->store($data); - session()->flash('success', (string)trans('firefly.created_new_rule_group', ['title' => $ruleGroup->title])); + session()->flash('success', (string) trans('firefly.created_new_rule_group', ['title' => $ruleGroup->title])); app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('rule-groups.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { session()->put('rule-groups.create.fromStore', true); $redirect = redirect(route('rule-groups.create'))->withInput(); diff --git a/app/Http/Controllers/RuleGroup/DeleteController.php b/app/Http/Controllers/RuleGroup/DeleteController.php index cfcd8d59d1..8d7bf2fea7 100644 --- a/app/Http/Controllers/RuleGroup/DeleteController.php +++ b/app/Http/Controllers/RuleGroup/DeleteController.php @@ -50,7 +50,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->repository = app(RuleGroupRepositoryInterface::class); @@ -67,7 +67,7 @@ class DeleteController extends Controller */ public function delete(RuleGroup $ruleGroup) { - $subTitle = (string)trans('firefly.delete_rule_group', ['title' => $ruleGroup->title]); + $subTitle = (string) trans('firefly.delete_rule_group', ['title' => $ruleGroup->title]); // put previous url in session $this->rememberPreviousUrl('rule-groups.delete.url'); @@ -85,10 +85,10 @@ class DeleteController extends Controller $title = $ruleGroup->title; /** @var RuleGroup $moveTo */ - $moveTo = $this->repository->find((int)$request->get('move_rules_before_delete')); + $moveTo = $this->repository->find((int) $request->get('move_rules_before_delete')); $this->repository->destroy($ruleGroup, $moveTo); - session()->flash('success', (string)trans('firefly.deleted_rule_group', ['title' => $title])); + session()->flash('success', (string) trans('firefly.deleted_rule_group', ['title' => $title])); app('preferences')->mark(); return redirect($this->getPreviousUrl('rule-groups.delete.url')); diff --git a/app/Http/Controllers/RuleGroup/EditController.php b/app/Http/Controllers/RuleGroup/EditController.php index fc0569fa9c..a1e6f47487 100644 --- a/app/Http/Controllers/RuleGroup/EditController.php +++ b/app/Http/Controllers/RuleGroup/EditController.php @@ -51,7 +51,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->repository = app(RuleGroupRepositoryInterface::class); @@ -68,11 +68,11 @@ class EditController extends Controller */ public function edit(Request $request, RuleGroup $ruleGroup) { - $subTitle = (string)trans('firefly.edit_rule_group', ['title' => $ruleGroup->title]); + $subTitle = (string) trans('firefly.edit_rule_group', ['title' => $ruleGroup->title]); $hasOldInput = null !== $request->old('_token'); $preFilled = [ - 'active' => $hasOldInput ? (bool)$request->old('active') : $ruleGroup->active, + 'active' => $hasOldInput ? (bool) $request->old('active') : $ruleGroup->active, ]; // put previous url in session if not redirect from store (not "return_to_edit"). if (true !== session('rule-groups.edit.fromUpdate')) { @@ -89,7 +89,7 @@ class EditController extends Controller */ public function moveGroup(Request $request): JsonResponse { - $groupId = (int)$request->get('id'); + $groupId = (int) $request->get('id'); $ruleGroup = $this->repository->find($groupId); if (null !== $ruleGroup) { $direction = $request->get('direction'); @@ -123,15 +123,15 @@ class EditController extends Controller $data = [ 'title' => $request->convertString('title'), 'description' => $request->stringWithNewlines('description'), - 'active' => 1 === (int)$request->input('active'), + 'active' => 1 === (int) $request->input('active'), ]; $this->repository->update($ruleGroup, $data); - session()->flash('success', (string)trans('firefly.updated_rule_group', ['title' => $ruleGroup->title])); + session()->flash('success', (string) trans('firefly.updated_rule_group', ['title' => $ruleGroup->title])); app('preferences')->mark(); $redirect = redirect($this->getPreviousUrl('rule-groups.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { session()->put('rule-groups.edit.fromUpdate', true); $redirect = redirect(route('rule-groups.edit', [$ruleGroup->id]))->withInput(['return_to_edit' => 1]); diff --git a/app/Http/Controllers/RuleGroup/ExecutionController.php b/app/Http/Controllers/RuleGroup/ExecutionController.php index f2048133e1..542d38d0a7 100644 --- a/app/Http/Controllers/RuleGroup/ExecutionController.php +++ b/app/Http/Controllers/RuleGroup/ExecutionController.php @@ -51,7 +51,7 @@ class ExecutionController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.rules')); + app('view')->share('title', (string) trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); $this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class); @@ -89,7 +89,7 @@ class ExecutionController extends Controller $newRuleEngine->fire(); // Tell the user that the job is queued - session()->flash('success', (string)trans('firefly.applied_rule_group_selection', ['title' => $ruleGroup->title])); + session()->flash('success', (string) trans('firefly.applied_rule_group_selection', ['title' => $ruleGroup->title])); return redirect()->route('rules.index'); } @@ -103,7 +103,7 @@ class ExecutionController extends Controller { $first = session('first')->format('Y-m-d'); $today = today(config('app.timezone'))->format('Y-m-d'); - $subTitle = (string)trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]); + $subTitle = (string) trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]); return view('rules.rule-group.select-transactions', compact('first', 'today', 'ruleGroup', 'subTitle')); } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index b68d9de8a7..6bf2204c3b 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -46,7 +46,7 @@ class SearchController extends Controller $this->middleware( static function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-search'); - app('view')->share('title', (string)trans('firefly.search')); + app('view')->share('title', (string) trans('firefly.search')); return $next($request); } @@ -65,9 +65,9 @@ class SearchController extends Controller if (is_array($request->get('search'))) { $fullQuery = ''; } - $fullQuery = (string)$fullQuery; - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $ruleId = (int)$request->get('rule'); + $fullQuery = (string) $fullQuery; + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); + $ruleId = (int) $request->get('rule'); $ruleChanged = false; // find rule, check if query is different, offer to update. @@ -86,7 +86,7 @@ class SearchController extends Controller $query = $searcher->getWordsAsString(); $operators = $searcher->getOperators(); $invalidOperators = $searcher->getInvalidOperators(); - $subTitle = (string)trans('breadcrumbs.search_result', ['query' => $fullQuery]); + $subTitle = (string) trans('breadcrumbs.search_result', ['query' => $fullQuery]); return view('search.index', compact('query', 'operators', 'page', 'rule', 'fullQuery', 'subTitle', 'ruleId', 'ruleChanged', 'invalidOperators')); } @@ -102,8 +102,8 @@ class SearchController extends Controller if (!is_scalar($entry)) { $entry = ''; } - $fullQuery = (string)$entry; - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); + $fullQuery = (string) $entry; + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); $searcher->parseQuery($fullQuery); diff --git a/app/Http/Controllers/System/InstallController.php b/app/Http/Controllers/System/InstallController.php index 6bf7858cc4..fef4db461e 100644 --- a/app/Http/Controllers/System/InstallController.php +++ b/app/Http/Controllers/System/InstallController.php @@ -62,8 +62,8 @@ class InstallController extends Controller 'migrate' => ['--seed' => true, '--force' => true], 'generate-keys' => [], // an exception :( 'firefly-iii:upgrade-database' => [], - 'firefly-iii:correct-database' => [], - 'firefly-iii:report-integrity' => [], + // 'firefly-iii:correct-database' => [], + // 'firefly-iii:report-integrity' => [], 'firefly-iii:set-latest-version' => ['--james-is-cool' => true], 'firefly-iii:verify-security-alerts' => [], ]; @@ -80,17 +80,17 @@ class InstallController extends Controller { app('view')->share('FF_VERSION', config('firefly.version')); // index will set FF3 version. - app('fireflyconfig')->set('ff3_version', (string)config('firefly.version')); + app('fireflyconfig')->set('ff3_version', (string) config('firefly.version')); // set new DB version. - app('fireflyconfig')->set('db_version', (int)config('firefly.db_version')); + app('fireflyconfig')->set('db_version', (int) config('firefly.db_version')); return view('install.index'); } public function runCommand(Request $request): JsonResponse { - $requestIndex = (int)$request->get('index'); + $requestIndex = (int) $request->get('index'); $response = [ 'hasNextCommand' => false, 'done' => true, @@ -171,7 +171,7 @@ class InstallController extends Controller return; } - file_put_contents($publicKey, (string)$key->getPublicKey()); + file_put_contents($publicKey, (string) $key->getPublicKey()); file_put_contents($privateKey, $key->toString('PKCS1')); } } diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index 832e0069b3..23c45810a0 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -57,7 +57,7 @@ class TagController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.tags')); + app('view')->share('title', (string) trans('firefly.tags')); app('view')->share('mainTitleIcon', 'fa-tag'); $this->attachmentsHelper = app(AttachmentHelperInterface::class); @@ -75,7 +75,7 @@ class TagController extends Controller */ public function create(Request $request) { - $subTitle = (string)trans('firefly.new_tag'); + $subTitle = (string) trans('firefly.new_tag'); $subTitleIcon = 'fa-tag'; // location info: @@ -105,7 +105,7 @@ class TagController extends Controller */ public function delete(Tag $tag) { - $subTitle = (string)trans('breadcrumbs.delete_tag', ['tag' => $tag->tag]); + $subTitle = (string) trans('breadcrumbs.delete_tag', ['tag' => $tag->tag]); // put previous url in session $this->rememberPreviousUrl('tags.delete.url'); @@ -120,7 +120,7 @@ class TagController extends Controller */ public function edit(Tag $tag) { - $subTitle = (string)trans('firefly.edit_tag', ['tag' => $tag->tag]); + $subTitle = (string) trans('firefly.edit_tag', ['tag' => $tag->tag]); $subTitleIcon = 'fa-tag'; $location = $this->repository->getLocation($tag); @@ -176,13 +176,13 @@ class TagController extends Controller { $tags = $request->get('tags'); if (null === $tags || !is_array($tags)) { - session()->flash('info', (string)trans('firefly.select_tags_to_delete')); + session()->flash('info', (string) trans('firefly.select_tags_to_delete')); return redirect(route('tags.index')); } $count = 0; foreach ($tags as $tagId) { - $tagId = (int)$tagId; + $tagId = (int) $tagId; $tag = $this->repository->find($tagId); if (null !== $tag) { $this->repository->destroy($tag); @@ -202,7 +202,7 @@ class TagController extends Controller $tagName = $tag->tag; $this->repository->destroy($tag); - session()->flash('success', (string)trans('firefly.deleted_tag', ['tag' => $tagName])); + session()->flash('success', (string) trans('firefly.deleted_tag', ['tag' => $tagName])); app('preferences')->mark(); return redirect($this->getPreviousUrl('tags.delete.url')); @@ -219,8 +219,8 @@ class TagController extends Controller { // default values: $subTitleIcon = 'fa-tag'; - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $start ??= session('start'); $end ??= session('end'); $location = $this->repository->getLocation($tag); @@ -262,10 +262,10 @@ class TagController extends Controller { // default values: $subTitleIcon = 'fa-tag'; - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $periods = []; - $subTitle = (string)trans('firefly.all_journals_for_tag', ['tag' => $tag->tag]); + $subTitle = (string) trans('firefly.all_journals_for_tag', ['tag' => $tag->tag]); $start = $this->repository->firstUseDate($tag) ?? today(config('app.timezone')); $end = $this->repository->lastUseDate($tag) ?? today(config('app.timezone')); $attachments = $this->repository->getAttachments($tag); @@ -295,7 +295,7 @@ class TagController extends Controller $result = $this->repository->store($data); app('log')->debug('Data after storage', $result->toArray()); - session()->flash('success', (string)trans('firefly.created_tag', ['tag' => $data['tag']])); + session()->flash('success', (string) trans('firefly.created_tag', ['tag' => $data['tag']])); app('preferences')->mark(); // store attachment(s): @@ -306,7 +306,7 @@ class TagController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachmentsHelper->getMessages()->get('attachments')) > 0) { @@ -316,7 +316,7 @@ class TagController extends Controller $request->session()->flash('error', $this->attachmentsHelper->getErrors()->get('attachments')); } $redirect = redirect($this->getPreviousUrl('tags.create.url')); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { session()->put('tags.create.fromStore', true); $redirect = redirect(route('tags.create'))->withInput(); @@ -333,7 +333,7 @@ class TagController extends Controller $data = $request->collectTagData(); $tag = $this->repository->update($tag, $data); - session()->flash('success', (string)trans('firefly.updated_tag', ['tag' => $data['tag']])); + session()->flash('success', (string) trans('firefly.updated_tag', ['tag' => $data['tag']])); app('preferences')->mark(); // store new attachment(s): @@ -344,7 +344,7 @@ class TagController extends Controller } if (null !== $files && auth()->user()->hasRole('demo')) { Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__)); - session()->flash('info', (string)trans('firefly.no_att_demo_user')); + session()->flash('info', (string) trans('firefly.no_att_demo_user')); } if (count($this->attachmentsHelper->getMessages()->get('attachments')) > 0) { @@ -354,7 +354,7 @@ class TagController extends Controller $request->session()->flash('error', $this->attachmentsHelper->getErrors()->get('attachments')); } $redirect = redirect($this->getPreviousUrl('tags.edit.url')); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { session()->put('tags.edit.fromUpdate', true); $redirect = redirect(route('tags.edit', [$tag->id]))->withInput(['return_to_edit' => 1]); diff --git a/app/Http/Controllers/Transaction/BulkController.php b/app/Http/Controllers/Transaction/BulkController.php index d6ac70283b..7d48564d4f 100644 --- a/app/Http/Controllers/Transaction/BulkController.php +++ b/app/Http/Controllers/Transaction/BulkController.php @@ -55,7 +55,7 @@ class BulkController extends Controller $this->middleware( function ($request, $next) { $this->repository = app(JournalRepositoryInterface::class); - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); return $next($request); @@ -72,7 +72,7 @@ class BulkController extends Controller */ public function edit(array $journals) { - $subTitle = (string)trans('firefly.mass_bulk_journals'); + $subTitle = (string) trans('firefly.mass_bulk_journals'); $this->rememberPreviousUrl('transactions.bulk-edit.url'); @@ -95,14 +95,14 @@ class BulkController extends Controller { $journalIds = $request->get('journals'); $journalIds = is_array($journalIds) ? $journalIds : []; - $ignoreCategory = 1 === (int)$request->get('ignore_category'); - $ignoreBudget = 1 === (int)$request->get('ignore_budget'); + $ignoreCategory = 1 === (int) $request->get('ignore_category'); + $ignoreBudget = 1 === (int) $request->get('ignore_budget'); $tagsAction = $request->get('tags_action'); $collection = new Collection(); $count = 0; foreach ($journalIds as $journalId) { - $journalId = (int)$journalId; + $journalId = (int) $journalId; $journal = $this->repository->find($journalId); if (null !== $journal) { $resultA = $this->updateJournalBudget($journal, $ignoreBudget, $request->integer('budget_id')); diff --git a/app/Http/Controllers/Transaction/ConvertController.php b/app/Http/Controllers/Transaction/ConvertController.php index b65208b005..b7cd58506c 100644 --- a/app/Http/Controllers/Transaction/ConvertController.php +++ b/app/Http/Controllers/Transaction/ConvertController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Transaction; +use Exception; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; @@ -33,6 +34,7 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Services\Internal\Update\JournalUpdateService; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\ModelInformation; use FireflyIII\Transformers\TransactionGroupTransformer; use FireflyIII\Validation\AccountValidator; @@ -214,15 +216,14 @@ class ConvertController extends Controller private function getLiabilities(): array { // make repositories - $accountList = $this->accountRepository->getActiveAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); - $defaultCurrency = app('amount')->getDefaultCurrency(); - $grouped = []; + $accountList = $this->accountRepository->getActiveAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); + $grouped = []; // group accounts: /** @var Account $account */ foreach ($accountList as $account) { - $balance = app('steam')->balance($account, today()); - $currency = $this->accountRepository->getAccountCurrency($account) ?? $defaultCurrency; + $balance = Steam::finalAccountBalance($account, today()->endOfDay())['balance']; + $currency = $this->accountRepository->getAccountCurrency($account) ?? $this->defaultCurrency; $role = 'l_'.$account->accountType->type; $key = (string) trans('firefly.opt_group_'.$role); $grouped[$key][$account->id] = $account->name.' ('.app('amount')->formatAnything($currency, $balance, false).')'; @@ -237,15 +238,14 @@ class ConvertController extends Controller private function getAssetAccounts(): array { // make repositories - $accountList = $this->accountRepository->getActiveAccountsByType([AccountType::ASSET]); - $defaultCurrency = app('amount')->getDefaultCurrency(); - $grouped = []; + $accountList = $this->accountRepository->getActiveAccountsByType([AccountType::ASSET]); + $grouped = []; // group accounts: /** @var Account $account */ foreach ($accountList as $account) { - $balance = app('steam')->balance($account, today()); - $currency = $this->accountRepository->getAccountCurrency($account) ?? $defaultCurrency; + $balance = Steam::finalAccountBalance($account, today()->endOfDay())['balance']; + $currency = $this->accountRepository->getAccountCurrency($account) ?? $this->defaultCurrency; $role = (string) $this->accountRepository->getMetaValue($account, 'account_role'); if ('' === $role) { $role = 'no_account_type'; diff --git a/app/Http/Controllers/Transaction/CreateController.php b/app/Http/Controllers/Transaction/CreateController.php index daca5c526b..fad87c640a 100644 --- a/app/Http/Controllers/Transaction/CreateController.php +++ b/app/Http/Controllers/Transaction/CreateController.php @@ -51,7 +51,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); $this->repository = app(TransactionGroupRepositoryInterface::class); @@ -62,7 +62,7 @@ class CreateController extends Controller public function cloneGroup(Request $request): JsonResponse { - $groupId = (int)$request->get('id'); + $groupId = (int) $request->get('id'); if (0 !== $groupId) { $group = $this->repository->find($groupId); if (null !== $group) { @@ -102,19 +102,19 @@ class CreateController extends Controller { app('preferences')->mark(); - $sourceId = (int)request()->get('source'); - $destinationId = (int)request()->get('destination'); + $sourceId = (int) request()->get('source'); + $destinationId = (int) request()->get('destination'); /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); $cash = $accountRepository->getCashAccount(); $preFilled = session()->has('preFilled') ? session('preFilled') : []; - $subTitle = (string)trans(sprintf('breadcrumbs.create_%s', strtolower((string)$objectType))); + $subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType))); $subTitleIcon = 'fa-plus'; $optionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data; $allowedOpposingTypes = config('firefly.allowed_opposing_types'); $accountToTypes = config('firefly.account_to_transaction'); - $defaultCurrency = app('amount')->getDefaultCurrency(); + $defaultCurrency = $this->defaultCurrency; $previousUrl = $this->rememberPreviousUrl('transactions.create.url'); $parts = parse_url($previousUrl); $search = sprintf('?%s', $parts['query'] ?? ''); diff --git a/app/Http/Controllers/Transaction/DeleteController.php b/app/Http/Controllers/Transaction/DeleteController.php index 934605501d..89785805a9 100644 --- a/app/Http/Controllers/Transaction/DeleteController.php +++ b/app/Http/Controllers/Transaction/DeleteController.php @@ -54,7 +54,7 @@ class DeleteController extends Controller // translations: $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); $this->repository = app(TransactionGroupRepositoryInterface::class); @@ -82,7 +82,7 @@ class DeleteController extends Controller throw new NotFoundHttpException(); } $objectType = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); - $subTitle = (string)trans('firefly.delete_'.$objectType, ['description' => $group->title ?? $journal->description]); + $subTitle = (string) trans('firefly.delete_'.$objectType, ['description' => $group->title ?? $journal->description]); $previous = app('steam')->getSafePreviousUrl(); // put previous url in session app('log')->debug('Will try to remember previous URL'); @@ -106,7 +106,7 @@ class DeleteController extends Controller throw new NotFoundHttpException(); } $objectType = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); - session()->flash('success', (string)trans('firefly.deleted_'.strtolower($objectType), ['description' => $group->title ?? $journal->description])); + session()->flash('success', (string) trans('firefly.deleted_'.strtolower($objectType), ['description' => $group->title ?? $journal->description])); // grab asset account(s) from group: $accounts = []; diff --git a/app/Http/Controllers/Transaction/EditController.php b/app/Http/Controllers/Transaction/EditController.php index 35917229e4..c638deb7c8 100644 --- a/app/Http/Controllers/Transaction/EditController.php +++ b/app/Http/Controllers/Transaction/EditController.php @@ -52,7 +52,7 @@ class EditController extends Controller // translations: $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); $this->repository = app(JournalRepositoryInterface::class); @@ -80,9 +80,9 @@ class EditController extends Controller $expectedSourceTypes = config('firefly.expected_source_types'); $allowedSourceDests = config('firefly.source_dests'); $title = $transactionGroup->transactionJournals()->count() > 1 ? $transactionGroup->title : $transactionGroup->transactionJournals()->first()->description; - $subTitle = (string)trans('firefly.edit_transaction_title', ['description' => $title]); + $subTitle = (string) trans('firefly.edit_transaction_title', ['description' => $title]); $subTitleIcon = 'fa-plus'; - $defaultCurrency = app('amount')->getDefaultCurrency(); + $defaultCurrency = $this->defaultCurrency; $cash = $repository->getCashAccount(); $previousUrl = $this->rememberPreviousUrl('transactions.edit.url'); $parts = parse_url($previousUrl); diff --git a/app/Http/Controllers/Transaction/IndexController.php b/app/Http/Controllers/Transaction/IndexController.php index 6e4f47df22..7b6ecd2b39 100644 --- a/app/Http/Controllers/Transaction/IndexController.php +++ b/app/Http/Controllers/Transaction/IndexController.php @@ -54,7 +54,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-exchange'); - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); $this->repository = app(JournalRepositoryInterface::class); @@ -78,8 +78,8 @@ class IndexController extends Controller $subTitleIcon = config('firefly.transactionIconsByType.'.$objectType); $types = config('firefly.transactionTypesByType.'.$objectType); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; if (null === $start) { $start = session('start'); @@ -94,7 +94,7 @@ class IndexController extends Controller [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; $startStr = $start->isoFormat($this->monthAndDayFormat); $endStr = $end->isoFormat($this->monthAndDayFormat); - $subTitle = (string)trans(sprintf('firefly.title_%s_between', $objectType), ['start' => $startStr, 'end' => $endStr]); + $subTitle = (string) trans(sprintf('firefly.title_%s_between', $objectType), ['start' => $startStr, 'end' => $endStr]); $path = route('transactions.index', [$objectType, $start->format('Y-m-d'), $end->format('Y-m-d')]); $firstJournal = $this->repository->firstNull(); $startPeriod = null === $firstJournal ? new Carbon() : $firstJournal->date; @@ -128,14 +128,14 @@ class IndexController extends Controller { $subTitleIcon = config('firefly.transactionIconsByType.'.$objectType); $types = config('firefly.transactionTypesByType.'.$objectType); - $page = (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $path = route('transactions.index.all', [$objectType]); $first = $this->repository->firstNull(); $start = null === $first ? new Carbon() : $first->date; $last = $this->repository->getLast(); $end = null !== $last ? $last->date : today(config('app.timezone')); - $subTitle = (string)trans('firefly.all_'.$objectType); + $subTitle = (string) trans('firefly.all_'.$objectType); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); diff --git a/app/Http/Controllers/Transaction/LinkController.php b/app/Http/Controllers/Transaction/LinkController.php index 49a2e27dc8..d46fa20eee 100644 --- a/app/Http/Controllers/Transaction/LinkController.php +++ b/app/Http/Controllers/Transaction/LinkController.php @@ -52,7 +52,7 @@ class LinkController extends Controller // some useful repositories: $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); $this->journalRepository = app(JournalRepositoryInterface::class); @@ -71,7 +71,7 @@ class LinkController extends Controller public function delete(TransactionJournalLink $link) { $subTitleIcon = 'fa-link'; - $subTitle = (string)trans('breadcrumbs.delete_journal_link'); + $subTitle = (string) trans('breadcrumbs.delete_journal_link'); $this->rememberPreviousUrl('journal_links.delete.url'); return view('transactions.links.delete', compact('link', 'subTitle', 'subTitleIcon')); @@ -86,10 +86,10 @@ class LinkController extends Controller { $this->repository->destroyLink($link); - session()->flash('success', (string)trans('firefly.deleted_link')); + session()->flash('success', (string) trans('firefly.deleted_link')); app('preferences')->mark(); - return redirect((string)session('journal_links.delete.url')); + return redirect((string) session('journal_links.delete.url')); } /** @@ -114,7 +114,7 @@ class LinkController extends Controller app('log')->debug('We are here (store)'); $other = $this->journalRepository->find($linkInfo['transaction_journal_id']); if (null === $other) { - session()->flash('error', (string)trans('firefly.invalid_link_selection')); + session()->flash('error', (string) trans('firefly.invalid_link_selection')); return redirect(route('transactions.show', [$journal->transaction_group_id])); } @@ -122,19 +122,19 @@ class LinkController extends Controller $alreadyLinked = $this->repository->findLink($journal, $other); if ($other->id === $journal->id) { - session()->flash('error', (string)trans('firefly.journals_link_to_self')); + session()->flash('error', (string) trans('firefly.journals_link_to_self')); return redirect(route('transactions.show', [$journal->transaction_group_id])); } if ($alreadyLinked) { - session()->flash('error', (string)trans('firefly.journals_error_linked')); + session()->flash('error', (string) trans('firefly.journals_error_linked')); return redirect(route('transactions.show', [$journal->transaction_group_id])); } app('log')->debug(sprintf('Journal is %d, opposing is %d', $journal->id, $other->id)); $this->repository->storeLink($linkInfo, $other, $journal); - session()->flash('success', (string)trans('firefly.journals_linked')); + session()->flash('success', (string) trans('firefly.journals_linked')); return redirect(route('transactions.show', [$journal->transaction_group_id])); } @@ -146,7 +146,7 @@ class LinkController extends Controller */ public function switchLink(Request $request) { - $linkId = (int)$request->get('id'); + $linkId = (int) $request->get('id'); $this->repository->switchLinkById($linkId); return redirect(app('steam')->getSafePreviousUrl()); diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index af7f14e87e..8278eff521 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -58,7 +58,7 @@ class MassController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); $this->repository = app(JournalRepositoryInterface::class); @@ -72,7 +72,7 @@ class MassController extends Controller */ public function delete(array $journals): IlluminateView { - $subTitle = (string)trans('firefly.mass_delete_journals'); + $subTitle = (string) trans('firefly.mass_delete_journals'); // put previous url in session $this->rememberPreviousUrl('transactions.mass-delete.url'); @@ -98,8 +98,8 @@ class MassController extends Controller app('log')->debug(sprintf('Searching for ID #%d', $journalId)); /** @var null|TransactionJournal $journal */ - $journal = $this->repository->find((int)$journalId); - if (null !== $journal && (int)$journalId === $journal->id) { + $journal = $this->repository->find((int) $journalId); + if (null !== $journal && (int) $journalId === $journal->id) { $this->repository->destroyJournal($journal); ++$count; app('log')->debug(sprintf('Deleted transaction journal #%d', $journalId)); @@ -121,7 +121,7 @@ class MassController extends Controller */ public function edit(array $journals): IlluminateView { - $subTitle = (string)trans('firefly.mass_edit_journals'); + $subTitle = (string) trans('firefly.mass_edit_journals'); /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); @@ -168,7 +168,7 @@ class MassController extends Controller /** @var string $journalId */ foreach ($journalIds as $journalId) { - $integer = (int)$journalId; + $integer = (int) $journalId; try { $this->updateJournal($integer, $request); @@ -251,7 +251,7 @@ class MassController extends Controller return null; } - return (string)$value[$journalId]; + return (string) $value[$journalId]; } private function getIntFromRequest(MassEditJournalRequest $request, int $journalId, string $string): ?int @@ -264,6 +264,6 @@ class MassController extends Controller return null; } - return (int)$value[$journalId]; + return (int) $value[$journalId]; } } diff --git a/app/Http/Controllers/Transaction/ShowController.php b/app/Http/Controllers/Transaction/ShowController.php index 24898e0d6f..2cd234f39a 100644 --- a/app/Http/Controllers/Transaction/ShowController.php +++ b/app/Http/Controllers/Transaction/ShowController.php @@ -57,7 +57,7 @@ class ShowController extends Controller $this->repository = app(TransactionGroupRepositoryInterface::class); $this->aleRepository = app(ALERepositoryInterface::class); - app('view')->share('title', (string)trans('firefly.transactions')); + app('view')->share('title', (string) trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); return $next($request); @@ -88,7 +88,7 @@ class ShowController extends Controller throw new FireflyException('This transaction is broken :(.'); } - $type = (string)trans(sprintf('firefly.%s', $first->transactionType->type)); + $type = (string) trans(sprintf('firefly.%s', $first->transactionType->type)); $title = 1 === $splits ? $first->description : $transactionGroup->title; $subTitle = sprintf('%s: "%s"', $type, $title); diff --git a/app/Http/Controllers/TransactionCurrency/CreateController.php b/app/Http/Controllers/TransactionCurrency/CreateController.php index 75bb2f9cf3..11dd895971 100644 --- a/app/Http/Controllers/TransactionCurrency/CreateController.php +++ b/app/Http/Controllers/TransactionCurrency/CreateController.php @@ -54,7 +54,7 @@ class CreateController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.currencies')); + app('view')->share('title', (string) trans('firefly.currencies')); app('view')->share('mainTitleIcon', 'fa-usd'); $this->repository = app(CurrencyRepositoryInterface::class); $this->userRepository = app(UserRepositoryInterface::class); @@ -74,13 +74,13 @@ class CreateController extends Controller /** @var User $user */ $user = auth()->user(); if (!$this->userRepository->hasRole($user, 'owner')) { - $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); + $request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); return redirect(route('currencies.index')); } $subTitleIcon = 'fa-plus'; - $subTitle = (string)trans('firefly.create_currency'); + $subTitle = (string) trans('firefly.create_currency'); // put previous url in session if not redirect from store (not "create another"). if (true !== session('currencies.create.fromStore')) { @@ -117,15 +117,15 @@ class CreateController extends Controller } catch (FireflyException $e) { app('log')->error($e->getMessage()); Log::channel('audit')->warning('Could not store (POST) currency without admin rights.', $data); - $request->session()->flash('error', (string)trans('firefly.could_not_store_currency')); + $request->session()->flash('error', (string) trans('firefly.could_not_store_currency')); $currency = null; } $redirect = redirect($this->getPreviousUrl('currencies.create.url')); if (null !== $currency) { - $request->session()->flash('success', (string)trans('firefly.created_currency', ['name' => $currency->name])); + $request->session()->flash('success', (string) trans('firefly.created_currency', ['name' => $currency->name])); Log::channel('audit')->info('Created (POST) currency.', $data); - if (1 === (int)$request->get('create_another')) { + if (1 === (int) $request->get('create_another')) { $request->session()->put('currencies.create.fromStore', true); $redirect = redirect(route('currencies.create'))->withInput(); diff --git a/app/Http/Controllers/TransactionCurrency/DeleteController.php b/app/Http/Controllers/TransactionCurrency/DeleteController.php index 5ddecbff90..c2785f2696 100644 --- a/app/Http/Controllers/TransactionCurrency/DeleteController.php +++ b/app/Http/Controllers/TransactionCurrency/DeleteController.php @@ -53,7 +53,7 @@ class DeleteController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.currencies')); + app('view')->share('title', (string) trans('firefly.currencies')); app('view')->share('mainTitleIcon', 'fa-usd'); $this->repository = app(CurrencyRepositoryInterface::class); $this->userRepository = app(UserRepositoryInterface::class); @@ -73,7 +73,7 @@ class DeleteController extends Controller /** @var User $user */ $user = auth()->user(); if (!$this->userRepository->hasRole($user, 'owner')) { - $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); + $request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); Log::channel('audit')->warning(sprintf('Tried to visit page to delete currency %s but is not site owner.', $currency->code)); return redirect(route('currencies.index')); @@ -81,7 +81,7 @@ class DeleteController extends Controller if ($this->repository->currencyInUse($currency)) { $location = $this->repository->currencyInUseAt($currency); - $message = (string)trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]); + $message = (string) trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]); $request->session()->flash('error', $message); Log::channel('audit')->warning(sprintf('Tried to visit page to delete currency %s but currency is in use.', $currency->code)); @@ -90,7 +90,7 @@ class DeleteController extends Controller // put previous url in session $this->rememberPreviousUrl('currencies.delete.url'); - $subTitle = (string)trans('form.delete_currency', ['name' => $currency->name]); + $subTitle = (string) trans('form.delete_currency', ['name' => $currency->name]); Log::channel('audit')->info(sprintf('Visit page to delete currency %s.', $currency->code)); return view('currencies.delete', compact('currency', 'subTitle')); @@ -106,21 +106,21 @@ class DeleteController extends Controller /** @var User $user */ $user = auth()->user(); if (!$this->userRepository->hasRole($user, 'owner')) { - $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); + $request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); Log::channel('audit')->warning(sprintf('Tried to delete currency %s but is not site owner.', $currency->code)); return redirect(route('currencies.index')); } if ($this->repository->currencyInUse($currency)) { - $request->session()->flash('error', (string)trans('firefly.cannot_delete_currency', ['name' => e($currency->name)])); + $request->session()->flash('error', (string) trans('firefly.cannot_delete_currency', ['name' => e($currency->name)])); Log::channel('audit')->info(sprintf('Tried to delete currency %s but is in use.', $currency->code)); return redirect(route('currencies.index')); } if ($this->repository->isFallbackCurrency($currency)) { - $request->session()->flash('error', (string)trans('firefly.cannot_delete_fallback_currency', ['name' => e($currency->name)])); + $request->session()->flash('error', (string) trans('firefly.cannot_delete_fallback_currency', ['name' => e($currency->name)])); Log::channel('audit')->info(sprintf('Tried to delete currency %s but is FALLBACK.', $currency->code)); return redirect(route('currencies.index')); @@ -129,7 +129,7 @@ class DeleteController extends Controller Log::channel('audit')->info(sprintf('Deleted currency %s.', $currency->code)); $this->repository->destroy($currency); - $request->session()->flash('success', (string)trans('firefly.deleted_currency', ['name' => $currency->name])); + $request->session()->flash('success', (string) trans('firefly.deleted_currency', ['name' => $currency->name])); return redirect($this->getPreviousUrl('currencies.delete.url')); } diff --git a/app/Http/Controllers/TransactionCurrency/EditController.php b/app/Http/Controllers/TransactionCurrency/EditController.php index 75ceb9f539..56ef93ae77 100644 --- a/app/Http/Controllers/TransactionCurrency/EditController.php +++ b/app/Http/Controllers/TransactionCurrency/EditController.php @@ -51,7 +51,7 @@ class EditController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.currencies')); + app('view')->share('title', (string) trans('firefly.currencies')); app('view')->share('mainTitleIcon', 'fa-usd'); $this->repository = app(CurrencyRepositoryInterface::class); $this->userRepository = app(UserRepositoryInterface::class); @@ -71,14 +71,14 @@ class EditController extends Controller /** @var User $user */ $user = auth()->user(); if (!$this->userRepository->hasRole($user, 'owner')) { - $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); + $request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); Log::channel('audit')->warning(sprintf('Tried to edit currency %s but is not owner.', $currency->code)); return redirect(route('currencies.index')); } $subTitleIcon = 'fa-pencil'; - $subTitle = (string)trans('breadcrumbs.edit_currency', ['name' => $currency->name]); + $subTitle = (string) trans('breadcrumbs.edit_currency', ['name' => $currency->name]); $currency->symbol = htmlentities($currency->symbol); // is currently enabled (for this user?) @@ -88,7 +88,7 @@ class EditController extends Controller // code to handle active-checkboxes $hasOldInput = null !== $request->old('_token'); $preFilled = [ - 'enabled' => $hasOldInput ? (bool)$request->old('enabled') : $enabled, + 'enabled' => $hasOldInput ? (bool) $request->old('enabled') : $enabled, ]; $request->session()->flash('preFilled', $preFilled); @@ -119,17 +119,17 @@ class EditController extends Controller } if (!$this->userRepository->hasRole($user, 'owner')) { - $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); + $request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))])); Log::channel('audit')->warning('Tried to update (POST) currency without admin rights.', $data); return redirect(route('currencies.index')); } $currency = $this->repository->update($currency, $data); Log::channel('audit')->info('Updated (POST) currency.', $data); - $request->session()->flash('success', (string)trans('firefly.updated_currency', ['name' => $currency->name])); + $request->session()->flash('success', (string) trans('firefly.updated_currency', ['name' => $currency->name])); app('preferences')->mark(); - if (1 === (int)$request->get('return_to_edit')) { + if (1 === (int) $request->get('return_to_edit')) { $request->session()->put('currencies.edit.fromUpdate', true); return redirect(route('currencies.edit', [$currency->id])); diff --git a/app/Http/Controllers/TransactionCurrency/IndexController.php b/app/Http/Controllers/TransactionCurrency/IndexController.php index 3dc9fb739f..0c728a982c 100644 --- a/app/Http/Controllers/TransactionCurrency/IndexController.php +++ b/app/Http/Controllers/TransactionCurrency/IndexController.php @@ -48,7 +48,7 @@ class IndexController extends Controller $this->middleware( function ($request, $next) { - app('view')->share('title', (string)trans('firefly.currencies')); + app('view')->share('title', (string) trans('firefly.currencies')); app('view')->share('mainTitleIcon', 'fa-usd'); $this->repository = app(CurrencyRepositoryInterface::class); $this->userRepository = app(UserRepositoryInterface::class); @@ -67,13 +67,11 @@ class IndexController extends Controller { /** @var User $user */ $user = auth()->user(); - $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); - $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; + $page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; $collection = $this->repository->getAll(); - $total = $collection->count(); - $collection = $collection->slice(($page - 1) * $pageSize, $pageSize); - // order so default is on top: + // order so default and enabled are on top: $collection = $collection->sortBy( static function (TransactionCurrency $currency) { $default = true === $currency->userGroupDefault ? 0 : 1; @@ -82,12 +80,14 @@ class IndexController extends Controller return sprintf('%s-%s-%s', $default, $enabled, $currency->code); } ); + $total = $collection->count(); + $collection = $collection->slice(($page - 1) * $pageSize, $pageSize); $currencies = new LengthAwarePaginator($collection, $total, $pageSize, $page); $currencies->setPath(route('currencies.index')); $isOwner = true; if (!$this->userRepository->hasRole($user, 'owner')) { - $request->session()->flash('info', (string)trans('firefly.ask_site_owner', ['owner' => config('firefly.site_owner')])); + $request->session()->flash('info', (string) trans('firefly.ask_site_owner', ['owner' => config('firefly.site_owner')])); $isOwner = false; } diff --git a/app/Http/Controllers/UserGroup/CreateController.php b/app/Http/Controllers/UserGroup/CreateController.php index c62367d193..0f20e9e4c2 100644 --- a/app/Http/Controllers/UserGroup/CreateController.php +++ b/app/Http/Controllers/UserGroup/CreateController.php @@ -36,8 +36,8 @@ class CreateController extends Controller */ public function create() { - $title = (string)trans('firefly.administrations_page_title'); - $subTitle = (string)trans('firefly.administrations_page_create_sub_title'); + $title = (string) trans('firefly.administrations_page_title'); + $subTitle = (string) trans('firefly.administrations_page_create_sub_title'); $mainTitleIcon = 'fa-book'; app('log')->debug(sprintf('Now at %s', __METHOD__)); diff --git a/app/Http/Controllers/UserGroup/EditController.php b/app/Http/Controllers/UserGroup/EditController.php index 1e61947ef3..c8d68e991d 100644 --- a/app/Http/Controllers/UserGroup/EditController.php +++ b/app/Http/Controllers/UserGroup/EditController.php @@ -37,8 +37,8 @@ class EditController extends Controller */ public function edit(UserGroup $userGroup) { - $title = (string)trans('firefly.administrations_page_title'); - $subTitle = (string)trans('firefly.administrations_page_edit_sub_title', ['title' => $userGroup->title]); + $title = (string) trans('firefly.administrations_page_title'); + $subTitle = (string) trans('firefly.administrations_page_edit_sub_title', ['title' => $userGroup->title]); $mainTitleIcon = 'fa-book'; app('log')->debug(sprintf('Now at %s', __METHOD__)); diff --git a/app/Http/Controllers/UserGroup/IndexController.php b/app/Http/Controllers/UserGroup/IndexController.php index 9b0f15c1dd..41c05ad8b1 100644 --- a/app/Http/Controllers/UserGroup/IndexController.php +++ b/app/Http/Controllers/UserGroup/IndexController.php @@ -38,8 +38,8 @@ class IndexController extends Controller */ public function index(Request $request) { - $title = (string)trans('firefly.administrations_page_title'); - $subTitle = (string)trans('firefly.administrations_page_sub_title'); + $title = (string) trans('firefly.administrations_page_title'); + $subTitle = (string) trans('firefly.administrations_page_sub_title'); $mainTitleIcon = 'fa-book'; app('log')->debug(sprintf('Now at %s', __METHOD__)); diff --git a/app/Http/Controllers/Webhooks/CreateController.php b/app/Http/Controllers/Webhooks/CreateController.php index b1d06ce1a1..70ac3cbd35 100644 --- a/app/Http/Controllers/Webhooks/CreateController.php +++ b/app/Http/Controllers/Webhooks/CreateController.php @@ -44,8 +44,8 @@ class CreateController extends Controller static function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-bolt'); app('view')->share('subTitleIcon', 'fa-plus'); - app('view')->share('title', (string)trans('firefly.webhooks')); - app('view')->share('subTitle', (string)trans('firefly.create_new_webhook')); + app('view')->share('title', (string) trans('firefly.webhooks')); + app('view')->share('subTitle', (string) trans('firefly.create_new_webhook')); return $next($request); } diff --git a/app/Http/Controllers/Webhooks/DeleteController.php b/app/Http/Controllers/Webhooks/DeleteController.php index 023016aeb5..4fd33895d1 100644 --- a/app/Http/Controllers/Webhooks/DeleteController.php +++ b/app/Http/Controllers/Webhooks/DeleteController.php @@ -49,8 +49,8 @@ class DeleteController extends Controller static function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-bolt'); app('view')->share('subTitleIcon', 'fa-trash'); - app('view')->share('title', (string)trans('firefly.webhooks')); - app('view')->share('subTitle', (string)trans('firefly.delete_webhook')); + app('view')->share('title', (string) trans('firefly.webhooks')); + app('view')->share('subTitle', (string) trans('firefly.delete_webhook')); return $next($request); } @@ -70,7 +70,7 @@ class DeleteController extends Controller throw new NotFoundHttpException('Webhooks are not enabled.'); } Log::channel('audit')->info('User visits webhook delete page.'); - $subTitle = (string)trans('firefly.delete_webhook', ['title' => $webhook->title]); + $subTitle = (string) trans('firefly.delete_webhook', ['title' => $webhook->title]); $this->rememberPreviousUrl('webhooks.delete.url'); return view('webhooks.delete', compact('webhook', 'subTitle')); diff --git a/app/Http/Controllers/Webhooks/EditController.php b/app/Http/Controllers/Webhooks/EditController.php index a8ab31b5c1..7ea2b6c63b 100644 --- a/app/Http/Controllers/Webhooks/EditController.php +++ b/app/Http/Controllers/Webhooks/EditController.php @@ -49,7 +49,7 @@ class EditController extends Controller static function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-bolt'); app('view')->share('subTitleIcon', 'fa-pencil'); - app('view')->share('title', (string)trans('firefly.webhooks')); + app('view')->share('title', (string) trans('firefly.webhooks')); return $next($request); } @@ -69,7 +69,7 @@ class EditController extends Controller throw new NotFoundHttpException('Webhooks are not enabled.'); } Log::channel('audit')->info('User visits webhook edit page.'); - $subTitle = (string)trans('firefly.edit_webhook', ['title' => $webhook->title]); + $subTitle = (string) trans('firefly.edit_webhook', ['title' => $webhook->title]); $this->rememberPreviousUrl('webhooks.edit.url'); return view('webhooks.edit', compact('webhook', 'subTitle')); diff --git a/app/Http/Controllers/Webhooks/IndexController.php b/app/Http/Controllers/Webhooks/IndexController.php index c6ee272353..a3ca4790c2 100644 --- a/app/Http/Controllers/Webhooks/IndexController.php +++ b/app/Http/Controllers/Webhooks/IndexController.php @@ -43,7 +43,7 @@ class IndexController extends Controller $this->middleware( static function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-bolt'); - app('view')->share('title', (string)trans('firefly.webhooks')); + app('view')->share('title', (string) trans('firefly.webhooks')); return $next($request); } diff --git a/app/Http/Controllers/Webhooks/ShowController.php b/app/Http/Controllers/Webhooks/ShowController.php index 46c9121217..37e0e03003 100644 --- a/app/Http/Controllers/Webhooks/ShowController.php +++ b/app/Http/Controllers/Webhooks/ShowController.php @@ -49,7 +49,7 @@ class ShowController extends Controller static function ($request, $next) { app('view')->share('mainTitleIcon', 'fa-bolt'); app('view')->share('subTitleIcon', 'fa-bolt'); - app('view')->share('title', (string)trans('firefly.webhooks')); + app('view')->share('title', (string) trans('firefly.webhooks')); return $next($request); } @@ -69,7 +69,7 @@ class ShowController extends Controller throw new NotFoundHttpException('Webhooks are not enabled.'); } Log::channel('audit')->info(sprintf('User visits webhook #%d page.', $webhook->id)); - $subTitle = (string)trans('firefly.show_webhook', ['title' => $webhook->title]); + $subTitle = (string) trans('firefly.show_webhook', ['title' => $webhook->title]); return view('webhooks.show', compact('webhook', 'subTitle')); } diff --git a/app/Http/Middleware/AcceptHeaders.php b/app/Http/Middleware/AcceptHeaders.php index 8c525a4a52..e3cd497da4 100644 --- a/app/Http/Middleware/AcceptHeaders.php +++ b/app/Http/Middleware/AcceptHeaders.php @@ -43,7 +43,7 @@ class AcceptHeaders $method = $request->getMethod(); $accepts = ['application/x-www-form-urlencoded', 'application/json', 'application/vnd.api+json', 'application/octet-stream', '*/*']; $contentTypes = ['application/x-www-form-urlencoded', 'application/json', 'application/vnd.api+json', 'application/octet-stream']; - $submitted = (string)$request->header('Content-Type'); + $submitted = (string) $request->header('Content-Type'); // if bad Accept header, send error. if (!$request->accepts($accepts)) { diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 400482d09d..eec83beed2 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -122,10 +122,10 @@ class Authenticate } if (null !== $user) { // app('log')->debug(get_class($user)); - if (1 === (int)$user->blocked) { - $message = (string)trans('firefly.block_account_logout'); + if (1 === (int) $user->blocked) { + $message = (string) trans('firefly.block_account_logout'); if ('email_changed' === $user->blocked_code) { - $message = (string)trans('firefly.email_changed_logout'); + $message = (string) trans('firefly.email_changed_logout'); } app('log')->warning('User is blocked, cannot use authentication method.'); app('session')->flash('logoutMessage', $message); diff --git a/app/Http/Middleware/Installer.php b/app/Http/Middleware/Installer.php index fbaed7f43b..158976ca98 100644 --- a/app/Http/Middleware/Installer.php +++ b/app/Http/Middleware/Installer.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Http\Middleware; -use DB; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Support\System\OAuthKeys; use Illuminate\Database\QueryException; @@ -132,8 +131,8 @@ class Installer private function oldDBVersion(): bool { // older version in config than database? - $configVersion = (int)config('firefly.db_version'); - $dbVersion = (int)app('fireflyconfig')->getFresh('db_version', 1)->data; + $configVersion = (int) config('firefly.db_version'); + $dbVersion = (int) app('fireflyconfig')->getFresh('db_version', 1)->data; if ($configVersion > $dbVersion) { Log::warning( sprintf( @@ -157,8 +156,8 @@ class Installer private function oldVersion(): bool { // version compare thing. - $configVersion = (string)config('firefly.version'); - $dbVersion = (string)app('fireflyconfig')->getFresh('ff3_version', '1.0')->data; + $configVersion = (string) config('firefly.version'); + $dbVersion = (string) app('fireflyconfig')->getFresh('ff3_version', '1.0')->data; if (str_starts_with($configVersion, 'develop')) { Log::debug('Skipping version check for develop version.'); diff --git a/app/Http/Middleware/InterestingMessage.php b/app/Http/Middleware/InterestingMessage.php index 91a5b1e041..c1fea3583b 100644 --- a/app/Http/Middleware/InterestingMessage.php +++ b/app/Http/Middleware/InterestingMessage.php @@ -95,7 +95,7 @@ class InterestingMessage // send message about newly created transaction group. /** @var null|TransactionGroup $group */ - $group = auth()->user()->transactionGroups()->with(['transactionJournals', 'transactionJournals.transactionType'])->find((int)$transactionGroupId); + $group = auth()->user()->transactionGroups()->with(['transactionJournals', 'transactionJournals.transactionType'])->find((int) $transactionGroupId); if (null === $group) { return; @@ -111,17 +111,17 @@ class InterestingMessage $title = $count > 1 ? $group->title : $journal->description; if ('created' === $message) { session()->flash('success_url', route('transactions.show', [$transactionGroupId])); - session()->flash('success', (string)trans('firefly.stored_journal', ['description' => $title])); + session()->flash('success', (string) trans('firefly.stored_journal', ['description' => $title])); } if ('updated' === $message) { $type = strtolower($journal->transactionType->type); session()->flash('success_url', route('transactions.show', [$transactionGroupId])); - session()->flash('success', (string)trans(sprintf('firefly.updated_%s', $type), ['description' => $title])); + session()->flash('success', (string) trans(sprintf('firefly.updated_%s', $type), ['description' => $title])); } if ('no_change' === $message) { $type = strtolower($journal->transactionType->type); session()->flash('warning_url', route('transactions.show', [$transactionGroupId])); - session()->flash('warning', (string)trans(sprintf('firefly.no_changes_%s', $type), ['description' => $title])); + session()->flash('warning', (string) trans(sprintf('firefly.no_changes_%s', $type), ['description' => $title])); } } @@ -147,13 +147,13 @@ class InterestingMessage return; } if ('deleted' === $message) { - session()->flash('success', (string)trans('firefly.account_deleted', ['name' => $account->name])); + session()->flash('success', (string) trans('firefly.account_deleted', ['name' => $account->name])); } if ('created' === $message) { - session()->flash('success', (string)trans('firefly.stored_new_account', ['name' => $account->name])); + session()->flash('success', (string) trans('firefly.stored_new_account', ['name' => $account->name])); } if ('updated' === $message) { - session()->flash('success', (string)trans('firefly.updated_account', ['name' => $account->name])); + session()->flash('success', (string) trans('firefly.updated_account', ['name' => $account->name])); } } @@ -179,10 +179,10 @@ class InterestingMessage return; } if ('deleted' === $message) { - session()->flash('success', (string)trans('firefly.deleted_bill', ['name' => $bill->name])); + session()->flash('success', (string) trans('firefly.deleted_bill', ['name' => $bill->name])); } if ('created' === $message) { - session()->flash('success', (string)trans('firefly.stored_new_bill', ['name' => $bill->name])); + session()->flash('success', (string) trans('firefly.stored_new_bill', ['name' => $bill->name])); } } @@ -208,13 +208,13 @@ class InterestingMessage return; } if ('deleted' === $message) { - session()->flash('success', (string)trans('firefly.deleted_webhook', ['title' => $webhook->title])); + session()->flash('success', (string) trans('firefly.deleted_webhook', ['title' => $webhook->title])); } if ('updated' === $message) { - session()->flash('success', (string)trans('firefly.updated_webhook', ['title' => $webhook->title])); + session()->flash('success', (string) trans('firefly.updated_webhook', ['title' => $webhook->title])); } if ('created' === $message) { - session()->flash('success', (string)trans('firefly.stored_new_webhook', ['title' => $webhook->title])); + session()->flash('success', (string) trans('firefly.stored_new_webhook', ['title' => $webhook->title])); } } @@ -241,22 +241,22 @@ class InterestingMessage return; } if ('enabled' === $message) { - session()->flash('success', (string)trans('firefly.currency_is_now_enabled', ['name' => $currency->name])); + session()->flash('success', (string) trans('firefly.currency_is_now_enabled', ['name' => $currency->name])); } if ('enable_failed' === $message) { - session()->flash('error', (string)trans('firefly.could_not_enable_currency', ['name' => $currency->name])); + session()->flash('error', (string) trans('firefly.could_not_enable_currency', ['name' => $currency->name])); } if ('disabled' === $message) { - session()->flash('success', (string)trans('firefly.currency_is_now_disabled', ['name' => $currency->name])); + session()->flash('success', (string) trans('firefly.currency_is_now_disabled', ['name' => $currency->name])); } if ('disable_failed' === $message) { - session()->flash('error', (string)trans('firefly.could_not_disable_currency', ['name' => $currency->name])); + session()->flash('error', (string) trans('firefly.could_not_disable_currency', ['name' => $currency->name])); } if ('default' === $message) { - session()->flash('success', (string)trans('firefly.new_default_currency', ['name' => $currency->name])); + session()->flash('success', (string) trans('firefly.new_default_currency', ['name' => $currency->name])); } if ('default_failed' === $message) { - session()->flash('error', (string)trans('firefly.default_currency_failed', ['name' => $currency->name])); + session()->flash('error', (string) trans('firefly.default_currency_failed', ['name' => $currency->name])); } } } diff --git a/app/Http/Middleware/IsDemoUser.php b/app/Http/Middleware/IsDemoUser.php index 0cc04ddc18..c984e947a5 100644 --- a/app/Http/Middleware/IsDemoUser.php +++ b/app/Http/Middleware/IsDemoUser.php @@ -49,7 +49,7 @@ class IsDemoUser $repository = app(UserRepositoryInterface::class); if ($repository->hasRole($user, 'demo')) { app('log')->info('User is a demo user.'); - $request->session()->flash('info', (string)trans('firefly.not_available_demo_user')); + $request->session()->flash('info', (string) trans('firefly.not_available_demo_user')); $current = $request->url(); $previous = $request->session()->previousUrl(); if ($current !== $previous) { diff --git a/app/Http/Middleware/Range.php b/app/Http/Middleware/Range.php index 1886600e38..3e0dc65f63 100644 --- a/app/Http/Middleware/Range.php +++ b/app/Http/Middleware/Range.php @@ -25,6 +25,7 @@ namespace FireflyIII\Http\Middleware; use Carbon\Carbon; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Http\Controllers\RequestInformation; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -71,8 +72,8 @@ class Range } $today = today(config('app.timezone')); - $start = app('navigation')->updateStartDate((string)$viewRange, $today); - $end = app('navigation')->updateEndDate((string)$viewRange, $start); + $start = app('navigation')->updateStartDate((string) $viewRange, $today); + $end = app('navigation')->updateEndDate((string) $viewRange, $start); app('session')->put('start', $start); app('session')->put('end', $end); @@ -108,19 +109,19 @@ class Range setlocale(LC_TIME, $localeArray); $moneyResult = setlocale(LC_MONETARY, $localeArray); - // send error to view, if could not set money format + // send error to view, if it could not set money format if (false === $moneyResult) { app('log')->error('Could not set locale. The following array doesnt work: ', $localeArray); app('view')->share('invalidMonetaryLocale', true); } // save some formats: - $monthAndDayFormat = (string)trans('config.month_and_day_js', [], $locale); - $dateTimeFormat = (string)trans('config.date_time_js', [], $locale); - $defaultCurrency = app('amount')->getDefaultCurrency(); + $monthAndDayFormat = (string) trans('config.month_and_day_js', [], $locale); + $dateTimeFormat = (string) trans('config.date_time_js', [], $locale); + $defaultCurrency = Amount::getDefaultCurrency(); // also format for moment JS: - $madMomentJS = (string)trans('config.month_and_day_moment_js', [], $locale); + $madMomentJS = (string) trans('config.month_and_day_moment_js', [], $locale); app('view')->share('madMomentJS', $madMomentJS); app('view')->share('monthAndDayFormat', $monthAndDayFormat); diff --git a/app/Http/Middleware/SecureHeaders.php b/app/Http/Middleware/SecureHeaders.php index d4c749a31c..2220dbfee7 100644 --- a/app/Http/Middleware/SecureHeaders.php +++ b/app/Http/Middleware/SecureHeaders.php @@ -24,9 +24,9 @@ declare(strict_types=1); namespace FireflyIII\Http\Middleware; +use Barryvdh\Debugbar\Facades\Debugbar; use Illuminate\Http\Request; use Illuminate\Support\Facades\Vite; -use Barryvdh\Debugbar\Facades\Debugbar; /** * Class SecureHeaders diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php index 5b778c0a78..d68f3054be 100644 --- a/app/Http/Middleware/TrustProxies.php +++ b/app/Http/Middleware/TrustProxies.php @@ -44,6 +44,6 @@ class TrustProxies extends Middleware */ public function __construct() { - $this->proxies = (string)config('firefly.trusted_proxies'); + $this->proxies = (string) config('firefly.trusted_proxies'); } } diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index beb7146345..748eae98b9 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -30,7 +30,8 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware; */ class VerifyCsrfToken extends Middleware { - protected $except = [ - 'oauth/token', - ]; + protected $except + = [ + 'oauth/token', + ]; } diff --git a/app/Http/Requests/JournalLinkRequest.php b/app/Http/Requests/JournalLinkRequest.php index 6e2e083f11..f03e2bf323 100644 --- a/app/Http/Requests/JournalLinkRequest.php +++ b/app/Http/Requests/JournalLinkRequest.php @@ -46,7 +46,7 @@ class JournalLinkRequest extends FormRequest $return = []; $linkType = $this->get('link_type'); $parts = explode('_', $linkType); - $return['link_type_id'] = (int)$parts[0]; + $return['link_type_id'] = (int) $parts[0]; $return['transaction_journal_id'] = $this->convertInteger('opposing'); $return['notes'] = $this->convertString('notes'); $return['direction'] = $parts[1]; diff --git a/app/Http/Requests/NotificationRequest.php b/app/Http/Requests/NotificationRequest.php new file mode 100644 index 0000000000..9ac3569206 --- /dev/null +++ b/app/Http/Requests/NotificationRequest.php @@ -0,0 +1,78 @@ + $info) { + $value = false; + if ($this->has(sprintf('notification_%s', $key))) { + $value = true; + } + $return[$key] = $value; + } + $return['slack_webhook_url'] = $this->convertString('slack_webhook_url'); + + $return['pushover_app_token'] = $this->convertString('pushover_app_token'); + $return['pushover_user_token'] = $this->convertString('pushover_user_token'); + + $return['ntfy_server'] = $this->convertString('ntfy_server'); + $return['ntfy_topic'] = $this->convertString('ntfy_topic'); + $return['ntfy_auth'] = $this->convertBoolean($this->get('ntfy_auth')); + $return['ntfy_user'] = $this->convertString('ntfy_user'); + $return['ntfy_pass'] = $this->convertString('ntfy_pass'); + + return $return; + } + + /** + * Rules for this request. + */ + public function rules(): array + { + $rules = [ + 'slack_webhook_url' => ['nullable', 'url', 'min:1', new IsValidSlackOrDiscordUrl()], + 'ntfy_server' => ['nullable', 'url', 'min:1'], + 'ntfy_user' => ['required_with:ntfy_pass,ntfy_auth', 'nullable', 'string', 'min:1'], + 'ntfy_pass' => ['required_with:ntfy_user,ntfy_auth', 'nullable', 'string', 'min:1'], + ]; + foreach (config('notifications.notifications.owner') as $key => $info) { + $rules[sprintf('notification_%s', $key)] = 'in:0,1'; + } + + return $rules; + } +} diff --git a/app/Http/Requests/PiggyBankStoreRequest.php b/app/Http/Requests/PiggyBankStoreRequest.php index 94087fedb8..5dfe510745 100644 --- a/app/Http/Requests/PiggyBankStoreRequest.php +++ b/app/Http/Requests/PiggyBankStoreRequest.php @@ -23,7 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Rules\IsValidPositiveAmount; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; @@ -43,15 +46,24 @@ class PiggyBankStoreRequest extends FormRequest */ public function getPiggyBankData(): array { - return [ - 'name' => $this->convertString('name'), - 'startdate' => $this->getCarbonDate('startdate'), - 'account_id' => $this->convertInteger('account_id'), - 'targetamount' => $this->convertString('targetamount'), - 'targetdate' => $this->getCarbonDate('targetdate'), - 'notes' => $this->stringWithNewlines('notes'), - 'object_group_title' => $this->convertString('object_group'), + $accounts = $this->get('accounts'); + $data = [ + 'name' => $this->convertString('name'), + 'start_date' => $this->getCarbonDate('start_date'), + 'target_amount' => $this->convertString('target_amount'), + 'transaction_currency_id' => $this->convertInteger('transaction_currency_id'), + 'target_date' => $this->getCarbonDate('target_date'), + 'notes' => $this->stringWithNewlines('notes'), + 'object_group_title' => $this->convertString('object_group'), ]; + if (!is_array($accounts)) { + $accounts = []; + } + foreach ($accounts as $item) { + $data['accounts'][] = ['account_id' => (int) $item]; + } + + return $data; } /** @@ -60,21 +72,63 @@ class PiggyBankStoreRequest extends FormRequest public function rules(): array { return [ - 'name' => 'required|min:1|max:255|uniquePiggyBankForUser', - 'account_id' => 'required|belongsToUser:accounts', - 'targetamount' => ['nullable', new IsValidPositiveAmount()], - 'startdate' => 'date', - 'targetdate' => 'date|nullable', - 'order' => 'integer|min:1', - 'object_group' => 'min:0|max:255', - 'notes' => 'min:1|max:32768|nullable', + 'name' => 'required|min:1|max:255|uniquePiggyBankForUser', + 'accounts' => 'required|array', + 'accounts.*' => 'required|belongsToUser:accounts', + 'target_amount' => ['nullable', new IsValidPositiveAmount()], + 'start_date' => 'date', + 'target_date' => 'date|nullable', + 'order' => 'integer|min:1', + 'object_group' => 'min:0|max:255', + 'notes' => 'min:1|max:32768|nullable', ]; } public function withValidator(Validator $validator): void { + // need to have more than one account. + // accounts need to have the same currency or be multi-currency(?). + $validator->after( + function (Validator $validator): void { + // validate start before end only if both are there. + $data = $validator->getData(); + $currency = $this->getCurrencyFromData($data); + if (array_key_exists('accounts', $data) && is_array($data['accounts'])) { + $repository = app(AccountRepositoryInterface::class); + $types = config('firefly.piggy_bank_account_types'); + foreach ($data['accounts'] as $value) { + $accountId = (int) $value; + $account = $repository->find($accountId); + if (null !== $account) { + // check currency here. + $accountCurrency = $repository->getAccountCurrency($account); + $isMultiCurrency = $repository->getMetaValue($account, 'is_multi_currency'); + if ($accountCurrency->id !== $currency->id && 'true' !== $isMultiCurrency) { + $validator->errors()->add('accounts', trans('validation.invalid_account_currency')); + } + $type = $account->accountType->type; + if (!in_array($type, $types, true)) { + $validator->errors()->add('accounts', trans('validation.invalid_account_type')); + } + } + } + } + } + ); + if ($validator->fails()) { Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); } } + + private function getCurrencyFromData(array $data): TransactionCurrency + { + $currencyId = (int) ($data['transaction_currency_id'] ?? 0); + $currency = TransactionCurrency::find($currencyId); + if (null === $currency) { + return Amount::getDefaultCurrency(); + } + + return $currency; + } } diff --git a/app/Http/Requests/PiggyBankUpdateRequest.php b/app/Http/Requests/PiggyBankUpdateRequest.php index 7f10011678..e3deddb188 100644 --- a/app/Http/Requests/PiggyBankUpdateRequest.php +++ b/app/Http/Requests/PiggyBankUpdateRequest.php @@ -24,7 +24,10 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Rules\IsValidPositiveAmount; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; @@ -44,15 +47,23 @@ class PiggyBankUpdateRequest extends FormRequest */ public function getPiggyBankData(): array { - return [ + $accounts = $this->get('accounts'); + $data = [ 'name' => $this->convertString('name'), - 'startdate' => $this->getCarbonDate('startdate'), - 'account_id' => $this->convertInteger('account_id'), - 'targetamount' => trim($this->convertString('targetamount')), - 'targetdate' => $this->getCarbonDate('targetdate'), + 'start_date' => $this->getCarbonDate('start_date'), + 'target_amount' => trim($this->convertString('target_amount')), + 'target_date' => $this->getCarbonDate('target_date'), 'notes' => $this->stringWithNewlines('notes'), 'object_group_title' => $this->convertString('object_group'), ]; + if (!is_array($accounts)) { + $accounts = []; + } + foreach ($accounts as $item) { + $data['accounts'][] = ['account_id' => (int) $item]; + } + + return $data; } /** @@ -64,21 +75,63 @@ class PiggyBankUpdateRequest extends FormRequest $piggy = $this->route()->parameter('piggyBank'); return [ - 'name' => sprintf('required|min:1|max:255|uniquePiggyBankForUser:%d', $piggy->id), - 'account_id' => 'required|belongsToUser:accounts', - 'targetamount' => ['nullable', new IsValidPositiveAmount()], - 'startdate' => 'date', - 'targetdate' => 'date|nullable', - 'order' => 'integer|max:32768|min:1', - 'object_group' => 'min:0|max:255', - 'notes' => 'min:1|max:32768|nullable', + 'name' => sprintf('required|min:1|max:255|uniquePiggyBankForUser:%d', $piggy->id), + 'accounts' => 'required|array', + 'accounts.*' => 'required|belongsToUser:accounts', + 'target_amount' => ['nullable', new IsValidPositiveAmount()], + 'start_date' => 'date', + 'target_date' => 'date|nullable', + 'order' => 'integer|max:32768|min:1', + 'object_group' => 'min:0|max:255', + 'notes' => 'min:1|max:32768|nullable', ]; } public function withValidator(Validator $validator): void - { + { // need to have more than one account. + // accounts need to have the same currency or be multi-currency(?). + $validator->after( + function (Validator $validator): void { + // validate start before end only if both are there. + $data = $validator->getData(); + $currency = $this->getCurrencyFromData($data); + if (array_key_exists('accounts', $data) && is_array($data['accounts'])) { + $repository = app(AccountRepositoryInterface::class); + $types = config('firefly.piggy_bank_account_types'); + foreach ($data['accounts'] as $value) { + $accountId = (int) $value; + $account = $repository->find($accountId); + if (null !== $account) { + // check currency here. + $accountCurrency = $repository->getAccountCurrency($account); + $isMultiCurrency = $repository->getMetaValue($account, 'is_multi_currency'); + if ($accountCurrency->id !== $currency->id && 'true' !== $isMultiCurrency) { + $validator->errors()->add('accounts', trans('validation.invalid_account_currency')); + } + $type = $account->accountType->type; + if (!in_array($type, $types, true)) { + $validator->errors()->add('accounts', trans('validation.invalid_account_type')); + } + } + } + } + } + ); + + if ($validator->fails()) { Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); } } + + private function getCurrencyFromData(array $data): TransactionCurrency + { + $currencyId = (int) ($data['transaction_currency_id'] ?? 0); + $currency = TransactionCurrency::find($currencyId); + if (null === $currency) { + return Amount::getDefaultCurrency(); + } + + return $currency; + } } diff --git a/app/Http/Requests/PreferencesRequest.php b/app/Http/Requests/PreferencesRequest.php new file mode 100644 index 0000000000..ca8236c464 --- /dev/null +++ b/app/Http/Requests/PreferencesRequest.php @@ -0,0 +1,52 @@ + ['nullable', 'url', 'min:1', new IsValidSlackOrDiscordUrl()], + 'ntfy_server' => ['nullable', 'url', 'min:1'], + 'ntfy_user' => ['required_with:ntfy_pass,ntfy_auth', 'nullable', 'string', 'min:1'], + 'ntfy_pass' => ['required_with:ntfy_user,ntfy_auth', 'nullable', 'string', 'min:1'], + ]; + foreach (config('notifications.notifications.user') as $key => $info) { + $rules[sprintf('notification_%s', $key)] = 'in:0,1'; + } + + return $rules; + } +} diff --git a/app/Http/Requests/RecurrenceFormRequest.php b/app/Http/Requests/RecurrenceFormRequest.php index 185f610490..dbb4c5d4e4 100644 --- a/app/Http/Requests/RecurrenceFormRequest.php +++ b/app/Http/Requests/RecurrenceFormRequest.php @@ -311,18 +311,18 @@ class RecurrenceFormRequest extends FormRequest $throwError = true; if ('withdrawal' === $type) { $throwError = false; - $sourceId = (int)$data['source_id']; - $destinationId = (int)$data['withdrawal_destination_id']; + $sourceId = (int) $data['source_id']; + $destinationId = (int) $data['withdrawal_destination_id']; } if ('deposit' === $type) { $throwError = false; - $sourceId = (int)$data['deposit_source_id']; - $destinationId = (int)($data['destination_id'] ?? 0); + $sourceId = (int) $data['deposit_source_id']; + $destinationId = (int) ($data['destination_id'] ?? 0); } if ('transfer' === $type) { $throwError = false; - $sourceId = (int)$data['source_id']; - $destinationId = (int)($data['destination_id'] ?? 0); + $sourceId = (int) $data['source_id']; + $destinationId = (int) ($data['destination_id'] ?? 0); } if (true === $throwError) { throw new FireflyException(sprintf('Cannot handle transaction type "%s"', $this->convertString('transaction_type'))); @@ -333,7 +333,7 @@ class RecurrenceFormRequest extends FormRequest // do something with result: if (false === $validSource) { - $message = (string)trans('validation.generic_invalid_source'); + $message = (string) trans('validation.generic_invalid_source'); $validator->errors()->add('source_id', $message); $validator->errors()->add('deposit_source_id', $message); @@ -344,7 +344,7 @@ class RecurrenceFormRequest extends FormRequest $validDestination = $accountValidator->validateDestination(['id' => $destinationId]); // do something with result: if (false === $validDestination) { - $message = (string)trans('validation.generic_invalid_destination'); + $message = (string) trans('validation.generic_invalid_destination'); $validator->errors()->add('destination_id', $message); $validator->errors()->add('withdrawal_destination_id', $message); } diff --git a/app/Http/Requests/ReportFormRequest.php b/app/Http/Requests/ReportFormRequest.php index 3a0977bbe7..da6c5e4417 100644 --- a/app/Http/Requests/ReportFormRequest.php +++ b/app/Http/Requests/ReportFormRequest.php @@ -55,7 +55,7 @@ class ReportFormRequest extends FormRequest $collection = new Collection(); if (is_array($set)) { foreach ($set as $accountId) { - $account = $repository->find((int)$accountId); + $account = $repository->find((int) $accountId); if (null !== $account) { $collection->push($account); } @@ -76,7 +76,7 @@ class ReportFormRequest extends FormRequest $collection = new Collection(); if (is_array($set)) { foreach ($set as $budgetId) { - $budget = $repository->find((int)$budgetId); + $budget = $repository->find((int) $budgetId); if (null !== $budget) { $collection->push($budget); } @@ -97,7 +97,7 @@ class ReportFormRequest extends FormRequest $collection = new Collection(); if (is_array($set)) { foreach ($set as $categoryId) { - $category = $repository->find((int)$categoryId); + $category = $repository->find((int) $categoryId); if (null !== $category) { $collection->push($category); } @@ -118,7 +118,7 @@ class ReportFormRequest extends FormRequest $collection = new Collection(); if (is_array($set)) { foreach ($set as $accountId) { - $account = $repository->find((int)$accountId); + $account = $repository->find((int) $accountId); if (null !== $account) { $collection->push($account); } @@ -137,7 +137,7 @@ class ReportFormRequest extends FormRequest { $date = today(config('app.timezone')); $range = $this->get('daterange'); - $parts = explode(' - ', (string)$range); + $parts = explode(' - ', (string) $range); if (2 === count($parts)) { $string = $parts[1]; // validate as date @@ -175,7 +175,7 @@ class ReportFormRequest extends FormRequest { $date = today(config('app.timezone')); $range = $this->get('daterange'); - $parts = explode(' - ', (string)$range); + $parts = explode(' - ', (string) $range); if (2 === count($parts)) { $string = $parts[0]; // validate as date @@ -229,7 +229,7 @@ class ReportFormRequest extends FormRequest continue; } - $tag = $repository->find((int)$tagTag); + $tag = $repository->find((int) $tagTag); if (null !== $tag) { $collection->push($tag); } diff --git a/app/Http/Requests/RuleFormRequest.php b/app/Http/Requests/RuleFormRequest.php index 29c2352a56..aadf8c7fee 100644 --- a/app/Http/Requests/RuleFormRequest.php +++ b/app/Http/Requests/RuleFormRequest.php @@ -70,8 +70,8 @@ class RuleFormRequest extends FormRequest $set = [ 'type' => $trigger['type'] ?? 'invalid', 'value' => $trigger['value'] ?? '', - 'stop_processing' => 1 === (int)$stopProcessing, - 'prohibited' => 1 === (int)$prohibited, + 'stop_processing' => 1 === (int) $stopProcessing, + 'prohibited' => 1 === (int) $prohibited, ]; $set = self::replaceAmountTrigger($set); $return[] = $set; @@ -116,7 +116,7 @@ class RuleFormRequest extends FormRequest $return[] = [ 'type' => $action['type'] ?? 'invalid', 'value' => $action['value'] ?? '', - 'stop_processing' => 1 === (int)$stopProcessing, + 'stop_processing' => 1 === (int) $stopProcessing, ]; } } @@ -144,7 +144,7 @@ class RuleFormRequest extends FormRequest 'description' => 'min:1|max:32768|nullable', 'stop_processing' => 'boolean', 'rule_group_id' => 'required|belongsToUser:rule_groups', - 'trigger' => 'required|in:store-journal,update-journal', + 'trigger' => 'required|in:store-journal,update-journal,manual-activation', 'triggers.*.type' => 'required|in:'.implode(',', $validTriggers), 'triggers.*.value' => sprintf('required_if:triggers.*.type,%s|max:1024|min:1|ruleTriggerValue', $contextTriggers), 'actions.*.type' => 'required|in:'.implode(',', $validActions), diff --git a/app/Jobs/CreateAutoBudgetLimits.php b/app/Jobs/CreateAutoBudgetLimits.php index ce0c15e173..e781cebb8a 100644 --- a/app/Jobs/CreateAutoBudgetLimits.php +++ b/app/Jobs/CreateAutoBudgetLimits.php @@ -126,7 +126,7 @@ class CreateAutoBudgetLimits implements ShouldQueue // find budget limit: $budgetLimit = $this->findBudgetLimit($autoBudget->budget, $start, $end); - if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_RESET === (int)$autoBudget->auto_budget_type) { + if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_RESET === (int) $autoBudget->auto_budget_type) { // that's easy: create one. // do nothing else. $this->createBudgetLimit($autoBudget, $start, $end); @@ -135,14 +135,14 @@ class CreateAutoBudgetLimits implements ShouldQueue return; } - if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ROLLOVER === (int)$autoBudget->auto_budget_type) { + if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ROLLOVER === (int) $autoBudget->auto_budget_type) { // budget limit exists already, $this->createRollover($autoBudget); app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id)); return; } - if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ADJUSTED === (int)$autoBudget->auto_budget_type) { + if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ADJUSTED === (int) $autoBudget->auto_budget_type) { // budget limit exists already, $this->createAdjustedLimit($autoBudget); app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id)); diff --git a/app/Jobs/CreateRecurringTransactions.php b/app/Jobs/CreateRecurringTransactions.php index 0efae2a1a9..540eff0ba9 100644 --- a/app/Jobs/CreateRecurringTransactions.php +++ b/app/Jobs/CreateRecurringTransactions.php @@ -441,7 +441,7 @@ class CreateRecurringTransactions implements ShouldQueue 'identifier' => $index, 'recurrence_id' => $recurrence->id, 'order' => $index, - 'notes' => (string)trans('firefly.created_from_recurrence', ['id' => $recurrence->id, 'title' => $recurrence->title]), + 'notes' => (string) trans('firefly.created_from_recurrence', ['id' => $recurrence->id, 'title' => $recurrence->title]), 'tags' => $this->repository->getTags($transaction), 'piggy_bank_id' => $this->repository->getPiggyBank($transaction), 'piggy_bank_name' => null, diff --git a/app/Jobs/DownloadExchangeRates.php b/app/Jobs/DownloadExchangeRates.php index e4a45a4bbd..5ba8d7f998 100644 --- a/app/Jobs/DownloadExchangeRates.php +++ b/app/Jobs/DownloadExchangeRates.php @@ -95,14 +95,14 @@ class DownloadExchangeRates implements ShouldQueue private function downloadRates(TransactionCurrency $currency): void { app('log')->debug(sprintf('Now downloading new exchange rates for currency %s.', $currency->code)); - $base = sprintf('%s/%s/%s', (string)config('cer.url'), $this->date->year, $this->date->isoWeek); + $base = sprintf('%s/%s/%s', (string) config('cer.url'), $this->date->year, $this->date->isoWeek); $client = new Client(); $url = sprintf('%s/%s.json', $base, $currency->code); try { $res = $client->get($url); } catch (ConnectException|RequestException $e) { - app('log')->warning(sprintf('Trying to grab "%s" resulted in error "%d".', $url, $e->getMessage())); + app('log')->warning(sprintf('Trying to grab "%s" resulted in error "%s".', $url, $e->getMessage())); return; } @@ -112,7 +112,7 @@ class DownloadExchangeRates implements ShouldQueue return; } - $body = (string)$res->getBody(); + $body = (string) $res->getBody(); $json = json_decode($body, true); if (false === $json || null === $json) { app('log')->warning(sprintf('Trying to grab "%s" resulted in bad JSON.', $url)); diff --git a/app/Jobs/MailError.php b/app/Jobs/MailError.php index 7d838228f8..16c9ad02a1 100644 --- a/app/Jobs/MailError.php +++ b/app/Jobs/MailError.php @@ -63,7 +63,7 @@ class MailError extends Job implements ShouldQueue */ public function handle(): void { - $email = (string)config('firefly.site_owner'); + $email = (string) config('firefly.site_owner'); $args = $this->exception; $args['loggedIn'] = $this->userData['id'] > 0; $args['user'] = $this->userData; @@ -84,7 +84,7 @@ class MailError extends Job implements ShouldQueue $args, static function (Message $message) use ($email): void { if ('mail@example.com' !== $email) { - $message->to($email, $email)->subject((string)trans('email.error_subject')); + $message->to($email, $email)->subject((string) trans('email.error_subject')); } } ); @@ -130,7 +130,7 @@ class MailError extends Job implements ShouldQueue } if (file_exists($file)) { Log::debug(sprintf('Read file in "%s"', $file)); - $limits = json_decode((string)file_get_contents($file), true); + $limits = json_decode((string) file_get_contents($file), true); } // limit reached? foreach ($types as $type => $info) { diff --git a/app/Jobs/WarnAboutBills.php b/app/Jobs/WarnAboutBills.php index 086a61407b..e79e05ea58 100644 --- a/app/Jobs/WarnAboutBills.php +++ b/app/Jobs/WarnAboutBills.php @@ -128,7 +128,7 @@ class WarnAboutBills implements ShouldQueue $today = clone $this->date; $carbon = clone $bill->{$field}; - return (int)$today->diffInDays($carbon); + return (int) $today->diffInDays($carbon); } private function sendWarning(Bill $bill, string $field): void diff --git a/app/JsonApi/Rules/IsValidFilter.php b/app/JsonApi/Rules/IsValidFilter.php deleted file mode 100644 index 9c8b0a4319..0000000000 --- a/app/JsonApi/Rules/IsValidFilter.php +++ /dev/null @@ -1,54 +0,0 @@ -allowed = $keys; - $this->allowed[] = 'user_group_id'; - } - - #[\Override] - public function validate(string $attribute, mixed $value, \Closure $fail): void - { - if ('filter' !== $attribute) { - $fail('validation.bad_api_filter')->translate(); - } - if (!is_array($value)) { - $value = explode(',', $value); - } - foreach ($value as $key => $val) { - if (!in_array($key, $this->allowed, true)) { - $fail('validation.bad_api_filter')->translate(['filter' => $key]); - } - } - } -} diff --git a/app/JsonApi/Rules/IsValidPage.php b/app/JsonApi/Rules/IsValidPage.php deleted file mode 100644 index b0c1182818..0000000000 --- a/app/JsonApi/Rules/IsValidPage.php +++ /dev/null @@ -1,53 +0,0 @@ -allowed = $keys; - } - - #[\Override] - public function validate(string $attribute, mixed $value, \Closure $fail): void - { - if ('page' !== $attribute) { - $fail('validation.bad_api_filter')->translate(); - } - if (!is_array($value)) { - $value = explode(',', $value); - } - foreach ($value as $key => $val) { - if (!in_array($key, $this->allowed, true)) { - $fail('validation.bad_api_page')->translate(); - } - } - } -} diff --git a/app/JsonApi/V2/AccountBalances/AccountBalanceResource.php b/app/JsonApi/V2/AccountBalances/AccountBalanceResource.php deleted file mode 100644 index e9d3520556..0000000000 --- a/app/JsonApi/V2/AccountBalances/AccountBalanceResource.php +++ /dev/null @@ -1,44 +0,0 @@ -resource->id; - } - - /** - * Get the resource's attributes. - * - * @param null|Request $request - */ - public function attributes($request): iterable - { - return [ - 'name' => $this->resource->amount, - 'amount' => $this->resource->amount, - ]; - } - - /** - * Get the resource's relationships. - * - * @param null|Request $request - */ - public function relationships($request): iterable - { - return [ - $this->relation('account')->withData($this->resource->getAccount()), - ]; - } -} diff --git a/app/JsonApi/V2/AccountBalances/AccountBalanceSchema.php b/app/JsonApi/V2/AccountBalances/AccountBalanceSchema.php deleted file mode 100644 index e0c2d13bb9..0000000000 --- a/app/JsonApi/V2/AccountBalances/AccountBalanceSchema.php +++ /dev/null @@ -1,50 +0,0 @@ -withServer($this->server) - ->withSchema($this) - ; - } -} diff --git a/app/JsonApi/V2/Accounts/AccountCollectionQuery.php b/app/JsonApi/V2/Accounts/AccountCollectionQuery.php deleted file mode 100644 index f736556a08..0000000000 --- a/app/JsonApi/V2/Accounts/AccountCollectionQuery.php +++ /dev/null @@ -1,77 +0,0 @@ - [ - 'nullable', - 'array', - JsonApiRule::fieldSets(), - ], - 'userGroupId' => [ - 'nullable', - 'integer', - new IsAllowedGroupAction(Account::class, request()->method()), - ], - 'startPeriod' => [ - 'nullable', - 'date', - new IsDateOrTime(), - new IsValidDateRange(), - ], - 'endPeriod' => [ - 'nullable', - 'date', - new IsDateOrTime(), - new IsValidDateRange(), - ], - 'filter' => [ - 'nullable', - 'array', - JsonApiRule::filter($validFilters), - new IsValidAccountType(), - ], - 'include' => [ - 'nullable', - 'string', - JsonApiRule::includePaths(), - ], - 'page' => [ - 'nullable', - 'array', - JsonApiRule::page(), - ], - 'sort' => [ - 'nullable', - 'string', - JsonApiRule::sort(), - ], - 'withCount' => [ - 'nullable', - 'string', - JsonApiRule::countable(), - ], - ]; - } -} diff --git a/app/JsonApi/V2/Accounts/AccountRepository.php b/app/JsonApi/V2/Accounts/AccountRepository.php deleted file mode 100644 index 039f52c355..0000000000 --- a/app/JsonApi/V2/Accounts/AccountRepository.php +++ /dev/null @@ -1,113 +0,0 @@ -enrichSingle($account); - } - - public function queryAll(): Capabilities\AccountQuery - { - Log::debug(__METHOD__); - - return Capabilities\AccountQuery::make() - ->withUserGroup($this->userGroup) - ->withServer($this->server) - ->withSchema($this->schema) - ; - } - - protected function crud(): Capabilities\CrudAccount - { - Log::debug(__METHOD__); - - return Capabilities\CrudAccount::make(); - } - - /** - * TODO piggy banks - * TODO transactions - */ - protected function relations(): CrudRelations - { - Log::debug(__METHOD__); - - return Capabilities\CrudAccountRelations::make(); - } -} diff --git a/app/JsonApi/V2/Accounts/AccountRequest.php b/app/JsonApi/V2/Accounts/AccountRequest.php deleted file mode 100644 index 8926e572bf..0000000000 --- a/app/JsonApi/V2/Accounts/AccountRequest.php +++ /dev/null @@ -1,58 +0,0 @@ -convertString('type'); - // var_dump($types);exit; - - return [ - 'name' => ['required', 'max:1024', 'min:1'], // , new IsUniqueAccount() - 'account_type' => ['required', 'max:1024', 'min:1', sprintf('in:%s', $types)], - // 'iban' => ['iban', 'nullable', new UniqueIban(null, $type)], - // 'bic' => 'bic|nullable', - // 'account_number' => ['min:1', 'max:255', 'nullable', new UniqueAccountNumber(null, $type)], - // 'opening_balance' => 'numeric|required_with:opening_balance_date|nullable', - // 'opening_balance_date' => 'date|required_with:opening_balance|nullable', - // 'virtual_balance' => 'numeric|nullable', - // 'order' => 'numeric|nullable', - // 'currency_id' => 'numeric|exists:transaction_currencies,id', - // 'currency_code' => 'min:3|max:3|exists:transaction_currencies,code', - // 'active' => [new IsBoolean()], - // 'include_net_worth' => [new IsBoolean()], - // 'account_role' => sprintf('nullable|in:%s|required_if:type,asset', $accountRoles), - // 'credit_card_type' => sprintf('nullable|in:%s|required_if:account_role,ccAsset', $ccPaymentTypes), - // 'monthly_payment_date' => 'nullable|date|required_if:account_role,ccAsset|required_if:credit_card_type,monthlyFull', - // 'liability_type' => 'nullable|required_if:type,liability|required_if:type,liabilities|in:loan,debt,mortgage', - // 'liability_amount' => ['required_with:liability_start_date', new IsValidPositiveAmount()], - // 'liability_start_date' => 'required_with:liability_amount|date', - // 'liability_direction' => 'nullable|required_if:type,liability|required_if:type,liabilities|in:credit,debit', - // 'interest' => 'min:0|max:100|numeric', - // 'interest_period' => sprintf('nullable|in:%s', implode(',', config('firefly.interest_periods'))), - // 'notes' => 'min:0|max:32768', - ]; - } -} diff --git a/app/JsonApi/V2/Accounts/AccountResource.php b/app/JsonApi/V2/Accounts/AccountResource.php deleted file mode 100644 index 9ffa40aa1a..0000000000 --- a/app/JsonApi/V2/Accounts/AccountResource.php +++ /dev/null @@ -1,83 +0,0 @@ -resource->id; - } - - /** - * Get the resource's attributes. - * - * @param null|Request $request - */ - public function attributes($request): iterable - { - // Log::debug(__METHOD__); - - return [ - 'created_at' => $this->resource->created_at, - 'updated_at' => $this->resource->updated_at, - 'name' => $this->resource->name, - 'active' => $this->resource->active, - 'order' => $this->resource->order, - 'iban' => $this->resource->iban, - 'account_type' => $this->resource->account_type_string, - 'account_role' => $this->resource->account_role, - 'account_number' => '' === $this->resource->account_number ? null : $this->resource->account_number, - - // currency (if the account has a currency setting, otherwise NULL). - 'currency_id' => $this->resource->currency_id, - 'currency_name' => $this->resource->currency_name, - 'currency_code' => $this->resource->currency_code, - 'currency_symbol' => $this->resource->currency_symbol, - 'currency_decimal_places' => $this->resource->currency_decimal_places, - 'is_multi_currency' => '1' === $this->resource->is_multi_currency, - - // balances - 'balance' => $this->resource->balance, - 'native_balance' => $this->resource->native_balance, - - // liability things - 'liability_direction' => $this->resource->liability_direction, - 'interest' => $this->resource->interest, - 'interest_period' => $this->resource->interest_period, - 'current_debt' => $this->resource->current_debt, // TODO may be removed in the future. - - // other things - 'last_activity' => $this->resource->last_activity, - - // object group - 'object_group_id' => $this->resource->object_group_id, - 'object_group_title' => $this->resource->object_group_title, - 'object_group_order' => $this->resource->object_group_order, - ]; - } - - /** - * Get the resource's relationships. - * - * @param null|Request $request - */ - public function relationships($request): iterable - { - return [ - $this->relation('user')->withData($this->resource->user), - ]; - } -} diff --git a/app/JsonApi/V2/Accounts/AccountResourceOld.php b/app/JsonApi/V2/Accounts/AccountResourceOld.php deleted file mode 100644 index b26fb7380f..0000000000 --- a/app/JsonApi/V2/Accounts/AccountResourceOld.php +++ /dev/null @@ -1,115 +0,0 @@ - $this->resource->created_at, - 'updated_at' => $this->resource->updated_at, - 'name' => $this->resource->name, - - // 'virtual_balance' => $this->resource->virtual_balance, - // 'native_balance' => $this->resource->native_balance, - // 'user' => $this->resource->user_array, - // 'balances' => [] - // - - // balance (in currency, on date) - // 'current_balance' => $this->resource->current_balance, - // 'current_balance' => app('steam')->bcround(app('steam')->balance($account, $date), $decimalPlaces), - // 'current_balance_date' => $date->toAtomString(), - - // 'notes' => $this->repository->getNoteText($account), - // 'monthly_payment_date' => $monthlyPaymentDate, - // 'credit_card_type' => $creditCardType, - // 'account_number' => $this->repository->getMetaValue($account, 'account_number'), - // 'bic' => $this->repository->getMetaValue($account, 'BIC'), - // 'opening_balance' => $openingBalance, - // 'opening_balance_date' => $openingBalanceDate, - // 'liability_type' => $liabilityType, - // 'liability_direction' => $liabilityDirection, - // 'interest' => $interest, - // 'interest_period' => $interestPeriod, - // 'current_debt' => $this->repository->getMetaValue($account, 'current_debt'), - // 'include_net_worth' => $includeNetWorth, - // 'longitude' => $longitude, - // 'latitude' => $latitude, - // 'zoom_level' => $zoomLevel, - - // 'order' => $order, - - // 'native_currency_id' => (string) $this->default->id, - // 'native_currency_code' => $this->default->code, - // 'native_currency_symbol' => $this->default->symbol, - // 'native_currency_decimal_places' => $this->default->decimal_places, - // - // // balance: - // 'current_balance' => $balance, - // 'native_current_balance' => $nativeBalance, - // 'current_balance_date' => $this->getDate()->endOfDay()->toAtomString(), - // - // // balance difference - // 'balance_difference' => $balanceDiff, - // 'native_balance_difference' => $nativeBalanceDiff, - // 'balance_difference_start' => $diffStart, - // 'balance_difference_end' => $diffEnd, - // - // - // // liability stuff - // 'liability_type' => $liabilityType, - // - // // object group - // 'object_group_id' => null !== $objectGroupId ? (string) $objectGroupId : null, - // 'object_group_order' => $objectGroupOrder, - // 'object_group_title' => $objectGroupTitle, - // 'notes' => $this->repository->getNoteText($account), - // 'monthly_payment_date' => $monthlyPaymentDate, - // 'credit_card_type' => $creditCardType, - // 'bic' => $this->repository->getMetaValue($account, 'BIC'), - // 'virtual_balance' => number_format((float) $account->virtual_balance, $decimalPlaces, '.', ''), - // 'opening_balance' => $openingBalance, - // 'opening_balance_date' => $openingBalanceDate, - // 'include_net_worth' => $includeNetWorth, - // 'longitude' => $longitude, - // 'latitude' => $latitude, - // 'zoom_level' => $zoomLevel, - ]; - } - - /** - * Get the resource's relationships. - * - * @param null|Request $request - */ - public function relationships($request): iterable - { - return [ - $this->relation('user')->withData($this->resource->user), - $this->relation('currency')->withData($this->resource->transactionCurrency), - // $this->relation('account_balances')->withData($this->resource->balances), - ]; - } -} diff --git a/app/JsonApi/V2/Accounts/AccountSchema.php b/app/JsonApi/V2/Accounts/AccountSchema.php deleted file mode 100644 index 33a010992c..0000000000 --- a/app/JsonApi/V2/Accounts/AccountSchema.php +++ /dev/null @@ -1,112 +0,0 @@ -sortable(), - Attribute::make('active')->sortable(), - Attribute::make('order')->sortable(), - Attribute::make('iban')->sortable(), - Attribute::make('account_type'), - Attribute::make('account_role'), - Attribute::make('account_number')->sortable(), - - // currency - Attribute::make('currency_id'), - Attribute::make('currency_name'), - Attribute::make('currency_code'), - Attribute::make('currency_symbol'), - Attribute::make('currency_decimal_places'), - Attribute::make('is_multi_currency'), - - // balance - Attribute::make('balance')->sortable(), - Attribute::make('native_balance')->sortable(), - - // liability things - Attribute::make('liability_direction'), - Attribute::make('interest'), - Attribute::make('interest_period'), - // Attribute::make('current_debt')->sortable(), - - // TODO credit card fields. - - // dynamic data - Attribute::make('last_activity')->sortable(), - Attribute::make('balance_difference')->sortable(), // only used for sort. - - // group - Attribute::make('object_group_id'), - Attribute::make('object_group_title'), - Attribute::make('object_group_order'), - - // relations. - HasOne::make('user')->readOnly(), - ]; - } - - /** - * Get the resource filters. - */ - public function filters(): array - { - Log::debug(__METHOD__); - $array = []; - $config = config('api.valid_api_filters')[Account::class]; - foreach ($config as $entry) { - $array[] = Filter::make($entry); - } - - return $array; - } - - public function repository(): AccountRepository - { - Log::debug(__METHOD__); - $this->setUserGroup($this->server->getUsergroup()); - - return AccountRepository::make() - ->withServer($this->server) - ->withSchema($this) - ->withUserGroup($this->userGroup) - ; - } - - public function pagination(): EnumerablePagination - { - Log::debug(__METHOD__); - - return EnumerablePagination::make(); - } -} diff --git a/app/JsonApi/V2/Accounts/AccountSchemaOld.php b/app/JsonApi/V2/Accounts/AccountSchemaOld.php deleted file mode 100644 index 748f40891d..0000000000 --- a/app/JsonApi/V2/Accounts/AccountSchemaOld.php +++ /dev/null @@ -1,72 +0,0 @@ -sortable()->readOnly(), - DateTime::make('updated_at')->sortable()->readOnly(), - Str::make('name')->sortable(), - // Str::make('account_type'), - // Str::make('virtual_balance'), - // Str::make('iban'), - // Boolean::make('active'), - // Number::make('order'), - HasOne::make('user')->readOnly(), - // HasMany::make('account_balances'), - ]; - } - - /** - * Filters mentioned here can be used to filter the results. - * TODO write down exactly how this works. - */ - public function filters(): array - { - return [ - WhereIdIn::make($this), - ]; - } - - /** - * Get the resource paginator. - */ - public function pagination(): ?Paginator - { - return PagePagination::make(); - } -} diff --git a/app/JsonApi/V2/Accounts/AccountSingleQuery.php b/app/JsonApi/V2/Accounts/AccountSingleQuery.php deleted file mode 100644 index b6729dc444..0000000000 --- a/app/JsonApi/V2/Accounts/AccountSingleQuery.php +++ /dev/null @@ -1,45 +0,0 @@ - [ - 'nullable', - 'array', - JsonApiRule::fieldSets(), - ], - 'filter' => [ - 'nullable', - 'array', - JsonApiRule::filter()->forget('id'), - ], - 'include' => [ - 'nullable', - 'string', - JsonApiRule::includePaths(), - ], - 'page' => JsonApiRule::notSupported(), - 'sort' => JsonApiRule::notSupported(), - 'withCount' => [ - 'nullable', - 'string', - JsonApiRule::countable(), - ], - ]; - } -} diff --git a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php deleted file mode 100644 index 65ab00a8ca..0000000000 --- a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php +++ /dev/null @@ -1,142 +0,0 @@ -queryParameters->sortFields(); - - // collect pagination based on the page - $pagination = $this->filtersPagination($this->queryParameters->page()); - - // check if we need all accounts, regardless of pagination - // This is necessary when the user wants to sort on specific params. - $needsAll = $this->needsFullDataset(Account::class, $sort); - - // params that were not recognised, may be my own custom stuff. - $otherParams = $this->getOtherParams($this->queryParameters->unrecognisedParameters()); - - // start the query - $query = $this->userGroup->accounts(); - - // add sort and filter parameters to the query. - $query = $this->addSortParams(Account::class, $query, $sort); - $query = $this->addFilterParams(Account::class, $query, $this->queryParameters->filter()); - - // collect the result. - $collection = $query->get(['accounts.*']); - // sort the data after the query, and return it right away. - $collection = $this->sortCollection(Account::class, $collection, $sort); - - // if the entire collection needs to be enriched and sorted, do so now: - $totalCount = $collection->count(); - Log::debug(sprintf('Total is %d', $totalCount)); - if ($needsAll) { - Log::debug('Needs the entire collection'); - // enrich the entire collection - $enrichment = new AccountEnrichment(); - $enrichment->setStart($otherParams['start'] ?? null); - $enrichment->setEnd($otherParams['end'] ?? null); - $collection = $enrichment->enrich($collection); - - // TODO sort the set based on post-query sort options: - $collection = $this->postQuerySort(Account::class, $collection, $sort); - - // take the current page from the enriched set. - $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); - } - if (!$needsAll) { - Log::debug('Needs only partial collection'); - // take from the collection the filtered page + page number: - $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); - - // enrich only the current page. - $enrichment = new AccountEnrichment(); - $enrichment->setStart($otherParams['start'] ?? null); - $enrichment->setEnd($otherParams['end'] ?? null); - $currentPage = $enrichment->enrich($currentPage); - } - // get current page? - Log::debug(sprintf('Skip %d, take %d', ($pagination['number'] - 1) * $pagination['size'], $pagination['size'])); - // $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); - Log::debug(sprintf('New collection size: %d', $currentPage->count())); - - // TODO add filters after the query, if there are filters that cannot be applied to the database - // TODO same for sort things. - - return new LengthAwarePaginator($currentPage, $totalCount, $pagination['size'], $pagination['number']); - } - - #[\Override] - public function paginate(array $page): Page - { - exit('here weare'); - // TODO: Implement paginate() method. - } - - #[\Override] - public function getOrPaginate(?array $page): iterable - { - exit('here weare'); - // TODO: Implement getOrPaginate() method. - } -} diff --git a/app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php b/app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php deleted file mode 100644 index 0c660d1e91..0000000000 --- a/app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php +++ /dev/null @@ -1,61 +0,0 @@ -getOtherParams($this->request->query->all()); - - Log::debug(__METHOD__); - // enrich the collected data - $enrichment = new AccountEnrichment(); - - // set start and date, if present. - $enrichment->setStart($otherParams['start'] ?? null); - $enrichment->setEnd($otherParams['end'] ?? null); - - return $enrichment->enrichSingle($account); - } - - public function create(array $validatedData): Account - { - var_dump($validatedData); - - exit; - } -} diff --git a/app/JsonApi/V2/Server.php b/app/JsonApi/V2/Server.php deleted file mode 100644 index e175deb8ef..0000000000 --- a/app/JsonApi/V2/Server.php +++ /dev/null @@ -1,53 +0,0 @@ -detectUserGroup(); - $this->setUserGroup($res); - } - - /** - * Get the server's list of schemas. - */ - protected function allSchemas(): array - { - // Log::debug(__METHOD__); - - return [ - AccountSchema::class, - UserSchema::class, - // AccountBalanceSchema::class, - ]; - } -} diff --git a/app/JsonApi/V2/Users/UserResource.php b/app/JsonApi/V2/Users/UserResource.php deleted file mode 100644 index 137325e65d..0000000000 --- a/app/JsonApi/V2/Users/UserResource.php +++ /dev/null @@ -1,41 +0,0 @@ - $this->resource->created_at, - 'updated_at' => $this->resource->updated_at, - 'email' => $this->resource->email, - ]; - } - - /** - * Get the resource's relationships. - * - * @param null|Request $request - */ - public function relationships($request): iterable - { - return [ - // @TODO - ]; - } -} diff --git a/app/JsonApi/V2/Users/UserSchema.php b/app/JsonApi/V2/Users/UserSchema.php deleted file mode 100644 index 8a32b34676..0000000000 --- a/app/JsonApi/V2/Users/UserSchema.php +++ /dev/null @@ -1,55 +0,0 @@ -sortable()->readOnly(), - DateTime::make('updated_at')->sortable()->readOnly(), - Str::make('email'), - HasMany::make('accounts'), - ]; - } - - /** - * Get the resource filters. - */ - public function filters(): array - { - return [ - WhereIdIn::make($this), - ]; - } - - /** - * Get the resource paginator. - */ - public function pagination(): ?Paginator - { - return PagePagination::make(); - } -} diff --git a/app/Mail/AccessTokenCreatedMail.php b/app/Mail/AccessTokenCreatedMail.php index a48bbcc900..12deb375c4 100644 --- a/app/Mail/AccessTokenCreatedMail.php +++ b/app/Mail/AccessTokenCreatedMail.php @@ -50,7 +50,7 @@ class AccessTokenCreatedMail extends Mailable { return $this ->markdown('emails.token-created') - ->subject((string)trans('email.access_token_created_subject')) + ->subject((string) trans('email.access_token_created_subject')) ; } } diff --git a/app/Mail/AdminTestMail.php b/app/Mail/AdminTestMail.php index 1971dce7dc..a24f87d214 100644 --- a/app/Mail/AdminTestMail.php +++ b/app/Mail/AdminTestMail.php @@ -53,7 +53,7 @@ class AdminTestMail extends Mailable { return $this ->markdown('emails.admin-test') - ->subject((string)trans('email.admin_test_subject')) + ->subject((string) trans('email.admin_test_subject')) ; } } diff --git a/app/Mail/BillWarningMail.php b/app/Mail/BillWarningMail.php index 588772148f..ac16d4a1de 100644 --- a/app/Mail/BillWarningMail.php +++ b/app/Mail/BillWarningMail.php @@ -58,9 +58,9 @@ class BillWarningMail extends Mailable */ public function build(): self { - $subject = (string)trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); + $subject = (string) trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); if (0 === $this->diff) { - $subject = (string)trans(sprintf('email.bill_warning_subject_now_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); + $subject = (string) trans(sprintf('email.bill_warning_subject_now_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); } return $this diff --git a/app/Mail/ConfirmEmailChangeMail.php b/app/Mail/ConfirmEmailChangeMail.php index d4346eff6c..d5161e2a66 100644 --- a/app/Mail/ConfirmEmailChangeMail.php +++ b/app/Mail/ConfirmEmailChangeMail.php @@ -62,7 +62,7 @@ class ConfirmEmailChangeMail extends Mailable // ->view('emails.confirm-email-change-html') // ->text('emails.confirm-email-change-text') ->markdown('emails.confirm-email-change') - ->subject((string)trans('email.email_change_subject')) + ->subject((string) trans('email.email_change_subject')) ; } } diff --git a/app/Mail/InvitationMail.php b/app/Mail/InvitationMail.php index 151e7d423a..36bae2e7bb 100644 --- a/app/Mail/InvitationMail.php +++ b/app/Mail/InvitationMail.php @@ -49,7 +49,7 @@ class InvitationMail extends Mailable $this->invitee = $invitee; $this->admin = $admin; $this->url = $url; - $this->host = (string)parse_url($url, PHP_URL_HOST); + $this->host = (string) parse_url($url, PHP_URL_HOST); } /** @@ -61,7 +61,7 @@ class InvitationMail extends Mailable { return $this ->markdown('emails.invitation') - ->subject((string)trans('email.invite_user_subject')) + ->subject((string) trans('email.invite_user_subject')) ; } } diff --git a/app/Mail/NewIPAddressWarningMail.php b/app/Mail/NewIPAddressWarningMail.php index e8fdf5ea86..a1a311977d 100644 --- a/app/Mail/NewIPAddressWarningMail.php +++ b/app/Mail/NewIPAddressWarningMail.php @@ -56,7 +56,7 @@ class NewIPAddressWarningMail extends Mailable */ public function build(): self { - $this->time = now(config('app.timezone'))->isoFormat((string)trans('config.date_time_js')); + $this->time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); $this->host = ''; try { @@ -71,7 +71,7 @@ class NewIPAddressWarningMail extends Mailable return $this ->markdown('emails.new-ip') - ->subject((string)trans('email.login_from_new_ip')) + ->subject((string) trans('email.login_from_new_ip')) ; } } diff --git a/app/Mail/OAuthTokenCreatedMail.php b/app/Mail/OAuthTokenCreatedMail.php index e6247d3475..3c252b6ad6 100644 --- a/app/Mail/OAuthTokenCreatedMail.php +++ b/app/Mail/OAuthTokenCreatedMail.php @@ -56,7 +56,7 @@ class OAuthTokenCreatedMail extends Mailable { return $this ->markdown('emails.oauth-client-created') - ->subject((string)trans('email.oauth_created_subject')) + ->subject((string) trans('email.oauth_created_subject')) ; } } diff --git a/app/Mail/RegisteredUser.php b/app/Mail/RegisteredUser.php index 8659c8e3f9..fbbe6d2aaa 100644 --- a/app/Mail/RegisteredUser.php +++ b/app/Mail/RegisteredUser.php @@ -56,7 +56,7 @@ class RegisteredUser extends Mailable { return $this ->markdown('emails.registered') - ->subject((string)trans('email.registered_subject')) + ->subject((string) trans('email.registered_subject')) ; } } diff --git a/app/Mail/RequestedNewPassword.php b/app/Mail/RequestedNewPassword.php index 0be46c5251..8d1342908e 100644 --- a/app/Mail/RequestedNewPassword.php +++ b/app/Mail/RequestedNewPassword.php @@ -55,7 +55,7 @@ class RequestedNewPassword extends Mailable { return $this ->markdown('emails.password') - ->subject((string)trans('email.reset_pw_subject')) + ->subject((string) trans('email.reset_pw_subject')) ; } } diff --git a/app/Mail/UndoEmailChangeMail.php b/app/Mail/UndoEmailChangeMail.php index 70d243685a..5f676b24d3 100644 --- a/app/Mail/UndoEmailChangeMail.php +++ b/app/Mail/UndoEmailChangeMail.php @@ -58,7 +58,7 @@ class UndoEmailChangeMail extends Mailable { return $this ->markdown('emails.undo-email-change') - ->subject((string)trans('email.email_change_subject')) + ->subject((string) trans('email.email_change_subject')) ; } } diff --git a/app/Models/Account.php b/app/Models/Account.php index 39b5e1f8ed..eede2e7f38 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -26,12 +26,12 @@ namespace FireflyIII\Models; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; -use GeneaLabs\LaravelModelCaching\Traits\Cachable; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphToMany; @@ -43,7 +43,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; */ class Account extends Model { - use Cachable; use HasFactory; use ReturnsIntegerIdTrait; use ReturnsIntegerUserIdTrait; @@ -51,16 +50,17 @@ class Account extends Model protected $casts = [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'user_id' => 'integer', - 'deleted_at' => 'datetime', - 'active' => 'boolean', - 'encrypted' => 'boolean', - 'virtual_balance' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'user_id' => 'integer', + 'deleted_at' => 'datetime', + 'active' => 'boolean', + 'encrypted' => 'boolean', + 'virtual_balance' => 'string', + 'native_virtual_balance' => 'string', ]; - protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban']; + protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban', 'native_virtual_balance']; protected $hidden = ['encrypted']; private bool $joinedAccountTypes = false; @@ -93,6 +93,11 @@ class Account extends Model return $this->belongsTo(User::class); } + public function accountBalances(): HasMany + { + return $this->hasMany(AccountBalance::class); + } + public function accountType(): BelongsTo { return $this->belongsTo(AccountType::class); @@ -122,11 +127,6 @@ class Account extends Model return $this->hasMany(AccountMeta::class); } - public function accountBalances(): HasMany - { - return $this->hasMany(AccountBalance::class); - } - public function getEditNameAttribute(): string { $name = $this->name; @@ -144,7 +144,7 @@ class Account extends Model } /** - * Get all of the notes. + * Get all the notes. */ public function notes(): MorphMany { @@ -159,9 +159,9 @@ class Account extends Model return $this->morphToMany(ObjectGroup::class, 'object_groupable'); } - public function piggyBanks(): HasMany + public function piggyBanks(): BelongsToMany { - return $this->hasMany(PiggyBank::class); + return $this->belongsToMany(PiggyBank::class); } public function scopeAccountTypeIn(EloquentBuilder $query, array $types): void diff --git a/app/Models/AccountBalance.php b/app/Models/AccountBalance.php index c1d6ccb6e8..10fed8cea3 100644 --- a/app/Models/AccountBalance.php +++ b/app/Models/AccountBalance.php @@ -15,15 +15,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class AccountBalance extends Model { use HasFactory; - protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance', 'date', 'date_tz']; - protected function casts(): array - { - return [ - 'date' => SeparateTimezoneCaster::class, - 'balance' => 'string', - ]; - } + protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance', 'date', 'date_tz']; public function account(): BelongsTo { @@ -34,4 +27,12 @@ class AccountBalance extends Model { return $this->belongsTo(TransactionCurrency::class); } + + protected function casts(): array + { + return [ + 'date' => SeparateTimezoneCaster::class, + 'balance' => 'string', + ]; + } } diff --git a/app/Models/AccountMeta.php b/app/Models/AccountMeta.php index 4beb609874..23c2a340d0 100644 --- a/app/Models/AccountMeta.php +++ b/app/Models/AccountMeta.php @@ -52,7 +52,7 @@ class AccountMeta extends Model public function getDataAttribute(mixed $value): string { - return (string)json_decode($value, true); + return (string) json_decode($value, true); } public function setDataAttribute(mixed $value): void diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index 013b7269bd..d5eda3c24e 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Enums\AccountTypeEnum; +use Deprecated; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -35,46 +35,46 @@ class AccountType extends Model { use ReturnsIntegerIdTrait; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string ASSET = 'Asset account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string BENEFICIARY = 'Beneficiary account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string CASH = 'Cash account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string CREDITCARD = 'Credit card'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string DEBT = 'Debt'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string DEFAULT = 'Default account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string EXPENSE = 'Expense account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string IMPORT = 'Import account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string INITIAL_BALANCE = 'Initial balance account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string LIABILITY_CREDIT = 'Liability credit account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string LOAN = 'Loan'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string MORTGAGE = 'Mortgage'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string RECONCILIATION = 'Reconciliation account'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string REVENUE = 'Revenue account'; protected $casts diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 21552e0a19..4b910caf3a 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -61,7 +61,7 @@ class Attachment extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $attachmentId = (int)$value; + $attachmentId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -94,7 +94,7 @@ class Attachment extends Model */ public function fileName(): string { - return sprintf('at-%s.data', (string)$this->id); + return sprintf('at-%s.data', (string) $this->id); } /** @@ -108,7 +108,7 @@ class Attachment extends Model protected function attachableId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/AuditLogEntry.php b/app/Models/AuditLogEntry.php index 9490d7302e..b754804345 100644 --- a/app/Models/AuditLogEntry.php +++ b/app/Models/AuditLogEntry.php @@ -60,14 +60,14 @@ class AuditLogEntry extends Model protected function auditableId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function changerId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index b67ca3be23..7eea5746e1 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Enums\AutoBudgetType; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; @@ -39,19 +38,20 @@ class AutoBudget extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const int AUTO_BUDGET_ADJUSTED = 3; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const int AUTO_BUDGET_RESET = 1; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const int AUTO_BUDGET_ROLLOVER = 2; - protected $fillable = ['budget_id', 'amount', 'period']; - - protected $casts = [ - 'amount' => 'string', - ]; + protected $casts + = [ + 'amount' => 'string', + 'native_amount' => 'string', + ]; + protected $fillable = ['budget_id', 'amount', 'period', 'native_amount']; public function budget(): BelongsTo { @@ -63,6 +63,20 @@ class AutoBudget extends Model return $this->belongsTo(TransactionCurrency::class); } + protected function amount(): Attribute + { + return Attribute::make( + get: static fn ($value) => (string) $value, + ); + } + + protected function budgetId(): Attribute + { + return Attribute::make( + get: static fn ($value) => (int) $value, + ); + } + protected function casts(): array { return [ @@ -70,24 +84,10 @@ class AutoBudget extends Model ]; } - protected function amount(): Attribute - { - return Attribute::make( - get: static fn ($value) => (string)$value, - ); - } - - protected function budgetId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int)$value, - ); - } - protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php index 47d149637b..45e2eb91bb 100644 --- a/app/Models/AvailableBudget.php +++ b/app/Models/AvailableBudget.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; @@ -50,9 +51,10 @@ class AvailableBudget extends Model 'end_date' => 'date', 'transaction_currency_id' => 'int', 'amount' => 'string', + 'native_amount' => 'string', ]; - protected $fillable = ['user_id', 'user_group_id', 'transaction_currency_id', 'amount', 'start_date', 'end_date', 'start_date_tz', 'end_date_tz']; + protected $fillable = ['user_id', 'user_group_id', 'transaction_currency_id', 'amount', 'start_date', 'end_date', 'start_date_tz', 'end_date_tz', 'native_amount']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -62,7 +64,7 @@ class AvailableBudget extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $availableBudgetId = (int)$value; + $availableBudgetId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -90,14 +92,30 @@ class AvailableBudget extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, + ); + } + + protected function startDate(): Attribute + { + return Attribute::make( + get: fn (string $value) => Carbon::parse($value), + set: fn (Carbon $value) => $value->format('Y-m-d'), + ); + } + + protected function endDate(): Attribute + { + return Attribute::make( + get: fn (string $value) => Carbon::parse($value), + set: fn (Carbon $value) => $value->format('Y-m-d'), ); } } diff --git a/app/Models/Bill.php b/app/Models/Bill.php index 9d41b9c5bc..34b934a62a 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -47,19 +47,21 @@ class Bill extends Model protected $casts = [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', - 'date' => SeparateTimezoneCaster::class, - 'end_date' => SeparateTimezoneCaster::class, - 'extension_date' => SeparateTimezoneCaster::class, - 'skip' => 'int', - 'automatch' => 'boolean', - 'active' => 'boolean', - 'name_encrypted' => 'boolean', - 'match_encrypted' => 'boolean', - 'amount_min' => 'string', - 'amount_max' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'date' => SeparateTimezoneCaster::class, + 'end_date' => SeparateTimezoneCaster::class, + 'extension_date' => SeparateTimezoneCaster::class, + 'skip' => 'int', + 'automatch' => 'boolean', + 'active' => 'boolean', + 'name_encrypted' => 'boolean', + 'match_encrypted' => 'boolean', + 'amount_min' => 'string', + 'amount_max' => 'string', + 'native_amount_min' => 'string', + 'native_amount_max' => 'string', ]; protected $fillable @@ -81,6 +83,8 @@ class Bill extends Model 'extension_date', 'end_date_tz', 'extension_date_tz', + 'native_amount_min', + 'native_amount_max', ]; protected $hidden = ['amount_min_encrypted', 'amount_max_encrypted', 'name_encrypted', 'match_encrypted']; @@ -93,7 +97,7 @@ class Bill extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $billId = (int)$value; + $billId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -139,7 +143,7 @@ class Bill extends Model */ public function setAmountMaxAttribute($value): void { - $this->attributes['amount_max'] = (string)$value; + $this->attributes['amount_max'] = (string) $value; } /** @@ -147,7 +151,7 @@ class Bill extends Model */ public function setAmountMinAttribute($value): void { - $this->attributes['amount_min'] = (string)$value; + $this->attributes['amount_min'] = (string) $value; } public function transactionCurrency(): BelongsTo @@ -166,7 +170,7 @@ class Bill extends Model protected function amountMax(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } @@ -176,14 +180,14 @@ class Bill extends Model protected function amountMin(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } @@ -193,14 +197,14 @@ class Bill extends Model protected function skip(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 6293c5de8e..e131aead22 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -65,7 +65,7 @@ class Budget extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $budgetId = (int)$value; + $budgetId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -121,7 +121,7 @@ class Budget extends Model protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index 22f3c0de2d..375b9e6b6c 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -31,6 +31,7 @@ use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -42,12 +43,13 @@ class BudgetLimit extends Model protected $casts = [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'start_date' => SeparateTimezoneCaster::class, - 'end_date' => SeparateTimezoneCaster::class, - 'auto_budget' => 'boolean', - 'amount' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'start_date' => SeparateTimezoneCaster::class, + 'end_date' => SeparateTimezoneCaster::class, + 'auto_budget' => 'boolean', + 'amount' => 'string', + 'native_amount' => 'string', ]; protected $dispatchesEvents = [ @@ -56,7 +58,7 @@ class BudgetLimit extends Model 'deleted' => Deleted::class, ]; - protected $fillable = ['budget_id', 'start_date', 'end_date', 'start_date_tz', 'end_date_tz', 'amount', 'transaction_currency_id']; + protected $fillable = ['budget_id', 'start_date', 'end_date', 'start_date_tz', 'end_date_tz', 'amount', 'transaction_currency_id', 'native_amount']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -66,7 +68,7 @@ class BudgetLimit extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $budgetLimitId = (int)$value; + $budgetLimitId = (int) $value; $budgetLimit = self::where('budget_limits.id', $budgetLimitId) ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') ->where('budgets.user_id', auth()->user()->id) @@ -85,6 +87,14 @@ class BudgetLimit extends Model return $this->belongsTo(Budget::class); } + /** + * Get all the notes. + */ + public function notes(): MorphMany + { + return $this->morphMany(Note::class, 'noteable'); + } + public function transactionCurrency(): BelongsTo { return $this->belongsTo(TransactionCurrency::class); @@ -96,21 +106,21 @@ class BudgetLimit extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function budgetId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/Category.php b/app/Models/Category.php index 2cb4680c7a..11eb99b3ff 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -63,7 +63,7 @@ class Category extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $categoryId = (int)$value; + $categoryId = (int) $value; /** @var User $user */ $user = auth()->user(); diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index 2bd2dcbb2f..1bbcee0e15 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -72,28 +72,28 @@ class CurrencyExchangeRate extends Model protected function fromCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function rate(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function toCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function userRate(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } } diff --git a/app/Models/GroupMembership.php b/app/Models/GroupMembership.php index 208476f09d..99bd626cab 100644 --- a/app/Models/GroupMembership.php +++ b/app/Models/GroupMembership.php @@ -59,7 +59,7 @@ class GroupMembership extends Model protected function userRoleId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php index f0634c27db..c6ffed23e0 100644 --- a/app/Models/InvitedUser.php +++ b/app/Models/InvitedUser.php @@ -53,7 +53,7 @@ class InvitedUser extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $attemptId = (int)$value; + $attemptId = (int) $value; /** @var null|InvitedUser $attempt */ $attempt = self::find($attemptId); diff --git a/app/Models/LinkType.php b/app/Models/LinkType.php index 23f4982e08..ce7a0a9558 100644 --- a/app/Models/LinkType.php +++ b/app/Models/LinkType.php @@ -55,7 +55,7 @@ class LinkType extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $linkTypeId = (int)$value; + $linkTypeId = (int) $value; $linkType = self::find($linkTypeId); if (null !== $linkType) { return $linkType; diff --git a/app/Models/Location.php b/app/Models/Location.php index fc5d4639c3..31f97af7bb 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -82,7 +82,7 @@ class Location extends Model protected function locatableId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/Note.php b/app/Models/Note.php index 5afbfbed76..e0ce8a0e2b 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -57,7 +57,7 @@ class Note extends Model protected function noteableId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/ObjectGroup.php b/app/Models/ObjectGroup.php index 7ec9a91ad3..1eebdba7d8 100644 --- a/app/Models/ObjectGroup.php +++ b/app/Models/ObjectGroup.php @@ -58,7 +58,7 @@ class ObjectGroup extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $objectGroupId = (int)$value; + $objectGroupId = (int) $value; /** @var null|ObjectGroup $objectGroup */ $objectGroup = self::where('object_groups.id', $objectGroupId) @@ -104,7 +104,7 @@ class ObjectGroup extends Model protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index abef05764f..3af9eabe22 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -27,6 +27,7 @@ use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphToMany; @@ -43,20 +44,19 @@ class PiggyBank extends Model protected $casts = [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', - 'startdate' => 'date', - 'targetdate' => 'date', - 'order' => 'int', - 'active' => 'boolean', - 'encrypted' => 'boolean', - 'targetamount' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'start_date' => 'date', + 'target_date' => 'date', + 'order' => 'int', + 'active' => 'boolean', + 'encrypted' => 'boolean', + 'target_amount' => 'string', + 'native_target_amount' => 'string', ]; - protected $fillable = ['name', 'account_id', 'order', 'targetamount', 'startdate', 'startdate_tz', 'targetdate', 'targetdate_tz', 'active']; - - protected $hidden = ['targetamount_encrypted', 'encrypted']; + protected $fillable = ['name', 'order', 'target_amount', 'start_date', 'start_date_tz', 'target_date', 'target_date_tz', 'active', 'transaction_currency_id', 'native_target_amount']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -66,9 +66,10 @@ class PiggyBank extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $piggyBankId = (int)$value; + $piggyBankId = (int) $value; $piggyBank = self::where('piggy_banks.id', $piggyBankId) - ->leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id') + ->leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') ->where('accounts.user_id', auth()->user()->id)->first(['piggy_banks.*']) ; if (null !== $piggyBank) { @@ -84,6 +85,11 @@ class PiggyBank extends Model return $this->belongsTo(Account::class); } + public function accounts(): BelongsToMany + { + return $this->belongsToMany(Account::class)->withPivot(['current_amount', 'native_current_amount']); + } + public function attachments(): MorphMany { return $this->morphMany(Attachment::class, 'attachable'); @@ -118,32 +124,37 @@ class PiggyBank extends Model /** * @param mixed $value */ - public function setTargetamountAttribute($value): void + public function setTargetAmountAttribute($value): void { - $this->attributes['targetamount'] = (string)$value; + $this->attributes['target_amount'] = (string) $value; + } + + public function transactionCurrency(): BelongsTo + { + return $this->belongsTo(TransactionCurrency::class); } protected function accountId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } /** * Get the max amount */ - protected function targetamount(): Attribute + protected function targetAmount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } } diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index f8f5a385a6..b1c6fc3e28 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -42,9 +42,10 @@ class PiggyBankEvent extends Model 'updated_at' => 'datetime', 'date' => SeparateTimezoneCaster::class, 'amount' => 'string', + 'amount' => 'native_string', ]; - protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'date_tz', 'amount']; + protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'date_tz', 'amount', 'native_amount']; protected $hidden = ['amount_encrypted']; @@ -58,7 +59,7 @@ class PiggyBankEvent extends Model */ public function setAmountAttribute($value): void { - $this->attributes['amount'] = (string)$value; + $this->attributes['amount'] = (string) $value; } public function transactionJournal(): BelongsTo @@ -72,14 +73,14 @@ class PiggyBankEvent extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function piggyBankId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index 38008361dd..4a0a79c7cb 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -42,12 +42,12 @@ class PiggyBankRepetition extends Model = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', - 'startdate' => SeparateTimezoneCaster::class, - 'targetdate' => SeparateTimezoneCaster::class, + 'start_date' => SeparateTimezoneCaster::class, + 'target_date' => SeparateTimezoneCaster::class, 'virtual_balance' => 'string', ]; - protected $fillable = ['piggy_bank_id', 'startdate', 'startdate_tz', 'targetdate', 'targetdate_tz', 'currentamount']; + protected $fillable = ['piggy_bank_id', 'start_date', 'start_date_tz', 'target_date', 'target_date_tz', 'current_amount']; public function piggyBank(): BelongsTo { @@ -56,7 +56,7 @@ class PiggyBankRepetition extends Model public function scopeOnDates(EloquentBuilder $query, Carbon $start, Carbon $target): EloquentBuilder { - return $query->where('startdate', $start->format('Y-m-d'))->where('targetdate', $target->format('Y-m-d')); + return $query->where('start_date', $start->format('Y-m-d'))->where('target_date', $target->format('Y-m-d')); } /** @@ -66,14 +66,14 @@ class PiggyBankRepetition extends Model { return $query->where( static function (EloquentBuilder $q) use ($date): void { - $q->where('startdate', '<=', $date->format('Y-m-d 00:00:00')); - $q->orWhereNull('startdate'); + $q->where('start_date', '<=', $date->format('Y-m-d 00:00:00')); + $q->orWhereNull('start_date'); } ) ->where( static function (EloquentBuilder $q) use ($date): void { - $q->where('targetdate', '>=', $date->format('Y-m-d 00:00:00')); - $q->orWhereNull('targetdate'); + $q->where('target_date', '>=', $date->format('Y-m-d 00:00:00')); + $q->orWhereNull('target_date'); } ) ; @@ -82,25 +82,25 @@ class PiggyBankRepetition extends Model /** * @param mixed $value */ - public function setCurrentamountAttribute($value): void + public function setCurrentAmountAttribute($value): void { - $this->attributes['currentamount'] = (string)$value; + $this->attributes['current_amount'] = (string) $value; } /** * Get the amount */ - protected function currentamount(): Attribute + protected function currentAmount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function piggyBankId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/Preference.php b/app/Models/Preference.php index 233d11dc79..f8a43e5283 100644 --- a/app/Models/Preference.php +++ b/app/Models/Preference.php @@ -45,7 +45,7 @@ class Preference extends Model 'data' => 'array', ]; - protected $fillable = ['user_id', 'data', 'name']; + protected $fillable = ['user_id', 'data', 'name', 'user_group_id']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -60,7 +60,7 @@ class Preference extends Model // some preferences do not have an administration ID. // some need it, to make sure the correct one is selected. - $userGroupId = (int)$user->user_group_id; + $userGroupId = (int) $user->user_group_id; $userGroupId = 0 === $userGroupId ? null : $userGroupId; /** @var null|Preference $preference */ @@ -77,7 +77,7 @@ class Preference extends Model // try again with ID, but this time don't care about the preferred user_group_id if (null === $preference) { - $preference = $user->preferences()->where('id', (int)$value)->first(); + $preference = $user->preferences()->where('id', (int) $value)->first(); } if (null !== $preference) { return $preference; @@ -87,7 +87,7 @@ class Preference extends Model $preference = new self(); $preference->name = $value; $preference->data = $default[$value]; - $preference->user_id = (int)$user->id; + $preference->user_id = (int) $user->id; $preference->user_group_id = in_array($value, $items, true) ? $userGroupId : null; $preference->save(); diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php index 62b75d3b2e..fdcdbf01db 100644 --- a/app/Models/Recurrence.php +++ b/app/Models/Recurrence.php @@ -75,7 +75,7 @@ class Recurrence extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $recurrenceId = (int)$value; + $recurrenceId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -136,7 +136,7 @@ class Recurrence extends Model protected function transactionTypeId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/RecurrenceMeta.php b/app/Models/RecurrenceMeta.php index 4fa9b14e25..82702a4535 100644 --- a/app/Models/RecurrenceMeta.php +++ b/app/Models/RecurrenceMeta.php @@ -60,7 +60,7 @@ class RecurrenceMeta extends Model protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/RecurrenceRepetition.php b/app/Models/RecurrenceRepetition.php index f3daa1657b..0d1453a986 100644 --- a/app/Models/RecurrenceRepetition.php +++ b/app/Models/RecurrenceRepetition.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Enums\RecurrenceRepetitionWeekend; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; @@ -39,16 +38,16 @@ class RecurrenceRepetition extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const int WEEKEND_DO_NOTHING = 1; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const int WEEKEND_SKIP_CREATION = 2; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const int WEEKEND_TO_FRIDAY = 3; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const int WEEKEND_TO_MONDAY = 4; protected $casts @@ -67,6 +66,11 @@ class RecurrenceRepetition extends Model /** @var string The table to store the data in */ protected $table = 'recurrences_repetitions'; + public function recurrence(): BelongsTo + { + return $this->belongsTo(Recurrence::class); + } + protected function casts(): array { return [ @@ -74,29 +78,24 @@ class RecurrenceRepetition extends Model ]; } - public function recurrence(): BelongsTo - { - return $this->belongsTo(Recurrence::class); - } - protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function repetitionSkip(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function weekend(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/RecurrenceTransaction.php b/app/Models/RecurrenceTransaction.php index 1b7ee27173..777413b1eb 100644 --- a/app/Models/RecurrenceTransaction.php +++ b/app/Models/RecurrenceTransaction.php @@ -102,49 +102,49 @@ class RecurrenceTransaction extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function destinationId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function foreignAmount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function sourceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function userId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/RecurrenceTransactionMeta.php b/app/Models/RecurrenceTransactionMeta.php index e84eb1ccde..7eadb34d98 100644 --- a/app/Models/RecurrenceTransactionMeta.php +++ b/app/Models/RecurrenceTransactionMeta.php @@ -60,7 +60,7 @@ class RecurrenceTransactionMeta extends Model protected function rtId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/Rule.php b/app/Models/Rule.php index 804772e1de..fe1b0ff434 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -64,7 +64,7 @@ class Rule extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $ruleId = (int)$value; + $ruleId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -115,14 +115,14 @@ class Rule extends Model protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function ruleGroupId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/RuleAction.php b/app/Models/RuleAction.php index 6e63d55374..69732ce0cf 100644 --- a/app/Models/RuleAction.php +++ b/app/Models/RuleAction.php @@ -54,7 +54,7 @@ class RuleAction extends Model if (false === config('firefly.feature_flags.expression_engine')) { Log::debug('Expression engine is disabled, returning action value as string.'); - return (string)$this->action_value; + return (string) $this->action_value; } if (true === config('firefly.feature_flags.expression_engine') && str_starts_with($this->action_value, '\=')) { // return literal string. @@ -66,7 +66,7 @@ class RuleAction extends Model $result = $expr->evaluate($journal); } catch (SyntaxError $e) { Log::error(sprintf('Expression engine failed to evaluate expression "%s" with error "%s".', $this->action_value, $e->getMessage())); - $result = (string)$this->action_value; + $result = (string) $this->action_value; } Log::debug(sprintf('Expression engine is enabled, result of expression "%s" is "%s".', $this->action_value, $result)); @@ -81,14 +81,14 @@ class RuleAction extends Model protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function ruleId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index 0e5df95266..753fa95341 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -62,7 +62,7 @@ class RuleGroup extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $ruleGroupId = (int)$value; + $ruleGroupId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -90,7 +90,7 @@ class RuleGroup extends Model protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/RuleTrigger.php b/app/Models/RuleTrigger.php index 3a83d72246..3bcfe632a1 100644 --- a/app/Models/RuleTrigger.php +++ b/app/Models/RuleTrigger.php @@ -54,14 +54,14 @@ class RuleTrigger extends Model protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function ruleId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index fa559ac459..571e15b4ac 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -66,7 +66,7 @@ class Tag extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $tagId = (int)$value; + $tagId = (int) $value; /** @var User $user */ $user = auth()->user(); diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index f2640398c2..793e0e7652 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -25,7 +25,6 @@ namespace FireflyIII\Models; use Carbon\Carbon; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; -use GeneaLabs\LaravelModelCaching\Traits\Cachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -39,26 +38,27 @@ use Illuminate\Database\Eloquent\SoftDeletes; */ class Transaction extends Model { - use Cachable; use HasFactory; use ReturnsIntegerIdTrait; use SoftDeletes; protected $casts = [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', - 'identifier' => 'int', - 'encrypted' => 'boolean', // model does not have these fields though - 'bill_name_encrypted' => 'boolean', - 'reconciled' => 'boolean', - 'balance_dirty' => 'boolean', - 'balance_before' => 'string', - 'balance_after' => 'string', - 'date' => 'datetime', - 'amount' => 'string', - 'foreign_amount' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'identifier' => 'int', + 'encrypted' => 'boolean', // model does not have these fields though + 'bill_name_encrypted' => 'boolean', + 'reconciled' => 'boolean', + 'balance_dirty' => 'boolean', + 'balance_before' => 'string', + 'balance_after' => 'string', + 'date' => 'datetime', + 'amount' => 'string', + 'foreign_amount' => 'string', + 'native_amount' => 'string', + 'native_foreign_amount' => 'string', ]; protected $fillable @@ -67,6 +67,8 @@ class Transaction extends Model 'transaction_journal_id', 'description', 'amount', + 'native_amount', + 'native_foreign_amount', 'identifier', 'transaction_currency_id', 'foreign_currency_id', @@ -163,7 +165,7 @@ class Transaction extends Model */ public function setAmountAttribute($value): void { - $this->attributes['amount'] = (string)$value; + $this->attributes['amount'] = (string) $value; } public function transactionCurrency(): BelongsTo @@ -179,14 +181,7 @@ class Transaction extends Model protected function accountId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, - ); - } - - protected function balanceDirty(): Attribute - { - return Attribute::make( - get: static fn ($value) => 1 === (int)$value, + get: static fn ($value) => (int) $value, ); } @@ -196,7 +191,14 @@ class Transaction extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, + ); + } + + protected function balanceDirty(): Attribute + { + return Attribute::make( + get: static fn ($value) => 1 === (int) $value, ); } @@ -206,14 +208,14 @@ class Transaction extends Model protected function foreignAmount(): Attribute { return Attribute::make( - get: static fn ($value) => (string)$value, + get: static fn ($value) => (string) $value, ); } protected function transactionJournalId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php index 96c8a8ea5b..2b159287dd 100644 --- a/app/Models/TransactionCurrency.php +++ b/app/Models/TransactionCurrency.php @@ -40,10 +40,10 @@ class TransactionCurrency extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - public ?bool $userGroupDefault; - public ?bool $userGroupEnabled; + public ?bool $userGroupDefault = null; + public ?bool $userGroupEnabled = null; protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', @@ -51,7 +51,7 @@ class TransactionCurrency extends Model 'enabled' => 'bool', ]; - protected $fillable = ['name', 'code', 'symbol', 'decimal_places', 'enabled']; + protected $fillable = ['name', 'code', 'symbol', 'decimal_places', 'enabled']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -61,7 +61,7 @@ class TransactionCurrency extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $currencyId = (int)$value; + $currencyId = (int) $value; $currency = self::find($currencyId); if (null !== $currency) { $currency->refreshForUser(auth()->user()); @@ -115,7 +115,7 @@ class TransactionCurrency extends Model protected function decimalPlaces(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php index 36022272df..2e7024da86 100644 --- a/app/Models/TransactionGroup.php +++ b/app/Models/TransactionGroup.php @@ -62,7 +62,7 @@ class TransactionGroup extends Model { app('log')->debug(sprintf('Now in %s("%s")', __METHOD__, $value)); if (auth()->check()) { - $groupId = (int)$value; + $groupId = (int) $value; /** @var User $user */ $user = auth()->user(); diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 87779dd2b4..01f49bd228 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -38,7 +38,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Facades\Log; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -170,15 +169,11 @@ class TransactionJournal extends Model public function scopeAfter(EloquentBuilder $query, Carbon $date): EloquentBuilder { - Log::debug(sprintf('scopeAfter("%s")', $date->format('Y-m-d H:i:s'))); - return $query->where('transaction_journals.date', '>=', $date->format('Y-m-d H:i:s')); } public function scopeBefore(EloquentBuilder $query, Carbon $date): EloquentBuilder { - Log::debug(sprintf('scopeBefore("%s")', $date->format('Y-m-d H:i:s'))); - return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')); } diff --git a/app/Models/TransactionJournalLink.php b/app/Models/TransactionJournalLink.php index c50a422b7a..0ddc38d13e 100644 --- a/app/Models/TransactionJournalLink.php +++ b/app/Models/TransactionJournalLink.php @@ -54,7 +54,7 @@ class TransactionJournalLink extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $linkId = (int)$value; + $linkId = (int) $value; $link = self::where('journal_links.id', $linkId) ->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id') ->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id') @@ -96,21 +96,21 @@ class TransactionJournalLink extends Model protected function destinationId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function linkTypeId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function sourceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/TransactionJournalMeta.php b/app/Models/TransactionJournalMeta.php index cd7608a8e4..7c89f9f4bd 100644 --- a/app/Models/TransactionJournalMeta.php +++ b/app/Models/TransactionJournalMeta.php @@ -66,7 +66,7 @@ class TransactionJournalMeta extends Model { $data = json_encode($value); $this->attributes['data'] = $data; - $this->attributes['hash'] = hash('sha256', (string)$data); + $this->attributes['hash'] = hash('sha256', (string) $data); } public function transactionJournal(): BelongsTo @@ -77,7 +77,7 @@ class TransactionJournalMeta extends Model protected function transactionJournalId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php index dfc466cf3a..b807264dec 100644 --- a/app/Models/TransactionType.php +++ b/app/Models/TransactionType.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -38,25 +37,25 @@ class TransactionType extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string DEPOSIT = 'Deposit'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string INVALID = 'Invalid'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string LIABILITY_CREDIT = 'Liability credit'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string OPENING_BALANCE = 'Opening balance'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string RECONCILIATION = 'Reconciliation'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string TRANSFER = 'Transfer'; - /** @deprecated */ + #[\Deprecated] /** @deprecated */ public const string WITHDRAWAL = 'Withdrawal'; protected $casts @@ -67,13 +66,6 @@ class TransactionType extends Model ]; protected $fillable = ['type']; - protected function casts(): array - { - return [ - // 'type' => TransactionTypeEnum::class, - ]; - } - /** * Route binder. Converts the key in the URL to the specified object (or throw 404). * @@ -116,4 +108,11 @@ class TransactionType extends Model { return $this->hasMany(TransactionJournal::class); } + + protected function casts(): array + { + return [ + // 'type' => TransactionTypeEnum::class, + ]; + } } diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index e161c89c4b..b3ed2716a9 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; @@ -50,7 +51,7 @@ class UserGroup extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $userGroupId = (int)$value; + $userGroupId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -150,7 +151,7 @@ class UserGroup extends Model */ public function piggyBanks(): HasManyThrough { - return $this->hasManyThrough(PiggyBank::class, Account::class); + throw new FireflyException('This user group method is EOL.'); } public function recurrences(): HasMany diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index 203f231490..aa957a1220 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -54,15 +54,6 @@ class Webhook extends Model ]; protected $fillable = ['active', 'trigger', 'response', 'delivery', 'user_id', 'user_group_id', 'url', 'title', 'secret']; - protected function casts(): array - { - return [ - // 'delivery' => WebhookDelivery::class, - // 'response' => WebhookResponse::class, - // 'trigger' => WebhookTrigger::class, - ]; - } - public static function getDeliveries(): array { $array = []; @@ -164,4 +155,13 @@ class Webhook extends Model { return $this->hasMany(WebhookMessage::class); } + + protected function casts(): array + { + return [ + // 'delivery' => WebhookDelivery::class, + // 'response' => WebhookResponse::class, + // 'trigger' => WebhookTrigger::class, + ]; + } } diff --git a/app/Models/WebhookAttempt.php b/app/Models/WebhookAttempt.php index 7470f3deaf..e2249cd2fb 100644 --- a/app/Models/WebhookAttempt.php +++ b/app/Models/WebhookAttempt.php @@ -48,7 +48,7 @@ class WebhookAttempt extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $attemptId = (int)$value; + $attemptId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -71,7 +71,7 @@ class WebhookAttempt extends Model protected function webhookMessageId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Models/WebhookMessage.php b/app/Models/WebhookMessage.php index fcee310a45..adc5f9de4c 100644 --- a/app/Models/WebhookMessage.php +++ b/app/Models/WebhookMessage.php @@ -56,7 +56,7 @@ class WebhookMessage extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $messageId = (int)$value; + $messageId = (int) $value; /** @var User $user */ $user = auth()->user(); @@ -87,14 +87,14 @@ class WebhookMessage extends Model protected function sent(): Attribute { return Attribute::make( - get: static fn ($value) => (bool)$value, + get: static fn ($value) => (bool) $value, ); } protected function webhookId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Notifications/Admin/TestNotification.php b/app/Notifications/Admin/TestNotification.php deleted file mode 100644 index 03b52e93d6..0000000000 --- a/app/Notifications/Admin/TestNotification.php +++ /dev/null @@ -1,114 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace FireflyIII\Notifications\Admin; - -use FireflyIII\Support\Notifications\UrlValidator; -use Illuminate\Bus\Queueable; -use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Messages\SlackMessage; -use Illuminate\Notifications\Notification; - -/** - * Class TestNotification - */ -class TestNotification extends Notification -{ - use Queueable; - - private string $address; - - /** - * Create a new notification instance. - */ - public function __construct(string $address) - { - $this->address = $address; - } - - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array - */ - public function toArray($notifiable) - { - return [ - ]; - } - - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return MailMessage - */ - public function toMail($notifiable) - { - return (new MailMessage()) - ->markdown('emails.admin-test', ['email' => $this->address]) - ->subject((string)trans('email.admin_test_subject')) - ; - } - - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return SlackMessage - */ - public function toSlack($notifiable) - { - return (new SlackMessage())->content((string)trans('email.admin_test_subject')); - } - - /** - * Get the notification's delivery channels. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @param mixed $notifiable - * - * @return array - */ - public function via($notifiable) - { - $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; - if (UrlValidator::isValidWebhookURL($slackUrl)) { - return ['mail', 'slack']; - } - - return ['mail']; - } -} diff --git a/app/Notifications/Admin/UnknownUserLoginAttempt.php b/app/Notifications/Admin/UnknownUserLoginAttempt.php new file mode 100644 index 0000000000..5409b18ee9 --- /dev/null +++ b/app/Notifications/Admin/UnknownUserLoginAttempt.php @@ -0,0 +1,116 @@ +address = $address; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(OwnerNotifiable $notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(OwnerNotifiable $notifiable): MailMessage + { + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); + + return new MailMessage() + ->markdown('emails.owner.unknown-user', ['address' => $this->address, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time]) + ->subject((string) trans('email.unknown_user_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toNtfy(OwnerNotifiable $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.unknown_user_subject')); + $message->body((string) trans('email.unknown_user_message', ['address' => $this->address])); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.unknown_user_message', ['address' => $this->address])) + ->title((string) trans('email.unknown_user_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(OwnerNotifiable $notifiable): SlackMessage + { + return new SlackMessage()->content( + (string) trans('email.unknown_user_body', ['address' => $this->address]) + ); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(OwnerNotifiable $notifiable) + { + return ReturnsAvailableChannels::returnChannels('owner'); + } +} diff --git a/app/Notifications/Admin/UserInvitation.php b/app/Notifications/Admin/UserInvitation.php index 15d97d1aa2..88a51f691a 100644 --- a/app/Notifications/Admin/UserInvitation.php +++ b/app/Notifications/Admin/UserInvitation.php @@ -25,11 +25,18 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Admin; use FireflyIII\Models\InvitedUser; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class UserInvitation @@ -38,80 +45,83 @@ class UserInvitation extends Notification { use Queueable; - private InvitedUser $invitee; + private InvitedUser $invitee; + private OwnerNotifiable $owner; - /** - * Create a new notification instance. - */ - public function __construct(InvitedUser $invitee) + public function __construct(OwnerNotifiable $owner, InvitedUser $invitee) { $this->invitee = $invitee; + $this->owner = $owner; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(OwnerNotifiable $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(OwnerNotifiable $notifiable) { + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); + + return (new MailMessage()) - ->markdown('emails.invitation-created', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email]) - ->subject((string)trans('email.invitation_created_subject')) + ->markdown('emails.invitation-created', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time]) + ->subject((string) trans('email.invitation_created_subject')) ; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toNtfy(OwnerNotifiable $notifiable): Message { - return (new SlackMessage())->content( - (string)trans('email.invitation_created_body', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email]) + Log::debug('Now in toNtfy() for UserInvitation'); + $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.invitation_created_subject')); + $message->body((string) trans('email.invitation_created_body', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email])); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage + { + Log::debug('Now in toPushover() for UserInvitation'); + + return PushoverMessage::create((string) trans('email.invitation_created_body', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email])) + ->title((string) trans('email.invitation_created_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(OwnerNotifiable $notifiable) + { + return new SlackMessage()->content( + (string) trans('email.invitation_created_body', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email]) ); } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(OwnerNotifiable $notifiable) { - $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; - if (UrlValidator::isValidWebhookURL($slackUrl)) { - return ['mail', 'slack']; - } - - return ['mail']; + return ReturnsAvailableChannels::returnChannels('owner'); } } diff --git a/app/Notifications/Admin/UserRegistration.php b/app/Notifications/Admin/UserRegistration.php index 759e200d82..b3410f2a29 100644 --- a/app/Notifications/Admin/UserRegistration.php +++ b/app/Notifications/Admin/UserRegistration.php @@ -24,12 +24,19 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Admin; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class UserRegistration @@ -38,78 +45,80 @@ class UserRegistration extends Notification { use Queueable; - private User $user; + private OwnerNotifiable $owner; + private User $user; - /** - * Create a new notification instance. - */ - public function __construct(User $user) + public function __construct(OwnerNotifiable $owner, User $user) { - $this->user = $user; + $this->user = $user; + $this->owner = $owner; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(OwnerNotifiable $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(OwnerNotifiable $notifiable) { + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); + return (new MailMessage()) - ->markdown('emails.registered-admin', ['email' => $this->user->email, 'id' => $this->user->id]) - ->subject((string)trans('email.registered_subject_admin')) + ->markdown('emails.registered-admin', ['email' => $this->user->email, 'id' => $this->user->id, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time]) + ->subject((string) trans('email.registered_subject_admin')) ; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toNtfy(OwnerNotifiable $notifiable): Message { - return (new SlackMessage())->content((string)trans('email.admin_new_user_registered', ['email' => $this->user->email, 'id' => $this->user->id])); + Log::debug('Now in toNtfy() for (Admin) UserRegistration'); + $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.registered_subject_admin')); + $message->body((string) trans('email.admin_new_user_registered', ['email' => $this->user->email, 'invitee' => $this->user->email])); + + return $message; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage { - $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; - if (UrlValidator::isValidWebhookURL($slackUrl)) { - return ['mail', 'slack']; - } + Log::debug('Now in toPushover() for UserRegistration'); - return ['mail']; + return PushoverMessage::create((string) trans('email.admin_new_user_registered', ['email' => $this->user->email, 'invitee' => $this->user->email])) + ->title((string) trans('email.registered_subject_admin')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(OwnerNotifiable $notifiable) + { + return new SlackMessage()->content((string) trans('email.admin_new_user_registered', ['email' => $this->user->email, 'id' => $this->user->id])); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(OwnerNotifiable $notifiable) + { + return ReturnsAvailableChannels::returnChannels('owner'); } } diff --git a/app/Notifications/Admin/VersionCheckResult.php b/app/Notifications/Admin/VersionCheckResult.php index 7ee75e8933..c4a4e783b9 100644 --- a/app/Notifications/Admin/VersionCheckResult.php +++ b/app/Notifications/Admin/VersionCheckResult.php @@ -24,11 +24,16 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Admin; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Log; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class VersionCheckResult @@ -39,58 +44,64 @@ class VersionCheckResult extends Notification private string $message; - /** - * Create a new notification instance. - */ public function __construct(string $message) { $this->message = $message; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(OwnerNotifiable $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(OwnerNotifiable $notifiable) { return (new MailMessage()) ->markdown('emails.new-version', ['message' => $this->message]) - ->subject((string)trans('email.new_version_email_subject')) + ->subject((string) trans('email.new_version_email_subject')) ; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toNtfy(OwnerNotifiable $notifiable): Message { - return (new SlackMessage())->content($this->message) + Log::debug('Now in toNtfy() for VersionCheckResult'); + $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.new_version_email_subject')); + $message->body($this->message); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage + { + Log::debug('Now in toPushover() for VersionCheckResult'); + + return PushoverMessage::create($this->message) + ->title((string) trans('email.new_version_email_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(OwnerNotifiable $notifiable) + { + return new SlackMessage()->content($this->message) ->attachment(static function ($attachment): void { $attachment->title('Firefly III @ GitHub', 'https://github.com/firefly-iii/firefly-iii/releases'); }) @@ -98,21 +109,10 @@ class VersionCheckResult extends Notification } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(OwnerNotifiable $notifiable) { - $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; - if (UrlValidator::isValidWebhookURL($slackUrl)) { - return ['mail', 'slack']; - } - - return ['mail']; + return ReturnsAvailableChannels::returnChannels('owner'); } } diff --git a/app/Notifications/Notifiables/OwnerNotifiable.php b/app/Notifications/Notifiables/OwnerNotifiable.php new file mode 100644 index 0000000000..300dd427fd --- /dev/null +++ b/app/Notifications/Notifiables/OwnerNotifiable.php @@ -0,0 +1,78 @@ +{$method}($notification); // @phpstan-ignore-line + } + Log::debug(sprintf('No method "%s" found, return generic settings.', $method)); + + return match ($driver) { + 'mail' => (string) config('firefly.site_owner'), + default => null, + }; + } + + public function routeNotificationForPushover() + { + Log::debug('Return settings for routeNotificationForPushover'); + $pushoverAppToken = (string) app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data; + $pushoverUserToken = (string) app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data; + + return PushoverReceiver::withUserKey($pushoverUserToken) + ->withApplicationToken($pushoverAppToken) + ; + } + + public function routeNotificationForSlack(): string + { + $res = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; + if (is_array($res)) { + $res = ''; + } + + return (string) $res; + } +} diff --git a/app/Notifications/ReturnsAvailableChannels.php b/app/Notifications/ReturnsAvailableChannels.php new file mode 100644 index 0000000000..6844cad6e2 --- /dev/null +++ b/app/Notifications/ReturnsAvailableChannels.php @@ -0,0 +1,117 @@ +getEncrypted('slack_webhook_url', '')->data; + if (UrlValidator::isValidWebhookURL($slackUrl)) { + $channels[] = 'slack'; + } + + // validate presence of of Ntfy settings. + if ('' !== (string) app('fireflyconfig')->getEncrypted('ntfy_topic', '')->data) { + Log::debug('Enabled ntfy.'); + $channels[] = NtfyChannel::class; + } + if ('' === (string) app('fireflyconfig')->getEncrypted('ntfy_topic', '')->data) { + Log::warning('No topic name for Ntfy, channel is disabled.'); + } + + // pushover + $pushoverAppToken = (string) app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data; + $pushoverUserToken = (string) app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data; + if ('' === $pushoverAppToken || '' === $pushoverUserToken) { + Log::warning('[b] No Pushover token, channel is disabled.'); + } + if ('' !== $pushoverAppToken && '' !== $pushoverUserToken) { + Log::debug('Enabled pushover.'); + $channels[] = PushoverChannel::class; + } + + Log::debug(sprintf('Final channel set in ReturnsAvailableChannels: %s ', implode(', ', $channels))); + + return $channels; + } + + private static function returnUserChannels(User $user): array + { + $channels = ['mail']; + $slackUrl = app('preferences')->getEncryptedForUser($user, 'slack_webhook_url', '')->data; + if (UrlValidator::isValidWebhookURL($slackUrl)) { + $channels[] = 'slack'; + } + + // validate presence of of Ntfy settings. + if ('' !== (string) app('preferences')->getEncryptedForUser($user, 'ntfy_topic', '')->data) { + Log::debug('Enabled ntfy.'); + $channels[] = NtfyChannel::class; + } + if ('' === (string) app('preferences')->getEncryptedForUser($user, 'ntfy_topic', '')->data) { + Log::warning('No topic name for Ntfy, channel is disabled.'); + } + + // pushover + $pushoverAppToken = (string) app('preferences')->getEncryptedForUser($user, 'pushover_app_token', '')->data; + $pushoverUserToken = (string) app('preferences')->getEncryptedForUser($user, 'pushover_user_token', '')->data; + if ('' === $pushoverAppToken || '' === $pushoverUserToken) { + Log::warning('[b] No Pushover token, channel is disabled.'); + } + if ('' !== $pushoverAppToken && '' !== $pushoverUserToken) { + Log::debug('Enabled pushover.'); + $channels[] = PushoverChannel::class; + } + + Log::debug(sprintf('Final channel set in ReturnsAvailableChannels (user): %s ', implode(', ', $channels))); + + // only the owner can get notifications over + return $channels; + } +} diff --git a/app/Notifications/ReturnsSettings.php b/app/Notifications/ReturnsSettings.php new file mode 100644 index 0000000000..f0582ec751 --- /dev/null +++ b/app/Notifications/ReturnsSettings.php @@ -0,0 +1,81 @@ + 'https://ntfy.sh', + 'ntfy_topic' => '', + 'ntfy_auth' => false, + 'ntfy_user' => '', + 'ntfy_pass' => '', + + ]; + if ('user' === $type && null !== $user) { + $settings['ntfy_server'] = Preferences::getEncryptedForUser($user, 'ntfy_server', 'https://ntfy.sh')->data; + $settings['ntfy_topic'] = Preferences::getEncryptedForUser($user, 'ntfy_topic', '')->data; + $settings['ntfy_auth'] = Preferences::getForUser($user, 'ntfy_auth', false)->data; + $settings['ntfy_user'] = Preferences::getEncryptedForUser($user, 'ntfy_user', '')->data; + $settings['ntfy_pass'] = Preferences::getEncryptedForUser($user, 'ntfy_pass', '')->data; + } + if ('owner' === $type) { + $settings['ntfy_server'] = FireflyConfig::getEncrypted('ntfy_server', 'https://ntfy.sh')->data; + $settings['ntfy_topic'] = FireflyConfig::getEncrypted('ntfy_topic', '')->data; + $settings['ntfy_auth'] = FireflyConfig::get('ntfy_auth', false)->data; + $settings['ntfy_user'] = FireflyConfig::getEncrypted('ntfy_user', '')->data; + $settings['ntfy_pass'] = FireflyConfig::getEncrypted('ntfy_pass', '')->data; + } + + // overrule config. + config(['ntfy-notification-channel.server' => $settings['ntfy_server']]); + config(['ntfy-notification-channel.topic' => $settings['ntfy_topic']]); + + if ($settings['ntfy_auth']) { + // overrule auth as well. + config(['ntfy-notification-channel.authentication.enabled' => true]); + config(['ntfy-notification-channel.authentication.username' => $settings['ntfy_user']]); + config(['ntfy-notification-channel.authentication.password' => $settings['ntfy_pass']]); + } + + return $settings; + } +} diff --git a/app/Notifications/Security/DisabledMFANotification.php b/app/Notifications/Security/DisabledMFANotification.php index 2676f3d5b9..c467c478bb 100644 --- a/app/Notifications/Security/DisabledMFANotification.php +++ b/app/Notifications/Security/DisabledMFANotification.php @@ -24,95 +24,88 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class DisabledMFANotification extends Notification { use Queueable; - private User $user; + private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user) { $this->user = $user; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans('email.disabled_mfa_subject'); + $subject = (string) trans('email.disabled_mfa_subject'); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); - return (new MailMessage())->markdown('emails.security.disabled-mfa', ['user' => $this->user])->subject($subject); + return (new MailMessage())->markdown('emails.security.disabled-mfa', ['user' => $this->user, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.disabled_mfa_subject')); + $message->body((string) trans('email.disabled_mfa_slack', ['email' => $this->user->email])); + + return $message; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - $message = (string)trans('email.disabled_mfa_slack', ['email' => $this->user->email]); - - return (new SlackMessage())->content($message); + return PushoverMessage::create((string) trans('email.disabled_mfa_slack', ['email' => $this->user->email])) + ->title((string) trans('email.disabled_mfa_subject')) + ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toSlack(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } + $message = (string) trans('email.disabled_mfa_slack', ['email' => $this->user->email]); - return ['mail']; + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/Security/EnabledMFANotification.php b/app/Notifications/Security/EnabledMFANotification.php index cfe5cf0e6d..daac64653b 100644 --- a/app/Notifications/Security/EnabledMFANotification.php +++ b/app/Notifications/Security/EnabledMFANotification.php @@ -24,95 +24,88 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class EnabledMFANotification extends Notification { use Queueable; - private User $user; + private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user) { $this->user = $user; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans('email.enabled_mfa_subject'); + $subject = (string) trans('email.enabled_mfa_subject'); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); - return (new MailMessage())->markdown('emails.security.enabled-mfa', ['user' => $this->user])->subject($subject); + return (new MailMessage())->markdown('emails.security.enabled-mfa', ['user' => $this->user, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.enabled_mfa_subject')); + $message->body((string) trans('email.enabled_mfa_slack', ['email' => $this->user->email])); + + return $message; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - $message = (string)trans('email.enabled_mfa_slack', ['email' => $this->user->email]); - - return (new SlackMessage())->content($message); + return PushoverMessage::create((string) trans('email.enabled_mfa_slack', ['email' => $this->user->email])) + ->title((string) trans('email.enabled_mfa_subject')) + ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toSlack(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } + $message = (string) trans('email.enabled_mfa_slack', ['email' => $this->user->email]); - return ['mail']; + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/Security/MFABackupFewLeftNotification.php b/app/Notifications/Security/MFABackupFewLeftNotification.php index d01925686f..60ef94e2f0 100644 --- a/app/Notifications/Security/MFABackupFewLeftNotification.php +++ b/app/Notifications/Security/MFABackupFewLeftNotification.php @@ -24,23 +24,25 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class MFABackupFewLeftNotification extends Notification { use Queueable; - private User $user; - private int $count; + private int $count; + private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user, int $count) { $this->user = $user; @@ -48,73 +50,64 @@ class MFABackupFewLeftNotification extends Notification } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans('email.mfa_few_backups_left_subject', ['count' => $this->count]); + $subject = (string) trans('email.mfa_few_backups_left_subject', ['count' => $this->count]); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); - return (new MailMessage())->markdown('emails.security.few-backup-codes', ['user' => $this->user, 'count' => $this->count])->subject($subject); + return (new MailMessage())->markdown('emails.security.few-backup-codes', ['user' => $this->user, 'count' => $this->count, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.mfa_few_backups_left_subject')); + $message->body((string) trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count])); + + return $message; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - $message = (string)trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count]); - - return (new SlackMessage())->content($message); + return PushoverMessage::create((string) trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count])) + ->title((string) trans('email.mfa_few_backups_left_subject')) + ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toSlack(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } + $message = (string) trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count]); - return ['mail']; + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/Security/MFABackupNoLeftNotification.php b/app/Notifications/Security/MFABackupNoLeftNotification.php index 91a98c1101..7c0344d9ce 100644 --- a/app/Notifications/Security/MFABackupNoLeftNotification.php +++ b/app/Notifications/Security/MFABackupNoLeftNotification.php @@ -24,95 +24,88 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class MFABackupNoLeftNotification extends Notification { use Queueable; - private User $user; + private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user) { $this->user = $user; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans('email.mfa_no_backups_left_subject'); + $subject = (string) trans('email.mfa_no_backups_left_subject'); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); - return (new MailMessage())->markdown('emails.security.no-backup-codes', ['user' => $this->user])->subject($subject); + return (new MailMessage())->markdown('emails.security.no-backup-codes', ['user' => $this->user, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.mfa_no_backups_left_subject')); + $message->body((string) trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email])); + + return $message; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - $message = (string)trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email]); - - return (new SlackMessage())->content($message); + return PushoverMessage::create((string) trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email])) + ->title((string) trans('email.mfa_no_backups_left_slack')) + ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toSlack(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } + $message = (string) trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email]); - return ['mail']; + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/Security/MFAManyFailedAttemptsNotification.php b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php index b8926e58e8..499c4d7ff8 100644 --- a/app/Notifications/Security/MFAManyFailedAttemptsNotification.php +++ b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php @@ -24,97 +24,87 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class MFAManyFailedAttemptsNotification extends Notification { use Queueable; - private User $user; - private int $count; + private int $count; + private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user, int $count) { $this->user = $user; $this->count = $count; } - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans('email.mfa_many_failed_subject', ['count' => $this->count]); + $subject = (string) trans('email.mfa_many_failed_subject', ['count' => $this->count]); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); - return (new MailMessage())->markdown('emails.security.many-failed-attempts', ['user' => $this->user, 'count' => $this->count])->subject($subject); + return (new MailMessage())->markdown('emails.security.many-failed-attempts', ['user' => $this->user, 'count' => $this->count, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.mfa_many_failed_subject')); + $message->body((string) trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count])); + + return $message; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - $message = (string)trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]); - - return (new SlackMessage())->content($message); + return PushoverMessage::create((string) trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count])) + ->title((string) trans('email.mfa_many_failed_subject')) + ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toSlack(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } + $message = (string) trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]); - return ['mail']; + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/Security/MFAUsedBackupCodeNotification.php b/app/Notifications/Security/MFAUsedBackupCodeNotification.php index 2179dd2357..7c4b754c5c 100644 --- a/app/Notifications/Security/MFAUsedBackupCodeNotification.php +++ b/app/Notifications/Security/MFAUsedBackupCodeNotification.php @@ -24,95 +24,88 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class MFAUsedBackupCodeNotification extends Notification { use Queueable; - private User $user; + private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user) { $this->user = $user; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans('email.used_backup_code_subject'); + $subject = (string) trans('email.used_backup_code_subject'); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); - return (new MailMessage())->markdown('emails.security.used-backup-code', ['user' => $this->user])->subject($subject); + return (new MailMessage())->markdown('emails.security.used-backup-code', ['user' => $this->user, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.used_backup_code_subject')); + $message->body((string) trans('email.used_backup_code_slack', ['email' => $this->user->email])); + + return $message; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - $message = (string)trans('email.used_backup_code_slack', ['email' => $this->user->email]); - - return (new SlackMessage())->content($message); + return PushoverMessage::create((string) trans('email.used_backup_code_slack', ['email' => $this->user->email])) + ->title((string) trans('email.used_backup_code_subject')) + ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toSlack(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } + $message = (string) trans('email.used_backup_code_slack', ['email' => $this->user->email]); - return ['mail']; + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/Security/NewBackupCodesNotification.php b/app/Notifications/Security/NewBackupCodesNotification.php index eeaac3b0ac..c3a7e72144 100644 --- a/app/Notifications/Security/NewBackupCodesNotification.php +++ b/app/Notifications/Security/NewBackupCodesNotification.php @@ -24,95 +24,88 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class NewBackupCodesNotification extends Notification { use Queueable; - private User $user; + private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user) { $this->user = $user; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans('email.new_backup_codes_subject'); + $subject = (string) trans('email.new_backup_codes_subject'); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); - return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user])->subject($subject); + return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.new_backup_codes_subject')); + $message->body((string) trans('email.new_backup_codes_slack', ['email' => $this->user->email])); + + return $message; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - $message = (string)trans('email.new_backup_codes_slack', ['email' => $this->user->email]); - - return (new SlackMessage())->content($message); + return PushoverMessage::create((string) trans('email.new_backup_codes_slack', ['email' => $this->user->email])) + ->title((string) trans('email.new_backup_codes_subject')) + ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toSlack(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } + $message = (string) trans('email.new_backup_codes_slack', ['email' => $this->user->email]); - return ['mail']; + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/Security/UserFailedLoginAttempt.php b/app/Notifications/Security/UserFailedLoginAttempt.php new file mode 100644 index 0000000000..25d02183e2 --- /dev/null +++ b/app/Notifications/Security/UserFailedLoginAttempt.php @@ -0,0 +1,108 @@ +user = $user; + } + + public function toArray(User $notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) + { + $subject = (string) trans('email.failed_login_subject'); + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); + + return (new MailMessage())->markdown('emails.security.failed-login', ['user' => $this->user, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time])->subject($subject); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.failed_login_subject')); + $message->body((string) trans('email.failed_login_message', ['email' => $this->user->email])); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.failed_login_message', ['email' => $this->user->email])) + ->title((string) trans('email.failed_login_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) + { + $message = (string) trans('email.failed_login_message', ['email' => $this->user->email]); + + return new SlackMessage()->content($message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); + } +} diff --git a/app/Notifications/Test/OwnerTestNotificationEmail.php b/app/Notifications/Test/OwnerTestNotificationEmail.php new file mode 100644 index 0000000000..72b1c85443 --- /dev/null +++ b/app/Notifications/Test/OwnerTestNotificationEmail.php @@ -0,0 +1,75 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Notification; + +/** + * Class TestNotification + */ +class OwnerTestNotificationEmail extends Notification +{ + use Queueable; + + private OwnerNotifiable $owner; + + public function __construct(OwnerNotifiable $owner) + { + $this->owner = $owner; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(OwnerNotifiable $notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(OwnerNotifiable $notifiable) + { + $address = (string) config('firefly.site_owner'); + + return (new MailMessage()) + ->markdown('emails.admin-test', ['email' => $address]) + ->subject((string) trans('email.admin_test_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(OwnerNotifiable $notifiable) + { + return ['mail']; + } +} diff --git a/app/Notifications/Test/OwnerTestNotificationNtfy.php b/app/Notifications/Test/OwnerTestNotificationNtfy.php new file mode 100644 index 0000000000..36ea816bf9 --- /dev/null +++ b/app/Notifications/Test/OwnerTestNotificationNtfy.php @@ -0,0 +1,83 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use FireflyIII\Notifications\ReturnsSettings; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Notification; +use Ntfy\Message; +use Wijourdil\NtfyNotificationChannel\Channels\NtfyChannel; + +// use Illuminate\Notifications\Slack\SlackMessage; + +/** + * Class TestNotification + */ +class OwnerTestNotificationNtfy extends Notification +{ + use Queueable; + + public OwnerNotifiable $owner; + + public function __construct(OwnerNotifiable $owner) + { + $this->owner = $owner; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param mixed $notifiable + */ + public function toArray($notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toNtfy(OwnerNotifiable $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.admin_test_subject')); + $message->body((string) trans('email.admin_test_message', ['channel' => 'ntfy'])); + $message->tags(['white_check_mark']); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(OwnerNotifiable $notifiable) + { + return [NtfyChannel::class]; + } +} diff --git a/app/Notifications/Test/OwnerTestNotificationPushover.php b/app/Notifications/Test/OwnerTestNotificationPushover.php new file mode 100644 index 0000000000..876d81f038 --- /dev/null +++ b/app/Notifications/Test/OwnerTestNotificationPushover.php @@ -0,0 +1,78 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Log; +use NotificationChannels\Pushover\PushoverChannel; +use NotificationChannels\Pushover\PushoverMessage; + +// use Illuminate\Notifications\Slack\SlackMessage; + +/** + * Class TestNotification + */ +class OwnerTestNotificationPushover extends Notification +{ + use Queueable; + + private OwnerNotifiable $owner; + + public function __construct(OwnerNotifiable $owner) + { + $this->owner = $owner; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(OwnerNotifiable $notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage + { + Log::debug('Now in toPushover()'); + + return PushoverMessage::create((string) trans('email.admin_test_message', ['channel' => 'Pushover'])) + ->title((string) trans('email.admin_test_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(OwnerNotifiable $notifiable) + { + return [PushoverChannel::class]; + } +} diff --git a/app/Notifications/Test/OwnerTestNotificationSlack.php b/app/Notifications/Test/OwnerTestNotificationSlack.php new file mode 100644 index 0000000000..a8dab9aa0e --- /dev/null +++ b/app/Notifications/Test/OwnerTestNotificationSlack.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\Notifications\Notifiables\OwnerNotifiable; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Messages\SlackMessage; +use Illuminate\Notifications\Notification; + +// use Illuminate\Notifications\Slack\SlackMessage; + +/** + * Class TestNotification + */ +class OwnerTestNotificationSlack extends Notification +{ + use Queueable; + + private OwnerNotifiable $owner; + + public function __construct(OwnerNotifiable $owner) + { + $this->owner = $owner; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(OwnerNotifiable $notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(OwnerNotifiable $notifiable) + { + return new SlackMessage()->content((string) trans('email.admin_test_subject')); + // return new SlackMessage()->text((string) trans('email.admin_test_subject'))->to($url); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(OwnerNotifiable $notifiable) + { + return ['slack']; + } +} diff --git a/app/Notifications/Test/UserTestNotificationEmail.php b/app/Notifications/Test/UserTestNotificationEmail.php new file mode 100644 index 0000000000..6001bcb69c --- /dev/null +++ b/app/Notifications/Test/UserTestNotificationEmail.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\User; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Notification; + +/** + * Class TestNotification + */ +class UserTestNotificationEmail extends Notification +{ + use Queueable; + + private User $user; + + public function __construct(User $user) + { + $this->user = $user; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) + { + return [ + ]; + } + + public function toMail(User $notifiable) + { + $address = (string) $notifiable->email; + + return (new MailMessage()) + ->markdown('emails.admin-test', ['email' => $address]) + ->subject((string) trans('email.admin_test_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return ['mail']; + } +} diff --git a/app/Notifications/Test/UserTestNotificationNtfy.php b/app/Notifications/Test/UserTestNotificationNtfy.php new file mode 100644 index 0000000000..0305c40775 --- /dev/null +++ b/app/Notifications/Test/UserTestNotificationNtfy.php @@ -0,0 +1,81 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\User; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Notification; +use Ntfy\Message; +use Wijourdil\NtfyNotificationChannel\Channels\NtfyChannel; + +// use Illuminate\Notifications\Slack\SlackMessage; + +/** + * Class TestNotification + */ +class UserTestNotificationNtfy extends Notification +{ + use Queueable; + + public User $user; + + public function __construct(User $user) + { + $this->user = $user; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toNtfy(User $user): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $user); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.admin_test_subject')); + $message->body((string) trans('email.admin_test_message', ['channel' => 'ntfy'])); + $message->tags(['white_check_mark']); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $user) + { + return [NtfyChannel::class]; + } +} diff --git a/app/Notifications/Test/UserTestNotificationPushover.php b/app/Notifications/Test/UserTestNotificationPushover.php new file mode 100644 index 0000000000..d4da811e72 --- /dev/null +++ b/app/Notifications/Test/UserTestNotificationPushover.php @@ -0,0 +1,78 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\User; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Log; +use NotificationChannels\Pushover\PushoverChannel; +use NotificationChannels\Pushover\PushoverMessage; + +// use Illuminate\Notifications\Slack\SlackMessage; + +/** + * Class TestNotification + */ +class UserTestNotificationPushover extends Notification +{ + use Queueable; + + private User $user; + + public function __construct(User $user) + { + $this->user = $user; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + Log::debug('Now in (user) toPushover()'); + + return PushoverMessage::create((string) trans('email.admin_test_message', ['channel' => 'Pushover'])) + ->title((string) trans('email.admin_test_subject')) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) + { + return [PushoverChannel::class]; + } +} diff --git a/app/Notifications/Test/UserTestNotificationSlack.php b/app/Notifications/Test/UserTestNotificationSlack.php new file mode 100644 index 0000000000..150ee0dd62 --- /dev/null +++ b/app/Notifications/Test/UserTestNotificationSlack.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use FireflyIII\User; +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Messages\SlackMessage; +use Illuminate\Notifications\Notification; + +// use Illuminate\Notifications\Slack\SlackMessage; + +/** + * Class TestNotification + */ +class UserTestNotificationSlack extends Notification +{ + use Queueable; + + private User $user; + + public function __construct(User $user) + { + $this->user = $user; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $user) + { + return [ + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $user) + { + return new SlackMessage()->content((string) trans('email.admin_test_subject')); + // return new SlackMessage()->text((string) trans('email.admin_test_subject'))->to($url); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $user) + { + return ['slack']; + } +} diff --git a/app/Notifications/User/BillReminder.php b/app/Notifications/User/BillReminder.php index 36d93b84e6..4e65a0b55b 100644 --- a/app/Notifications/User/BillReminder.php +++ b/app/Notifications/User/BillReminder.php @@ -25,12 +25,15 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; use FireflyIII\Models\Bill; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class BillReminder @@ -43,9 +46,6 @@ class BillReminder extends Notification private int $diff; private string $field; - /** - * Create a new notification instance. - */ public function __construct(Bill $bill, string $field, int $diff) { $this->bill = $bill; @@ -54,90 +54,78 @@ class BillReminder extends Notification } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $subject = (string)trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); - if (0 === $this->diff) { - $subject = (string)trans(sprintf('email.bill_warning_subject_now_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); - } - return (new MailMessage()) ->markdown('emails.bill-warning', ['field' => $this->field, 'diff' => $this->diff, 'bill' => $this->bill]) - ->subject($subject) + ->subject($this->getSubject()) + ; + } + + private function getSubject(): string + { + $message = (string) trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); + if (0 === $this->diff) { + $message = (string) trans(sprintf('email.bill_warning_subject_now_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); + } + + return $message; + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title($this->getSubject()); + $message->body((string) trans('email.bill_warning_please_action')); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.bill_warning_please_action')) + ->title($this->getSubject()) ; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toSlack(User $notifiable) { - $message = (string)trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); - if (0 === $this->diff) { - $message = (string)trans(sprintf('email.bill_warning_subject_now_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); - } - $bill = $this->bill; - $url = route('bills.show', [$bill->id]); + $bill = $this->bill; + $url = route('bills.show', [$bill->id]); - return (new SlackMessage()) + return new SlackMessage() ->warning() ->attachment(static function ($attachment) use ($bill, $url): void { - $attachment->title((string)trans('firefly.visit_bill', ['name' => $bill->name]), $url); + $attachment->title((string) trans('firefly.visit_bill', ['name' => $bill->name]), $url); }) - ->content($message) + ->content($this->getSubject()) ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } - - return ['mail']; + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/User/NewAccessToken.php b/app/Notifications/User/NewAccessToken.php index b32192d089..dd8873fb43 100644 --- a/app/Notifications/User/NewAccessToken.php +++ b/app/Notifications/User/NewAccessToken.php @@ -24,12 +24,17 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class NewAccessToken @@ -38,78 +43,64 @@ class NewAccessToken extends Notification { use Queueable; - /** - * Create a new notification instance. - */ public function __construct() {} - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); + return (new MailMessage()) - ->markdown('emails.token-created') - ->subject((string)trans('email.access_token_created_subject')) + ->markdown('emails.token-created', ['ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time]) + ->subject((string) trans('email.access_token_created_subject')) + ; + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.access_token_created_subject')); + $message->body((string) trans('email.access_token_created_body')); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.access_token_created_body')) + ->title((string) trans('email.access_token_created_subject')) ; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toSlack(User $notifiable) { - return (new SlackMessage())->content((string)trans('email.access_token_created_body')); + return new SlackMessage()->content((string) trans('email.access_token_created_body')); } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } - - return ['mail']; + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/User/RuleActionFailed.php b/app/Notifications/User/RuleActionFailed.php index 669d8b8e7b..d798ff666f 100644 --- a/app/Notifications/User/RuleActionFailed.php +++ b/app/Notifications/User/RuleActionFailed.php @@ -24,11 +24,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class RuleActionFailed @@ -43,9 +46,6 @@ class RuleActionFailed extends Notification private string $ruleLink; private string $ruleTitle; - /** - * Create a new notification instance. - */ public function __construct(array $params) { [$mainMessage, $groupTitle, $groupLink, $ruleTitle, $ruleLink] = $params; @@ -57,67 +57,59 @@ class RuleActionFailed extends Notification } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->body($this->message); + + return $message; + } + /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create($this->message); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { $groupTitle = $this->groupTitle; $groupLink = $this->groupLink; $ruleTitle = $this->ruleTitle; $ruleLink = $this->ruleLink; - return (new SlackMessage())->content($this->message)->attachment(static function ($attachment) use ($groupTitle, $groupLink): void { - $attachment->title((string)trans('rules.inspect_transaction', ['title' => $groupTitle]), $groupLink); + return new SlackMessage()->content($this->message)->attachment(static function ($attachment) use ($groupTitle, $groupLink): void { + $attachment->title((string) trans('rules.inspect_transaction', ['title' => $groupTitle]), $groupLink); })->attachment(static function ($attachment) use ($ruleTitle, $ruleLink): void { - $attachment->title((string)trans('rules.inspect_rule', ['title' => $ruleTitle]), $ruleLink); + $attachment->title((string) trans('rules.inspect_rule', ['title' => $ruleTitle]), $ruleLink); }); } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; + $channels = ReturnsAvailableChannels::returnChannels('user', $notifiable); + if (($key = array_search('mail', $channels, true)) !== false) { + unset($channels[$key]); } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - app('log')->debug('Will send ruleActionFailed through Slack or Discord!'); - return ['slack']; - } - app('log')->debug('Will NOT send ruleActionFailed through Slack or Discord'); - - return []; + return $channels; } } diff --git a/app/Notifications/User/TransactionCreation.php b/app/Notifications/User/TransactionCreation.php index fcf9f95fc0..034ffc39c8 100644 --- a/app/Notifications/User/TransactionCreation.php +++ b/app/Notifications/User/TransactionCreation.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -37,39 +38,24 @@ class TransactionCreation extends Notification private array $collection; - /** - * Create a new notification instance. - */ public function __construct(array $collection) { $this->collection = $collection; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { return (new MailMessage()) ->markdown('emails.report-new-journals', ['transformed' => $this->collection]) @@ -78,15 +64,9 @@ class TransactionCreation extends Notification } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(User $notifiable) { return ['mail']; } diff --git a/app/Notifications/User/UserLogin.php b/app/Notifications/User/UserLogin.php index e7fc4e8a53..95e5bcba13 100644 --- a/app/Notifications/User/UserLogin.php +++ b/app/Notifications/User/UserLogin.php @@ -24,13 +24,17 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; -use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class UserLogin @@ -39,108 +43,70 @@ class UserLogin extends Notification { use Queueable; - private string $ip; - - /** - * Create a new notification instance. - */ - public function __construct(string $ip) - { - $this->ip = $ip; - } - - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { - $time = now(config('app.timezone'))->isoFormat((string)trans('config.date_time_js')); - $host = ''; - - try { - $hostName = app('steam')->getHostName($this->ip); - } catch (FireflyException $e) { - app('log')->error($e->getMessage()); - $hostName = $this->ip; - } - if ($hostName !== $this->ip) { - $host = $hostName; - } + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); return (new MailMessage()) - ->markdown('emails.new-ip', ['time' => $time, 'ipAddress' => $this->ip, 'host' => $host]) - ->subject((string)trans('email.login_from_new_ip')) + ->markdown('emails.new-ip', ['ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time]) + ->subject((string) trans('email.login_from_new_ip')) + ; + } + + public function toNtfy(User $notifiable): Message + { + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title((string) trans('email.login_from_new_ip')); + $message->body((string) trans('email.slack_login_from_new_ip', ['ip' => $ip, 'host' => $host])); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + $ip = Request::ip(); + $host = Steam::getHostName($ip); + + return PushoverMessage::create((string) trans('email.slack_login_from_new_ip', ['ip' => $ip, 'host' => $host])) + ->title((string) trans('email.login_from_new_ip')) ; } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toSlack(User $notifiable) { - $host = ''; + $ip = Request::ip(); + $host = Steam::getHostName($ip); - try { - $hostName = app('steam')->getHostName($this->ip); - } catch (FireflyException $e) { - app('log')->error($e->getMessage()); - $hostName = $this->ip; - } - if ($hostName !== $this->ip) { - $host = $hostName; - } - - return (new SlackMessage())->content((string)trans('email.slack_login_from_new_ip', ['host' => $host, 'ip' => $this->ip])); + return new SlackMessage()->content((string) trans('email.slack_login_from_new_ip', ['ip' => $ip, 'host' => $host])); } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(User $notifiable) { - /** @var null|User $user */ - $user = auth()->user(); - $slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data; - if (is_array($slackUrl)) { - $slackUrl = ''; - } - if (UrlValidator::isValidWebhookURL((string)$slackUrl)) { - return ['mail', 'slack']; - } - - return ['mail']; + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/User/UserNewPassword.php b/app/Notifications/User/UserNewPassword.php index e4090984d5..ca14855de5 100644 --- a/app/Notifications/User/UserNewPassword.php +++ b/app/Notifications/User/UserNewPassword.php @@ -24,9 +24,17 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; +use FireflyIII\Support\Facades\Steam; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Request; +use NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; /** * Class UserNewPassword @@ -37,57 +45,64 @@ class UserNewPassword extends Notification private string $url; - /** - * Create a new notification instance. - */ public function __construct(string $url) { $this->url = $url; } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { + $ip = Request::ip(); + $host = Steam::getHostName($ip); + $userAgent = Request::userAgent(); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); + return (new MailMessage()) - ->markdown('emails.password', ['url' => $this->url]) - ->subject((string)trans('email.reset_pw_subject')) + ->markdown('emails.password', ['url' => $this->url, 'ip' => $ip, 'host' => $host, 'userAgent' => $userAgent, 'time' => $time]) + ->subject((string) trans('email.reset_pw_subject')) ; } + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->body((string) trans('email.reset_pw_message')); + + return $message; + } + /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function toPushover(User $notifiable): PushoverMessage { - return ['mail']; + return PushoverMessage::create((string) trans('email.reset_pw_message')); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) + { + return new SlackMessage()->content((string) trans('email.reset_pw_message')); + } + + public function via(User $notifiable) + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/User/UserRegistration.php b/app/Notifications/User/UserRegistration.php index 50aae568a3..2d7c32a07c 100644 --- a/app/Notifications/User/UserRegistration.php +++ b/app/Notifications/User/UserRegistration.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -35,54 +36,34 @@ class UserRegistration extends Notification { use Queueable; - /** - * Create a new notification instance. - */ public function __construct() {} /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toMail($notifiable) + public function toMail(User $notifiable) { return (new MailMessage()) ->markdown('emails.registered', ['address' => route('index')]) - ->subject((string)trans('email.registered_subject')) + ->subject((string) trans('email.registered_subject')) ; } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function via($notifiable) + public function via(User $notifiable) { + // other settings will not be available at this point anyway. return ['mail']; } } diff --git a/app/Policies/AccountPolicy.php b/app/Policies/AccountPolicy.php index 3f2851707c..6f6044fa1a 100644 --- a/app/Policies/AccountPolicy.php +++ b/app/Policies/AccountPolicy.php @@ -30,19 +30,16 @@ use Illuminate\Support\Facades\Log; class AccountPolicy { - /** - * TODO needs better authentication, also for group. - */ - public function view(User $user, Account $account): bool - { - return auth()->check() && $user->id === $account->user_id; - } - public function create(): bool { return auth()->check(); } + public function viewAccountBalances(User $user, Account $account): bool + { + return $this->view($user, $account); + } + /** * Everybody can do this, but selection should limit to user. * @@ -65,8 +62,11 @@ class AccountPolicy return $this->view($user, $account); } - public function viewAccountBalances(User $user, Account $account): bool + /** + * TODO needs better authentication, also for group. + */ + public function view(User $user, Account $account): bool { - return $this->view($user, $account); + return auth()->check() && $user->id === $account->user_id; } } diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index ecc0526c91..5f71cfa585 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -38,6 +38,13 @@ class UserPolicy return auth()->check() && $user->id === $account->user_id; } + public function viewAccounts(User $user): bool + { + return true; + + return auth()->check(); + } + /** * Everybody can do this, but selection should limit to user. * @@ -49,11 +56,4 @@ class UserPolicy return auth()->check(); } - - public function viewAccounts(User $user): bool - { - return true; - - return auth()->check(); - } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c8ffb3bde2..ca3b82710a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -45,7 +45,7 @@ class AppServiceProvider extends ServiceProvider $headers = [ 'Cache-Control' => 'no-store', ]; - $uuid = (string)request()->header('X-Trace-Id'); + $uuid = (string) request()->header('X-Trace-Id'); if ('' !== trim($uuid) && (1 === preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', trim($uuid)))) { $headers['X-Trace-Id'] = $uuid; } diff --git a/app/Providers/CurrencyServiceProvider.php b/app/Providers/CurrencyServiceProvider.php index 6643cfa4c4..601ea77f43 100644 --- a/app/Providers/CurrencyServiceProvider.php +++ b/app/Providers/CurrencyServiceProvider.php @@ -27,6 +27,8 @@ use FireflyIII\Repositories\Currency\CurrencyRepository; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepository as GroupCurrencyRepository; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface as GroupCurrencyRepositoryInterface; +use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepository; +use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepositoryInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -71,5 +73,13 @@ class CurrencyServiceProvider extends ServiceProvider return $repository; } ); + + $this->app->bind( + ExchangeRateRepositoryInterface::class, + static function (Application $app) { + // @var ExchangeRateRepository $repository + return app(ExchangeRateRepository::class); + } + ); } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index b89317fede..d66b6d8ca5 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -25,7 +25,6 @@ namespace FireflyIII\Providers; use FireflyIII\Events\ActuallyLoggedIn; use FireflyIII\Events\Admin\InvitationCreated; -use FireflyIII\Events\AdminRequestedTestMessage; use FireflyIII\Events\DestroyedTransactionGroup; use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\Model\BudgetLimit\Created; @@ -35,6 +34,7 @@ use FireflyIII\Events\Model\PiggyBank\ChangedAmount; use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray; use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject; use FireflyIII\Events\NewVersionAvailable; +use FireflyIII\Events\Preferences\UserGroupChangedDefaultCurrency; use FireflyIII\Events\RegisteredUser; use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\RequestedReportOnJournals; @@ -47,8 +47,12 @@ use FireflyIII\Events\Security\MFABackupNoLeft; use FireflyIII\Events\Security\MFAManyFailedAttempts; use FireflyIII\Events\Security\MFANewBackupCodes; use FireflyIII\Events\Security\MFAUsedBackupCode; +use FireflyIII\Events\Security\UnknownUserAttemptedLogin; +use FireflyIII\Events\Security\UserAttemptedLogin; use FireflyIII\Events\StoredAccount; use FireflyIII\Events\StoredTransactionGroup; +use FireflyIII\Events\Test\OwnerTestNotificationChannel; +use FireflyIII\Events\Test\UserTestNotificationChannel; use FireflyIII\Events\TriggeredAuditLog; use FireflyIII\Events\UpdatedAccount; use FireflyIII\Events\UpdatedTransactionGroup; @@ -56,9 +60,13 @@ use FireflyIII\Events\UserChangedEmail; use FireflyIII\Events\WarnUserAboutBill; use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Handlers\Observer\AttachmentObserver; +use FireflyIII\Handlers\Observer\AutoBudgetObserver; +use FireflyIII\Handlers\Observer\AvailableBudgetObserver; use FireflyIII\Handlers\Observer\BillObserver; +use FireflyIII\Handlers\Observer\BudgetLimitObserver; use FireflyIII\Handlers\Observer\BudgetObserver; use FireflyIII\Handlers\Observer\CategoryObserver; +use FireflyIII\Handlers\Observer\PiggyBankEventObserver; use FireflyIII\Handlers\Observer\PiggyBankObserver; use FireflyIII\Handlers\Observer\RecurrenceObserver; use FireflyIII\Handlers\Observer\RecurrenceTransactionObserver; @@ -72,10 +80,14 @@ use FireflyIII\Handlers\Observer\WebhookMessageObserver; use FireflyIII\Handlers\Observer\WebhookObserver; use FireflyIII\Models\Account; use FireflyIII\Models\Attachment; +use FireflyIII\Models\AutoBudget; +use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Category; use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Models\Recurrence; use FireflyIII\Models\RecurrenceTransaction; use FireflyIII\Models\Rule; @@ -100,141 +112,154 @@ class EventServiceProvider extends ServiceProvider protected $listen = [ // is a User related event. - RegisteredUser::class => [ + RegisteredUser::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail', 'FireflyIII\Handlers\Events\UserEventHandler@sendAdminRegistrationNotification', 'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole', 'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership', 'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates', ], + UserAttemptedLogin::class => [ + 'FireflyIII\Handlers\Events\UserEventHandler@sendLoginAttemptNotification', + ], // is a User related event. - Login::class => [ + Login::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin', 'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish', ], - ActuallyLoggedIn::class => [ + ActuallyLoggedIn::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress', ], - DetectedNewIPAddress::class => [ + DetectedNewIPAddress::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@notifyNewIPAddress', ], - RequestedVersionCheckStatus::class => [ + RequestedVersionCheckStatus::class => [ 'FireflyIII\Handlers\Events\VersionCheckEventHandler@checkForUpdates', ], - RequestedReportOnJournals::class => [ + RequestedReportOnJournals::class => [ 'FireflyIII\Handlers\Events\AutomationHandler@reportJournals', ], // is a User related event. - RequestedNewPassword::class => [ + RequestedNewPassword::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword', ], + UserTestNotificationChannel::class => [ + 'FireflyIII\Handlers\Events\UserEventHandler@sendTestNotification', + ], // is a User related event. - UserChangedEmail::class => [ + UserChangedEmail::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeConfirmMail', 'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail', ], // admin related - AdminRequestedTestMessage::class => [ - 'FireflyIII\Handlers\Events\AdminEventHandler@sendTestMessage', + OwnerTestNotificationChannel::class => [ + 'FireflyIII\Handlers\Events\AdminEventHandler@sendTestNotification', ], - NewVersionAvailable::class => [ + NewVersionAvailable::class => [ 'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion', ], - InvitationCreated::class => [ + InvitationCreated::class => [ 'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification', 'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite', ], + UnknownUserAttemptedLogin::class => [ + 'FireflyIII\Handlers\Events\AdminEventHandler@sendLoginAttemptNotification', + ], // is a Transaction Journal related event. - StoredTransactionGroup::class => [ + StoredTransactionGroup::class => [ 'FireflyIII\Handlers\Events\StoredGroupEventHandler@processRules', 'FireflyIII\Handlers\Events\StoredGroupEventHandler@recalculateCredit', 'FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerWebhooks', ], // is a Transaction Journal related event. - UpdatedTransactionGroup::class => [ + UpdatedTransactionGroup::class => [ 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@unifyAccounts', 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@processRules', 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@recalculateCredit', 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@triggerWebhooks', ], - DestroyedTransactionGroup::class => [ + DestroyedTransactionGroup::class => [ 'FireflyIII\Handlers\Events\DestroyedGroupEventHandler@triggerWebhooks', ], // API related events: - AccessTokenCreated::class => [ + AccessTokenCreated::class => [ 'FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated', ], // Webhook related event: - RequestedSendWebhookMessages::class => [ + RequestedSendWebhookMessages::class => [ 'FireflyIII\Handlers\Events\WebhookEventHandler@sendWebhookMessages', ], // account related events: - StoredAccount::class => [ + StoredAccount::class => [ 'FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit', ], - UpdatedAccount::class => [ + UpdatedAccount::class => [ 'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit', ], // bill related events: - WarnUserAboutBill::class => [ + WarnUserAboutBill::class => [ 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill', ], // audit log events: - TriggeredAuditLog::class => [ + TriggeredAuditLog::class => [ 'FireflyIII\Handlers\Events\AuditEventHandler@storeAuditEvent', ], // piggy bank related events: - ChangedAmount::class => [ + ChangedAmount::class => [ 'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount', ], // budget related events: CRUD budget limit - Created::class => [ + Created::class => [ 'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@created', ], - Updated::class => [ + Updated::class => [ 'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@updated', ], - Deleted::class => [ + Deleted::class => [ 'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@deleted', ], // rule actions - RuleActionFailedOnArray::class => [ + RuleActionFailedOnArray::class => [ 'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray', ], - RuleActionFailedOnObject::class => [ + RuleActionFailedOnObject::class => [ 'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnObject', ], // security related - EnabledMFA::class => [ + EnabledMFA::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail', ], - DisabledMFA::class => [ + DisabledMFA::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail', ], - MFANewBackupCodes::class => [ + MFANewBackupCodes::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail', ], - MFAUsedBackupCode::class => [ + MFAUsedBackupCode::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendUsedBackupCodeMail', ], - MFABackupFewLeft::class => [ + MFABackupFewLeft::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupFewLeftMail', ], - MFABackupNoLeft::class => [ + MFABackupNoLeft::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupNoLeftMail', ], - MFAManyFailedAttempts::class => [ + MFAManyFailedAttempts::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail', ], + // preferences + UserGroupChangedDefaultCurrency::class => [ + 'FireflyIII\Handlers\Events\PreferencesEventHandler@resetNativeAmounts', + ], ]; /** @@ -248,11 +273,15 @@ class EventServiceProvider extends ServiceProvider private function registerObservers(): void { Attachment::observe(new AttachmentObserver()); - PiggyBank::observe(new PiggyBankObserver()); Account::observe(new AccountObserver()); + AutoBudget::observe(new AutoBudgetObserver()); + AvailableBudget::observe(new AvailableBudgetObserver()); Bill::observe(new BillObserver()); Budget::observe(new BudgetObserver()); + BudgetLimit::observe(new BudgetLimitObserver()); Category::observe(new CategoryObserver()); + PiggyBank::observe(new PiggyBankObserver()); + PiggyBankEvent::observe(new PiggyBankEventObserver()); Recurrence::observe(new RecurrenceObserver()); RecurrenceTransaction::observe(new RecurrenceTransactionObserver()); Rule::observe(new RuleObserver()); diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index cfe2e325d4..651d73fdc2 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -391,7 +391,7 @@ class AccountRepository implements AccountRepositoryInterface if (!in_array($type, $list, true)) { return null; } - $currencyId = (int)$this->getMetaValue($account, 'currency_id'); + $currencyId = (int) $this->getMetaValue($account, 'currency_id'); if ($currencyId > 0) { return TransactionCurrency::find($currencyId); } @@ -413,7 +413,7 @@ class AccountRepository implements AccountRepositoryInterface return null; } if (1 === $result->count()) { - return (string)$result->first()->data; + return (string) $result->first()->data; } return null; @@ -431,11 +431,10 @@ class AccountRepository implements AccountRepositoryInterface public function getUsedCurrencies(Account $account): Collection { - $info = $account->transactions()->get(['transaction_currency_id', 'foreign_currency_id'])->toArray(); + $info = $account->transactions()->distinct()->groupBy('transaction_currency_id')->get(['transaction_currency_id'])->toArray(); $currencyIds = []; foreach ($info as $entry) { - $currencyIds[] = (int)$entry['transaction_currency_id']; - $currencyIds[] = (int)$entry['foreign_currency_id']; + $currencyIds[] = (int) $entry['transaction_currency_id']; } $currencyIds = array_unique($currencyIds); @@ -458,14 +457,14 @@ class AccountRepository implements AccountRepositoryInterface AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::CREDITCARD, AccountType::MORTGAGE], ]; if (array_key_exists(ucfirst($type), $sets)) { - $order = (int)$this->getAccountsByType($sets[ucfirst($type)])->max('order'); + $order = (int) $this->getAccountsByType($sets[ucfirst($type)])->max('order'); app('log')->debug(sprintf('Return max order of "%s" set: %d', $type, $order)); return $order; } $specials = [AccountType::CASH, AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION]; - $order = (int)$this->getAccountsByType($specials)->max('order'); + $order = (int) $this->getAccountsByType($specials)->max('order'); app('log')->debug(sprintf('Return max order of "%s" set (specials!): %d', $type, $order)); return $order; @@ -546,7 +545,7 @@ class AccountRepository implements AccountRepositoryInterface continue; } - if ($index !== (int)$account->order) { + if ($index !== (int) $account->order) { app('log')->debug(sprintf('Account #%d ("%s"): order should %d be but is %d.', $account->id, $account->name, $index, $account->order)); $account->order = $index; $account->save(); @@ -562,6 +561,17 @@ class AccountRepository implements AccountRepositoryInterface ; } + /** + * @throws FireflyException + */ + public function update(Account $account, array $data): Account + { + /** @var AccountUpdateService $service */ + $service = app(AccountUpdateService::class); + + return $service->update($account, $data); + } + public function searchAccount(string $query, array $types, int $limit): Collection { $dbQuery = $this->user->accounts() @@ -634,15 +644,4 @@ class AccountRepository implements AccountRepositoryInterface return $factory->create($data); } - - /** - * @throws FireflyException - */ - public function update(Account $account, array $data): Account - { - /** @var AccountUpdateService $service */ - $service = app(AccountUpdateService::class); - - return $service->update($account, $data); - } } diff --git a/app/Repositories/Account/AccountTasker.php b/app/Repositories/Account/AccountTasker.php index 000d4a0589..d367817c73 100644 --- a/app/Repositories/Account/AccountTasker.php +++ b/app/Repositories/Account/AccountTasker.php @@ -47,8 +47,8 @@ class AccountTasker implements AccountTaskerInterface { $yesterday = clone $start; $yesterday->subDay(); - $startSet = app('steam')->balancesByAccounts($accounts, $yesterday); - $endSet = app('steam')->balancesByAccounts($accounts, $end); + $startSet = app('steam')->finalAccountsBalance($accounts, $yesterday); + $endSet = app('steam')->finalAccountsBalance($accounts, $end); app('log')->debug('Start of accountreport'); /** @var AccountRepositoryInterface $repository */ @@ -86,8 +86,8 @@ class AccountTasker implements AccountTaskerInterface // get first journal date: $first = $repository->oldestJournal($account); - $entry['start_balance'] = $startSet[$account->id] ?? '0'; - $entry['end_balance'] = $endSet[$account->id] ?? '0'; + $entry['start_balance'] = $startSet[$account->id]['balance'] ?? '0'; + $entry['end_balance'] = $endSet[$account->id]['balance'] ?? '0'; // first journal exists, and is on start, then this is the actual opening balance: if (null !== $first && $first->date->isSameDay($start) && TransactionType::OPENING_BALANCE === $first->transactionType->type) { @@ -130,7 +130,7 @@ class AccountTasker implements AccountTaskerInterface // Obtain a list of columns $sum = []; foreach ($report['accounts'] as $accountId => $row) { - $sum[$accountId] = (float)$row['sum']; // intentional float + $sum[$accountId] = (float) $row['sum']; // intentional float } array_multisort($sum, SORT_ASC, $report['accounts']); @@ -155,8 +155,8 @@ class AccountTasker implements AccountTaskerInterface /** @var array $journal */ foreach ($array as $journal) { - $sourceId = (int)$journal['destination_account_id']; - $currencyId = (int)$journal['currency_id']; + $sourceId = (int) $journal['destination_account_id']; + $currencyId = (int) $journal['currency_id']; $key = sprintf('%s-%s', $sourceId, $currencyId); $currencies[$currencyId] ??= $currencyRepos->find($currencyId); $report['accounts'][$key] ??= [ @@ -181,7 +181,7 @@ class AccountTasker implements AccountTaskerInterface // do averages and sums. foreach (array_keys($report['accounts']) as $key) { if ($report['accounts'][$key]['count'] > 1) { - $report['accounts'][$key]['average'] = bcdiv($report['accounts'][$key]['sum'], (string)$report['accounts'][$key]['count']); + $report['accounts'][$key]['average'] = bcdiv($report['accounts'][$key]['sum'], (string) $report['accounts'][$key]['count']); } $currencyId = $report['accounts'][$key]['currency_id']; $report['sums'][$currencyId] ??= [ @@ -218,7 +218,7 @@ class AccountTasker implements AccountTaskerInterface // Obtain a list of columns $sum = []; foreach ($report['accounts'] as $accountId => $row) { - $sum[$accountId] = (float)$row['sum']; // intentional float + $sum[$accountId] = (float) $row['sum']; // intentional float } array_multisort($sum, SORT_DESC, $report['accounts']); @@ -243,8 +243,8 @@ class AccountTasker implements AccountTaskerInterface /** @var array $journal */ foreach ($array as $journal) { - $sourceId = (int)$journal['source_account_id']; - $currencyId = (int)$journal['currency_id']; + $sourceId = (int) $journal['source_account_id']; + $currencyId = (int) $journal['currency_id']; $key = sprintf('%s-%s', $sourceId, $currencyId); if (!array_key_exists($key, $report['accounts'])) { $currencies[$currencyId] ??= $currencyRepos->find($currencyId); @@ -268,7 +268,7 @@ class AccountTasker implements AccountTaskerInterface // do averages and sums. foreach (array_keys($report['accounts']) as $key) { if ($report['accounts'][$key]['count'] > 1) { - $report['accounts'][$key]['average'] = bcdiv($report['accounts'][$key]['sum'], (string)$report['accounts'][$key]['count']); + $report['accounts'][$key]['average'] = bcdiv($report['accounts'][$key]['sum'], (string) $report['accounts'][$key]['count']); } $currencyId = $report['accounts'][$key]['currency_id']; $report['sums'][$currencyId] ??= [ diff --git a/app/Repositories/Account/OperationsRepository.php b/app/Repositories/Account/OperationsRepository.php index 5beaa5a49f..d7959127d8 100644 --- a/app/Repositories/Account/OperationsRepository.php +++ b/app/Repositories/Account/OperationsRepository.php @@ -25,9 +25,10 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionCurrency; -use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Report\Summarizer\TransactionSummarizer; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; @@ -46,7 +47,7 @@ class OperationsRepository implements OperationsRepositoryInterface */ public function listExpenses(Carbon $start, Carbon $end, Collection $accounts): array { - $journals = $this->getTransactions($start, $end, $accounts, TransactionType::WITHDRAWAL); + $journals = $this->getTransactions($start, $end, $accounts, TransactionTypeEnum::WITHDRAWAL->value); return $this->sortByCurrency($journals, 'negative'); } @@ -76,8 +77,8 @@ class OperationsRepository implements OperationsRepositoryInterface { $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $journalId = (int)$journal['transaction_journal_id']; + $currencyId = (int) $journal['currency_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId] ??= [ 'currency_id' => $journal['currency_id'], 'currency_name' => $journal['currency_name'], @@ -88,7 +89,7 @@ class OperationsRepository implements OperationsRepositoryInterface ]; $array[$currencyId]['transaction_journals'][$journalId] = [ - 'amount' => app('steam')->{$direction}((string)$journal['amount']), // @phpstan-ignore-line + 'amount' => app('steam')->{$direction}((string) $journal['amount']), // @phpstan-ignore-line 'date' => $journal['date'], 'transaction_journal_id' => $journalId, 'budget_name' => $journal['budget_name'], @@ -115,7 +116,7 @@ class OperationsRepository implements OperationsRepositoryInterface */ public function listIncome(Carbon $start, Carbon $end, ?Collection $accounts = null): array { - $journals = $this->getTransactions($start, $end, $accounts, TransactionType::DEPOSIT); + $journals = $this->getTransactions($start, $end, $accounts, TransactionTypeEnum::DEPOSIT->value); return $this->sortByCurrency($journals, 'positive'); } @@ -130,7 +131,7 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $expense = null, ?TransactionCurrency $currency = null ): array { - $journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency); + $journals = $this->getTransactionsForSum(TransactionTypeEnum::WITHDRAWAL->value, $start, $end, $accounts, $expense, $currency); return $this->groupByCurrency($journals, 'negative'); } @@ -155,7 +156,7 @@ class OperationsRepository implements OperationsRepositoryInterface $collector->setUser($this->user)->setRange($start, $end)->setTypes([$type])->withAccountInformation(); // depends on transaction type: - if (TransactionType::WITHDRAWAL === $type) { + if (TransactionTypeEnum::WITHDRAWAL->value === $type) { if (null !== $accounts) { $collector->setSourceAccounts($accounts); } @@ -163,7 +164,7 @@ class OperationsRepository implements OperationsRepositoryInterface $collector->setDestinationAccounts($opposing); } } - if (TransactionType::DEPOSIT === $type) { + if (TransactionTypeEnum::DEPOSIT->value === $type) { if (null !== $accounts) { $collector->setDestinationAccounts($accounts); } @@ -172,7 +173,7 @@ class OperationsRepository implements OperationsRepositoryInterface } } // supports only accounts, not opposing. - if (TransactionType::TRANSFER === $type && null !== $accounts) { + if (TransactionTypeEnum::TRANSFER->value === $type && null !== $accounts) { $collector->setAccounts($accounts); } @@ -188,7 +189,7 @@ class OperationsRepository implements OperationsRepositoryInterface $collector->setUser($this->user)->setRange($start, $end)->setTypes([$type])->withAccountInformation() ->setForeignCurrency($currency) ; - if (TransactionType::WITHDRAWAL === $type) { + if (TransactionTypeEnum::WITHDRAWAL->value === $type) { if (null !== $accounts) { $collector->setSourceAccounts($accounts); } @@ -196,7 +197,7 @@ class OperationsRepository implements OperationsRepositoryInterface $collector->setDestinationAccounts($opposing); } } - if (TransactionType::DEPOSIT === $type) { + if (TransactionTypeEnum::DEPOSIT->value === $type) { if (null !== $accounts) { $collector->setDestinationAccounts($accounts); } @@ -216,36 +217,9 @@ class OperationsRepository implements OperationsRepositoryInterface private function groupByCurrency(array $journals, string $direction): array { - $array = []; + $summarizer = new TransactionSummarizer($this->user); - foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $array[$currencyId] ??= [ - 'sum' => '0', - 'currency_id' => $currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - ]; - $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->{$direction}($journal['amount'])); // @phpstan-ignore-line - - // also do foreign amount: - $foreignId = (int)$journal['foreign_currency_id']; - if (0 !== $foreignId) { - $array[$foreignId] ??= [ - 'sum' => '0', - 'currency_id' => $foreignId, - 'currency_name' => $journal['foreign_currency_name'], - 'currency_symbol' => $journal['foreign_currency_symbol'], - 'currency_code' => $journal['foreign_currency_code'], - 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], - ]; - $array[$foreignId]['sum'] = bcadd($array[$foreignId]['sum'], app('steam')->{$direction}($journal['foreign_amount'])); // @phpstan-ignore-line - } - } - - return $array; + return $summarizer->groupByCurrencyId($journals, $direction); } /** @@ -258,49 +232,16 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $expense = null, ?TransactionCurrency $currency = null ): array { - $journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency); + $journals = $this->getTransactionsForSum(TransactionTypeEnum::WITHDRAWAL->value, $start, $end, $accounts, $expense, $currency); return $this->groupByDirection($journals, 'destination', 'negative'); } private function groupByDirection(array $journals, string $direction, string $method): array { - $array = []; - $idKey = sprintf('%s_account_id', $direction); - $nameKey = sprintf('%s_account_name', $direction); + $summarizer = new TransactionSummarizer($this->user); - foreach ($journals as $journal) { - $key = sprintf('%s-%s', $journal[$idKey], $journal['currency_id']); - $array[$key] ??= [ - 'id' => $journal[$idKey], - 'name' => $journal[$nameKey], - 'sum' => '0', - 'currency_id' => $journal['currency_id'], - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - ]; - $array[$key]['sum'] = bcadd($array[$key]['sum'], app('steam')->{$method}((string)$journal['amount'])); // @phpstan-ignore-line - - // also do foreign amount: - if (0 !== (int)$journal['foreign_currency_id']) { - $key = sprintf('%s-%s', $journal[$idKey], $journal['foreign_currency_id']); - $array[$key] ??= [ - 'id' => $journal[$idKey], - 'name' => $journal[$nameKey], - 'sum' => '0', - 'currency_id' => $journal['foreign_currency_id'], - 'currency_name' => $journal['foreign_currency_name'], - 'currency_symbol' => $journal['foreign_currency_symbol'], - 'currency_code' => $journal['foreign_currency_code'], - 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], - ]; - $array[$key]['sum'] = bcadd($array[$key]['sum'], app('steam')->{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line - } - } - - return $array; + return $summarizer->groupByDirection($journals, $method, $direction); } /** @@ -313,7 +254,7 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $expense = null, ?TransactionCurrency $currency = null ): array { - $journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency); + $journals = $this->getTransactionsForSum(TransactionTypeEnum::WITHDRAWAL->value, $start, $end, $accounts, $expense, $currency); return $this->groupByDirection($journals, 'source', 'negative'); } @@ -328,7 +269,7 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $revenue = null, ?TransactionCurrency $currency = null ): array { - $journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency); + $journals = $this->getTransactionsForSum(TransactionTypeEnum::DEPOSIT->value, $start, $end, $accounts, $revenue, $currency); return $this->groupByCurrency($journals, 'positive'); } @@ -343,7 +284,7 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $revenue = null, ?TransactionCurrency $currency = null ): array { - $journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency); + $journals = $this->getTransactionsForSum(TransactionTypeEnum::DEPOSIT->value, $start, $end, $accounts, $revenue, $currency); return $this->groupByDirection($journals, 'destination', 'positive'); } @@ -358,14 +299,14 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $revenue = null, ?TransactionCurrency $currency = null ): array { - $journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency); + $journals = $this->getTransactionsForSum(TransactionTypeEnum::DEPOSIT->value, $start, $end, $accounts, $revenue, $currency); return $this->groupByDirection($journals, 'source', 'positive'); } public function sumTransfers(Carbon $start, Carbon $end, ?Collection $accounts = null, ?TransactionCurrency $currency = null): array { - $journals = $this->getTransactionsForSum(TransactionType::TRANSFER, $start, $end, $accounts, null, $currency); + $journals = $this->getTransactionsForSum(TransactionTypeEnum::TRANSFER->value, $start, $end, $accounts, null, $currency); return $this->groupByEither($journals); } @@ -380,9 +321,9 @@ class OperationsRepository implements OperationsRepositoryInterface } $final = []; foreach ($return as $array) { - $array['difference_float'] = (float)$array['difference']; - $array['in_float'] = (float)$array['in']; - $array['out_float'] = (float)$array['out']; + $array['difference_float'] = (float) $array['difference']; + $array['in_float'] = (float) $array['in']; + $array['out_float'] = (float) $array['out']; $final[] = $array; } @@ -400,7 +341,7 @@ class OperationsRepository implements OperationsRepositoryInterface // source first $return[$sourceKey] ??= [ - 'id' => (string)$sourceId, + 'id' => (string) $sourceId, 'name' => $journal['source_account_name'], 'difference' => '0', 'difference_float' => 0, @@ -408,13 +349,13 @@ class OperationsRepository implements OperationsRepositoryInterface 'in_float' => 0, 'out' => '0', 'out_float' => 0, - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_code' => $journal['currency_code'], ]; // dest next: $return[$destKey] ??= [ - 'id' => (string)$destinationId, + 'id' => (string) $destinationId, 'name' => $journal['destination_account_name'], 'difference' => '0', 'difference_float' => 0, @@ -422,7 +363,7 @@ class OperationsRepository implements OperationsRepositoryInterface 'in_float' => 0, 'out' => '0', 'out_float' => 0, - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_code' => $journal['currency_code'], ]; @@ -444,7 +385,7 @@ class OperationsRepository implements OperationsRepositoryInterface // same as above: // source first $return[$sourceKey] ??= [ - 'id' => (string)$sourceId, + 'id' => (string) $sourceId, 'name' => $journal['source_account_name'], 'difference' => '0', 'difference_float' => 0, @@ -452,13 +393,13 @@ class OperationsRepository implements OperationsRepositoryInterface 'in_float' => 0, 'out' => '0', 'out_float' => 0, - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_code' => $journal['foreign_currency_code'], ]; // dest next: $return[$destKey] ??= [ - 'id' => (string)$destinationId, + 'id' => (string) $destinationId, 'name' => $journal['destination_account_name'], 'difference' => '0', 'difference_float' => 0, @@ -466,7 +407,7 @@ class OperationsRepository implements OperationsRepositoryInterface 'in_float' => 0, 'out' => '0', 'out_float' => 0, - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_code' => $journal['foreign_currency_code'], ]; // source account? money goes out! (same as above) diff --git a/app/Repositories/Attachment/AttachmentRepository.php b/app/Repositories/Attachment/AttachmentRepository.php index e9e69d1f1d..a1744a3dac 100644 --- a/app/Repositories/Attachment/AttachmentRepository.php +++ b/app/Repositories/Attachment/AttachmentRepository.php @@ -71,7 +71,7 @@ class AttachmentRepository implements AttachmentRepositoryInterface $unencryptedContent = ''; if ($disk->exists($file)) { - $encryptedContent = (string)$disk->get($file); + $encryptedContent = (string) $disk->get($file); try { $unencryptedContent = \Crypt::decrypt($encryptedContent); // verified @@ -104,7 +104,7 @@ class AttachmentRepository implements AttachmentRepositoryInterface { $note = $attachment->notes()->first(); if (null !== $note) { - return (string)$note->text; + return (string) $note->text; } return null; @@ -139,20 +139,20 @@ class AttachmentRepository implements AttachmentRepositoryInterface $attachment->title = $data['title']; } - if (array_key_exists('filename', $data) && '' !== (string)$data['filename'] && $data['filename'] !== $attachment->filename) { + if (array_key_exists('filename', $data) && '' !== (string) $data['filename'] && $data['filename'] !== $attachment->filename) { $attachment->filename = $data['filename']; } // update model (move attachment) // should be validated already: if (array_key_exists('attachable_type', $data) && array_key_exists('attachable_id', $data)) { - $attachment->attachable_id = (int)$data['attachable_id']; + $attachment->attachable_id = (int) $data['attachable_id']; $attachment->attachable_type = sprintf('FireflyIII\Models\%s', $data['attachable_type']); } $attachment->save(); $attachment->refresh(); if (array_key_exists('notes', $data)) { - $this->updateNote($attachment, (string)$data['notes']); + $this->updateNote($attachment, (string) $data['notes']); } return $attachment; diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 28b4dae6f1..b0d63df1d9 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -37,6 +37,7 @@ use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; use FireflyIII\Services\Internal\Destroy\BillDestroyService; use FireflyIII\Services\Internal\Update\BillUpdateService; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Facades\Amount; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Query\JoinClause; @@ -240,7 +241,7 @@ class BillRepository implements BillRepositoryInterface /** @var null|Note $note */ $note = $bill->notes()->first(); - return (string)$note?->text; + return (string) $note?->text; } public function getOverallAverage(Bill $bill): array @@ -256,19 +257,25 @@ class BillRepository implements BillRepositoryInterface /** @var TransactionJournal $journal */ foreach ($journals as $journal) { /** @var Transaction $transaction */ - $transaction = $journal->transactions()->where('amount', '<', 0)->first(); - $currencyId = (int)$journal->transaction_currency_id; - $currency = $journal->transactionCurrency; + $transaction = $journal->transactions()->where('amount', '<', 0)->first(); + $currencyId = (int) $journal->transaction_currency_id; + $currency = $journal->transactionCurrency; $result[$currencyId] ??= [ 'sum' => '0', + 'native_sum' => '0', 'count' => 0, 'avg' => '0', + 'native_avg' => '0', 'currency_id' => $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, ]; - $result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], $transaction->amount); + $result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], $transaction->amount); + $result[$currencyId]['native_sum'] = bcadd($result[$currencyId]['native_sum'], $transaction->native_amount ?? '0'); + if ($journal->foreign_currency_id === Amount::getDefaultCurrency()->id) { + $result[$currencyId]['native_sum'] = bcadd($result[$currencyId]['native_sum'], $transaction->amount); + } ++$result[$currencyId]['count']; } @@ -278,7 +285,8 @@ class BillRepository implements BillRepositoryInterface * @var array $arr */ foreach ($result as $currencyId => $arr) { - $result[$currencyId]['avg'] = bcdiv($arr['sum'], (string)$arr['count']); + $result[$currencyId]['avg'] = bcdiv($arr['sum'], (string) $arr['count']); + $result[$currencyId]['native_avg'] = bcdiv($arr['native_sum'], (string) $arr['count']); } return $result; @@ -378,14 +386,15 @@ class BillRepository implements BillRepositoryInterface /** @var TransactionJournal $journal */ foreach ($journals as $journal) { /** @var null|Transaction $transaction */ - $transaction = $journal->transactions()->where('amount', '<', 0)->first(); + $transaction = $journal->transactions()->where('amount', '<', 0)->first(); if (null === $transaction) { continue; } - $currencyId = (int)$journal->transaction_currency_id; - $currency = $journal->transactionCurrency; + $currencyId = (int) $journal->transaction_currency_id; + $currency = $journal->transactionCurrency; $result[$currencyId] ??= [ 'sum' => '0', + 'native_sum' => '0', 'count' => 0, 'avg' => '0', 'currency_id' => $currency->id, @@ -393,7 +402,11 @@ class BillRepository implements BillRepositoryInterface 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, ]; - $result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], $transaction->amount); + $result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], $transaction->amount); + $result[$currencyId]['native_sum'] = bcadd($result[$currencyId]['native_sum'], $transaction->native_amount ?? '0'); + if ($journal->foreign_currency_id === Amount::getDefaultCurrency()->id) { + $result[$currencyId]['native_sum'] = bcadd($result[$currencyId]['native_sum'], $transaction->amount); + } ++$result[$currencyId]['count']; } @@ -403,7 +416,8 @@ class BillRepository implements BillRepositoryInterface * @var array $arr */ foreach ($result as $currencyId => $arr) { - $result[$currencyId]['avg'] = bcdiv($arr['sum'], (string)$arr['count']); + $result[$currencyId]['avg'] = bcdiv($arr['sum'], (string) $arr['count']); + $result[$currencyId]['native_avg'] = bcdiv($arr['native_sum'], (string) $arr['count']); } return $result; @@ -416,7 +430,7 @@ class BillRepository implements BillRepositoryInterface { /** @var Transaction $transaction */ foreach ($transactions as $transaction) { - $journal = $bill->user->transactionJournals()->find((int)$transaction['transaction_journal_id']); + $journal = $bill->user->transactionJournals()->find((int) $transaction['transaction_journal_id']); $journal->bill_id = $bill->id; $journal->save(); app('log')->debug(sprintf('Linked journal #%d to bill #%d', $journal->id, $bill->id)); @@ -510,37 +524,35 @@ class BillRepository implements BillRepositoryInterface public function sumPaidInRange(Carbon $start, Carbon $end): array { - $bills = $this->getActiveBills(); - $return = []; + Log::debug(sprintf('sumPaidInRange from %s to %s', $start->toW3cString(), $end->toW3cString())); + $bills = $this->getActiveBills(); + $return = []; + $convertToNative = Amount::convertToNative($this->user); + $default = app('amount')->getDefaultCurrency(); /** @var Bill $bill */ foreach ($bills as $bill) { - /** @var Collection $set */ - $set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']); - $currency = $bill->transactionCurrency; - $return[$currency->id] ??= [ - 'id' => (string)$currency->id, + /** @var Collection $set */ + $set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']); + $currency = $convertToNative && $bill->transactionCurrency->id !== $default->id ? $default : $bill->transactionCurrency; + $return[(int) $currency->id] ??= [ + 'id' => (string) $currency->id, 'name' => $currency->name, 'symbol' => $currency->symbol, 'code' => $currency->code, 'decimal_places' => $currency->decimal_places, 'sum' => '0', ]; + $setAmount = '0'; /** @var TransactionJournal $transactionJournal */ foreach ($set as $transactionJournal) { - /** @var null|Transaction $sourceTransaction */ - $sourceTransaction = $transactionJournal->transactions()->where('amount', '<', 0)->first(); - if (null !== $sourceTransaction) { - $amount = $sourceTransaction->amount; - if ((int)$sourceTransaction->foreign_currency_id === $currency->id) { - // use foreign amount instead! - $amount = (string)$sourceTransaction->foreign_amount; - } - $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount); - } + $setAmount = bcadd($setAmount, Amount::getAmountFromJournalObject($transactionJournal)); } + Log::debug(sprintf('Bill #%d ("%s") with %d transaction(s) and sum %s %s', $bill->id, $bill->name, $set->count(), $currency->code, $setAmount)); + $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $setAmount); + Log::debug(sprintf('Total sum is now %s', $return[$currency->id]['sum'])); } return $return; @@ -558,30 +570,37 @@ class BillRepository implements BillRepositoryInterface public function sumUnpaidInRange(Carbon $start, Carbon $end): array { app('log')->debug(sprintf('Now in sumUnpaidInRange("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'))); - $bills = $this->getActiveBills(); - $return = []; + $bills = $this->getActiveBills(); + $return = []; + $convertToNative = Amount::convertToNative($this->user); + $default = app('amount')->getDefaultCurrency(); /** @var Bill $bill */ foreach ($bills as $bill) { // app('log')->debug(sprintf('Processing bill #%d ("%s")', $bill->id, $bill->name)); - $dates = $this->getPayDatesInRange($bill, $start, $end); - $count = $bill->transactionJournals()->after($start)->before($end)->count(); - $total = $dates->count() - $count; + $dates = $this->getPayDatesInRange($bill, $start, $end); + $count = $bill->transactionJournals()->after($start)->before($end)->count(); + $total = $dates->count() - $count; // app('log')->debug(sprintf('Pay dates: %d, count: %d, left: %d', $dates->count(), $count, $total)); // app('log')->debug('dates', $dates->toArray()); + $minField = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_amount_min' : 'amount_min'; + $maxField = $convertToNative && $bill->transactionCurrency->id !== $default->id ? 'native_amount_max' : 'amount_max'; + Log::debug(sprintf('min field is %s, max field is %s', $minField, $maxField)); + if ($total > 0) { - $currency = $bill->transactionCurrency; - $average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2'); + $currency = $convertToNative && $bill->transactionCurrency->id !== $default->id ? $default : $bill->transactionCurrency; + $average = bcdiv(bcadd($bill->{$maxField} ?? '0', $bill->{$minField} ?? '0'), '2'); + Log::debug(sprintf('Amount to pay is %s %s (%d times)', $currency->code, $average, $total)); $return[$currency->id] ??= [ - 'id' => (string)$currency->id, + 'id' => (string) $currency->id, 'name' => $currency->name, 'symbol' => $currency->symbol, 'code' => $currency->code, 'decimal_places' => $currency->decimal_places, 'sum' => '0', ]; - $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], bcmul($average, (string)$total)); + $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], bcmul($average, (string) $total)); } } diff --git a/app/Repositories/Budget/AvailableBudgetRepository.php b/app/Repositories/Budget/AvailableBudgetRepository.php index fc1e0ee5cd..3a990a3b18 100644 --- a/app/Repositories/Budget/AvailableBudgetRepository.php +++ b/app/Repositories/Budget/AvailableBudgetRepository.php @@ -27,6 +27,7 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Support\Facades\Amount; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Builder; @@ -63,7 +64,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface */ public function get(?Carbon $start = null, ?Carbon $end = null): Collection { - $query = $this->user->availableBudgets()->with(['transactionCurrency']); + $query = $this->user->availableBudgets()->with(['transactionCurrency']); if (null !== $start && null !== $end) { $query->where( static function (Builder $q1) use ($start, $end): void { // @phpstan-ignore-line @@ -72,8 +73,10 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface } ); } + $result = $query->get(['available_budgets.*']); + Log::debug(sprintf('Found %d available budgets between %s and %s', $result->count(), $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); - return $query->get(['available_budgets.*']); + return $result; } /** @@ -127,15 +130,26 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface public function getAvailableBudgetWithCurrency(Carbon $start, Carbon $end): array { + Log::debug(sprintf('Now in %s(%s, %s)', __METHOD__, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); $return = []; $availableBudgets = $this->user->availableBudgets() - ->where('start_date', $start->format('Y-m-d')) - ->where('end_date', $end->format('Y-m-d'))->get() + ->where('start_date', $start->format('Y-m-d H:i:s')) + ->where('end_date', $end->format('Y-m-d H:i:s'))->get() ; + Log::debug(sprintf('Found %d available budgets', $availableBudgets->count())); + + // use native amount if necessary? + $convertToNative = Amount::convertToNative($this->user); + $default = Amount::getDefaultCurrency(); + /** @var AvailableBudget $availableBudget */ foreach ($availableBudgets as $availableBudget) { - $return[$availableBudget->transaction_currency_id] = $availableBudget->amount; + $currencyId = $convertToNative && $availableBudget->transaction_currency_id !== $default->id ? $default->id : $availableBudget->transaction_currency_id; + $field = $convertToNative && $availableBudget->transaction_currency_id !== $default->id ? 'native_amount' : 'amount'; + $return[$currencyId] ??= '0'; + $return[$currencyId] = bcadd($return[$currencyId], $availableBudget->{$field}); + Log::debug(sprintf('Add #%d %s (%s) for a total of %s', $currencyId, $availableBudget->{$field}, $field, $return[$currencyId])); } return $return; @@ -202,9 +216,9 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface $availableBudget = new AvailableBudget(); $availableBudget->user()->associate($this->user); $availableBudget->transactionCurrency()->associate($currency); - $availableBudget->start_date = $start->startOfDay()->format('Y-m-d'); // @phpstan-ignore-line + $availableBudget->start_date = $start->startOfDay(); $availableBudget->start_date_tz = $start->format('e'); - $availableBudget->end_date = $end->endOfDay()->format('Y-m-d'); // @phpstan-ignore-line + $availableBudget->end_date = $end->endOfDay(); $availableBudget->end_date_tz = $end->format('e'); } $availableBudget->amount = $amount; @@ -237,9 +251,9 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface 'user_group_id' => $this->user->user_group_id, 'transaction_currency_id' => $data['currency_id'], 'amount' => $data['amount'], - 'start_date' => $start->format('Y-m-d'), + 'start_date' => $start, 'start_date_tz' => $start->format('e'), - 'end_date' => $end->format('Y-m-d'), + 'end_date' => $end, 'end_date_tz' => $end->format('e'), ] ); @@ -261,7 +275,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface $start = $data['start']; if ($start instanceof Carbon) { $start = $data['start']->startOfDay(); - $availableBudget->start_date = $start->format('Y-m-d'); + $availableBudget->start_date = $start; $availableBudget->start_date_tz = $start->format('e'); $availableBudget->save(); } @@ -271,7 +285,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface $end = $data['end']; if ($end instanceof Carbon) { $end = $data['end']->endOfDay(); - $availableBudget->end_date = $end->format('Y-m-d'); + $availableBudget->end_date = $end; $availableBudget->end_date_tz = $end->format('e'); $availableBudget->save(); } diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php index 6921968dc3..c976bb6941 100644 --- a/app/Repositories/Budget/BudgetLimitRepository.php +++ b/app/Repositories/Budget/BudgetLimitRepository.php @@ -29,6 +29,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\Note; use FireflyIII\Models\TransactionCurrency; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; @@ -255,6 +256,12 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface ; } + #[\Override] + public function getNoteText(BudgetLimit $budgetLimit): string + { + return (string) $budgetLimit->notes()->first()?->text; + } + public function setUser(null|Authenticatable|User $user): void { if ($user instanceof User) { @@ -303,6 +310,12 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface $limit->amount = $data['amount']; $limit->transaction_currency_id = $currency->id; $limit->save(); + + $noteText = (string) ($data['notes'] ?? ''); + if ('' !== $noteText) { + $this->setNoteText($limit, $noteText); + } + app('log')->debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount'])); return $limit; @@ -317,6 +330,23 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface ; } + #[\Override] + public function setNoteText(BudgetLimit $budgetLimit, string $text): void + { + $dbNote = $budgetLimit->notes()->first(); + if ('' !== $text) { + if (null === $dbNote) { + $dbNote = new Note(); + $dbNote->noteable()->associate($budgetLimit); + } + $dbNote->text = trim($text); + $dbNote->save(); + + return; + } + $dbNote?->delete(); + } + /** * @throws FireflyException */ @@ -353,6 +383,11 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface $budgetLimit->transaction_currency_id = $currency->id; $budgetLimit->save(); + // update notes if they exist. + if (array_key_exists('notes', $data)) { + $this->setNoteText($budgetLimit, (string) $data['notes']); + } + return $budgetLimit; } diff --git a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php index 9bb52abb15..bb101e2068 100644 --- a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php @@ -64,6 +64,10 @@ interface BudgetLimitRepositoryInterface public function getBudgetLimits(Budget $budget, ?Carbon $start = null, ?Carbon $end = null): Collection; + public function getNoteText(BudgetLimit $budgetLimit): string; + + public function setNoteText(BudgetLimit $budgetLimit, string $text): void; + public function setUser(null|Authenticatable|User $user): void; public function store(array $data): BudgetLimit; diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 6a5369a350..ea9c4f3586 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -103,12 +103,12 @@ class BudgetRepository implements BudgetRepositoryInterface $rate = $converter->getCurrencyRate($currency, $defaultCurrency, $end); $currencyCode = $currency->code; $return[$currencyCode] ??= [ - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_name' => $currency->name, 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, 'currency_decimal_places' => $currency->decimal_places, - 'native_currency_id' => (string)$defaultCurrency->id, + 'native_currency_id' => (string) $defaultCurrency->id, 'native_currency_name' => $defaultCurrency->name, 'native_currency_symbol' => $defaultCurrency->symbol, 'native_currency_code' => $defaultCurrency->code, @@ -134,13 +134,13 @@ class BudgetRepository implements BudgetRepositoryInterface } $total = $limit->start_date->diffInDays($limit->end_date, true) + 1; // include the day itself. $days = $this->daysInOverlap($limit, $start, $end); - $amount = bcmul(bcdiv($limit->amount, (string)$total), (string)$days); + $amount = bcmul(bcdiv($limit->amount, (string) $total), (string) $days); $return[$currencyCode]['sum'] = bcadd($return[$currencyCode]['sum'], $amount); $return[$currencyCode]['native_sum'] = bcmul($rate, $return[$currencyCode]['sum']); app('log')->debug( sprintf( 'Amount per day: %s (%s over %d days). Total amount for %d days: %s', - bcdiv($limit->amount, (string)$total), + bcdiv($limit->amount, (string) $total), $limit->amount, $total, $days, @@ -183,21 +183,21 @@ class BudgetRepository implements BudgetRepositoryInterface // |-----------| // |----------------| if ($start->gte($limit->start_date) && $end->lte($limit->end_date)) { - return (int)$start->diffInDays($end, true) + 1; // add one day + return (int) $start->diffInDays($end, true) + 1; // add one day } // limit starts earlier and limit ends first: // |-----------| // |-------| if ($limit->start_date->lte($start) && $limit->end_date->lte($end)) { // return days in the range $start-$limit_end - return (int)$start->diffInDays($limit->end_date, true) + 1; // add one day, the day itself + return (int) $start->diffInDays($limit->end_date, true) + 1; // add one day, the day itself } // limit starts later and limit ends earlier // |-----------| // |-------| if ($limit->start_date->gte($start) && $limit->end_date->gte($end)) { // return days in the range $limit_start - $end - return (int)$limit->start_date->diffInDays($end, true) + 1; // add one day, the day itself + return (int) $limit->start_date->diffInDays($end, true) + 1; // add one day, the day itself } return 0; @@ -220,7 +220,7 @@ class BudgetRepository implements BudgetRepositoryInterface app('log')->debug(sprintf('Budget limit #%d', $limit->id)); $currency = $limit->transactionCurrency; $return[$currency->id] ??= [ - 'id' => (string)$currency->id, + 'id' => (string) $currency->id, 'name' => $currency->name, 'symbol' => $currency->symbol, 'code' => $currency->code, @@ -243,12 +243,12 @@ class BudgetRepository implements BudgetRepositoryInterface } $total = $limit->start_date->diffInDays($limit->end_date) + 1; // include the day itself. $days = $this->daysInOverlap($limit, $start, $end); - $amount = bcmul(bcdiv($limit->amount, (string)$total), (string)$days); + $amount = bcmul(bcdiv($limit->amount, (string) $total), (string) $days); $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount); app('log')->debug( sprintf( 'Amount per day: %s (%s over %d days). Total amount for %d days: %s', - bcdiv($limit->amount, (string)$total), + bcdiv($limit->amount, (string) $total), $limit->amount, $total, $days, @@ -297,7 +297,7 @@ class BudgetRepository implements BudgetRepositoryInterface $budget->active = $data['active']; } if (array_key_exists('notes', $data)) { - $this->setNoteText($budget, (string)$data['notes']); + $this->setNoteText($budget, (string) $data['notes']); } $budget->save(); @@ -408,8 +408,8 @@ class BudgetRepository implements BudgetRepositoryInterface if (array_key_exists('currency_id', $data) || array_key_exists('currency_code', $data)) { /** @var CurrencyRepositoryInterface $repos */ $repos = app(CurrencyRepositoryInterface::class); - $currencyId = (int)($data['currency_id'] ?? 0); - $currencyCode = (string)($data['currency_code'] ?? ''); + $currencyId = (int) ($data['currency_id'] ?? 0); + $currencyCode = (string) ($data['currency_code'] ?? ''); $currency = $repos->find($currencyId); if (null === $currency) { $currency = $repos->findByCode($currencyCode); @@ -463,8 +463,8 @@ class BudgetRepository implements BudgetRepositoryInterface foreach ($budgets as $budget) { \DB::table('budget_transaction')->where('budget_id', $budget->id)->delete(); \DB::table('budget_transaction_journal')->where('budget_id', $budget->id)->delete(); - RecurrenceTransactionMeta::where('name', 'budget_id')->where('value', (string)$budget->id)->delete(); - RuleAction::where('action_type', 'set_budget')->where('action_value', (string)$budget->id)->delete(); + RecurrenceTransactionMeta::where('name', 'budget_id')->where('value', (string) $budget->id)->delete(); + RuleAction::where('action_type', 'set_budget')->where('action_value', (string) $budget->id)->delete(); $budget->delete(); } Log::channel('audit')->info('Delete all budgets through destroyAll'); @@ -489,7 +489,7 @@ class BudgetRepository implements BudgetRepositoryInterface { app('log')->debug('Now in findBudget()'); app('log')->debug(sprintf('Searching for budget with ID #%d...', $budgetId)); - $result = $this->find((int)$budgetId); + $result = $this->find((int) $budgetId); if (null === $result && null !== $budgetName && '' !== $budgetName) { app('log')->debug(sprintf('Searching for budget with name %s...', $budgetName)); $result = $this->findByName($budgetName); @@ -625,9 +625,9 @@ class BudgetRepository implements BudgetRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ - 'id' => (string)$currencyId, + 'id' => (string) $currencyId, 'name' => $journal['currency_name'], 'symbol' => $journal['currency_symbol'], 'code' => $journal['currency_code'], @@ -637,10 +637,10 @@ class BudgetRepository implements BudgetRepositoryInterface $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'])); // also do foreign amount: - $foreignId = (int)$journal['foreign_currency_id']; + $foreignId = (int) $journal['foreign_currency_id']; if (0 !== $foreignId) { $array[$foreignId] ??= [ - 'id' => (string)$foreignId, + 'id' => (string) $foreignId, 'name' => $journal['foreign_currency_name'], 'symbol' => $journal['foreign_currency_symbol'], 'code' => $journal['foreign_currency_code'], @@ -687,9 +687,9 @@ class BudgetRepository implements BudgetRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ - 'id' => (string)$currencyId, + 'id' => (string) $currencyId, 'name' => $journal['currency_name'], 'symbol' => $journal['currency_symbol'], 'code' => $journal['currency_code'], @@ -699,10 +699,10 @@ class BudgetRepository implements BudgetRepositoryInterface $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'])); // also do foreign amount: - $foreignId = (int)$journal['foreign_currency_id']; + $foreignId = (int) $journal['foreign_currency_id']; if (0 !== $foreignId) { $array[$foreignId] ??= [ - 'id' => (string)$foreignId, + 'id' => (string) $foreignId, 'name' => $journal['foreign_currency_name'], 'symbol' => $journal['foreign_currency_symbol'], 'code' => $journal['foreign_currency_code'], @@ -744,7 +744,7 @@ class BudgetRepository implements BudgetRepositoryInterface // set notes if (array_key_exists('notes', $data)) { - $this->setNoteText($newBudget, (string)$data['notes']); + $this->setNoteText($newBudget, (string) $data['notes']); } if (!array_key_exists('auto_budget_type', $data) || !array_key_exists('auto_budget_amount', $data) || !array_key_exists('auto_budget_period', $data)) { @@ -772,10 +772,10 @@ class BudgetRepository implements BudgetRepositoryInterface $repos = app(CurrencyRepositoryInterface::class); $currency = null; if (array_key_exists('currency_id', $data)) { - $currency = $repos->find((int)$data['currency_id']); + $currency = $repos->find((int) $data['currency_id']); } if (array_key_exists('currency_code', $data)) { - $currency = $repos->findByCode((string)$data['currency_code']); + $currency = $repos->findByCode((string) $data['currency_code']); } if (null === $currency) { $currency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); @@ -811,6 +811,6 @@ class BudgetRepository implements BudgetRepositoryInterface public function getMaxOrder(): int { - return (int)$this->user->budgets()->max('order'); + return (int) $this->user->budgets()->max('order'); } } diff --git a/app/Repositories/Budget/NoBudgetRepository.php b/app/Repositories/Budget/NoBudgetRepository.php index 9708913db4..7e50046aca 100644 --- a/app/Repositories/Budget/NoBudgetRepository.php +++ b/app/Repositories/Budget/NoBudgetRepository.php @@ -25,9 +25,11 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Report\Summarizer\TransactionSummarizer; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; @@ -48,14 +50,14 @@ class NoBudgetRepository implements NoBudgetRepositoryInterface $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end); - $collector->setTypes([TransactionType::WITHDRAWAL]); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); $collector->withoutBudget(); $journals = $collector->getExtractedJournals(); $data = []; /** @var array $journal */ foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $data[$currencyId] ??= [ 'id' => 0, @@ -79,54 +81,6 @@ class NoBudgetRepository implements NoBudgetRepositoryInterface return $data; } - /** - * @deprecated - */ - public function spentInPeriodWoBudgetMc(Collection $accounts, Carbon $start, Carbon $end): array - { - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - - $collector->setUser($this->user); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutBudget(); - - if ($accounts->count() > 0) { - $collector->setAccounts($accounts); - } - $journals = $collector->getExtractedJournals(); - $return = []; - $total = []; - $currencies = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - $code = $journal['currency_code']; - if (!array_key_exists($code, $currencies)) { - $currencies[$code] = [ - 'id' => $journal['currency_id'], - 'name' => $journal['currency_name'], - 'symbol' => $journal['currency_symbol'], - 'decimal_places' => $journal['currency_decimal_places'], - ]; - } - $total[$code] = array_key_exists($code, $total) ? bcadd($total[$code], $journal['amount']) : $journal['amount']; - } - foreach ($total as $code => $spent) { - /** @var TransactionCurrency $currency */ - $currency = $currencies[$code]; - $return[] = [ - 'currency_id' => (string)$currency['id'], - 'currency_code' => $code, - 'currency_name' => $currency['name'], - 'currency_symbol' => $currency['symbol'], - 'currency_decimal_places' => $currency['decimal_places'], - 'amount' => app('steam')->bcround($spent, $currency['decimal_places']), - ]; - } - - return $return; - } - public function setUser(null|Authenticatable|User $user): void { if ($user instanceof User) { @@ -134,14 +88,10 @@ class NoBudgetRepository implements NoBudgetRepositoryInterface } } - /** - * TODO this method does not include multi currency. It just counts. - * TODO this probably also applies to the other "sumExpenses" methods. - */ public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?TransactionCurrency $currency = null): array { /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]); if (null !== $accounts && $accounts->count() > 0) { @@ -152,22 +102,9 @@ class NoBudgetRepository implements NoBudgetRepositoryInterface } $collector->withoutBudget(); $collector->withBudgetInformation(); - $journals = $collector->getExtractedJournals(); - $array = []; + $journals = $collector->getExtractedJournals(); + $summarizer = new TransactionSummarizer($this->user); - foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $array[$currencyId] ??= [ - 'sum' => '0', - 'currency_id' => $currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - ]; - $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'])); - } - - return $array; + return $summarizer->groupByCurrencyId($journals); } } diff --git a/app/Repositories/Budget/NoBudgetRepositoryInterface.php b/app/Repositories/Budget/NoBudgetRepositoryInterface.php index e430d6e8cd..01b4d6d1f1 100644 --- a/app/Repositories/Budget/NoBudgetRepositoryInterface.php +++ b/app/Repositories/Budget/NoBudgetRepositoryInterface.php @@ -42,10 +42,5 @@ interface NoBudgetRepositoryInterface public function setUser(null|Authenticatable|User $user): void; - /** - * @deprecated - */ - public function spentInPeriodWoBudgetMc(Collection $accounts, Carbon $start, Carbon $end): array; - public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?TransactionCurrency $currency = null): array; } diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php index af1095f878..bc0e78304c 100644 --- a/app/Repositories/Budget/OperationsRepository.php +++ b/app/Repositories/Budget/OperationsRepository.php @@ -25,15 +25,19 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Budget; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Report\Summarizer\TransactionSummarizer; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class OperationsRepository @@ -52,17 +56,17 @@ class OperationsRepository implements OperationsRepositoryInterface $total = '0'; $count = 0; foreach ($budget->budgetlimits as $limit) { - $diff = (int)$limit->start_date->diffInDays($limit->end_date, true); + $diff = (int) $limit->start_date->diffInDays($limit->end_date, true); $diff = 0 === $diff ? 1 : $diff; $amount = $limit->amount; - $perDay = bcdiv($amount, (string)$diff); + $perDay = bcdiv($amount, (string) $diff); $total = bcadd($total, $perDay); ++$count; app('log')->debug(sprintf('Found %d budget limits. Per day is %s, total is %s', $count, $perDay, $total)); } $avg = $total; if ($count > 0) { - $avg = bcdiv($total, (string)$count); + $avg = bcdiv($total, (string) $count); } app('log')->debug(sprintf('%s / %d = %s = average.', $total, $count, $avg)); @@ -91,9 +95,9 @@ class OperationsRepository implements OperationsRepositoryInterface /** @var array $journal */ foreach ($journals as $journal) { // prep data array for currency: - $budgetId = (int)$journal['budget_id']; + $budgetId = (int) $journal['budget_id']; $budgetName = $journal['budget_name']; - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $key = sprintf('%d-%d', $budgetId, $currencyId); $data[$key] ??= [ @@ -138,9 +142,9 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $budgetId = (int)$journal['budget_id']; - $budgetName = (string)$journal['budget_name']; + $currencyId = (int) $journal['currency_id']; + $budgetId = (int) $journal['budget_id']; + $budgetName = (string) $journal['budget_name']; // catch "no category" entries. if (0 === $budgetId) { @@ -166,7 +170,7 @@ class OperationsRepository implements OperationsRepositoryInterface // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['budgets'][$budgetId]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->negative($journal['amount']), 'destination_account_id' => $journal['destination_account_id'], @@ -208,15 +212,12 @@ class OperationsRepository implements OperationsRepositoryInterface ?Collection $budgets = null, ?TransactionCurrency $currency = null ): array { - // app('log')->debug(sprintf('Now in %s', __METHOD__)); - $start->startOfDay(); - $end->endOfDay(); + Log::debug(sprintf('Start of %s.', __METHOD__)); + // this collector excludes all transfers TO liabilities (which are also withdrawals) + // because those expenses only become expenses once they move from the liability to the friend. + + // 2024-12-24 disable the exclusion for now. - // this collector excludes all transfers TO - // liabilities (which are also withdrawals) - // because those expenses only become expenses - // once they move from the liability to the friend. - // TODO this filter must be somewhere in AccountRepositoryInterface because I suspect its needed more often (A113) $repository = app(AccountRepositoryInterface::class); $repository->setUser($this->user); $subset = $repository->getAccountsByType(config('firefly.valid_liabilities')); @@ -233,8 +234,8 @@ class OperationsRepository implements OperationsRepositoryInterface $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user) ->setRange($start, $end) - ->excludeDestinationAccounts($selection) - ->setTypes([TransactionType::WITHDRAWAL]) + // ->excludeDestinationAccounts($selection) + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]) ; if (null !== $accounts) { @@ -244,57 +245,18 @@ class OperationsRepository implements OperationsRepositoryInterface $budgets = $this->getBudgets(); } if (null !== $currency) { - $collector->setCurrency($currency); + Log::debug(sprintf('Limit to currency %s', $currency->code)); + $collector->setNormalCurrency($currency); } $collector->setBudgets($budgets); $journals = $collector->getExtractedJournals(); - // same but for foreign currencies: + // same but for transactions in the foreign currency: if (null !== $currency) { - // app('log')->debug(sprintf('Currency is "%s".', $currency->name)); - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]) - ->setForeignCurrency($currency)->setBudgets($budgets) - ; - - if (null !== $accounts) { - $collector->setAccounts($accounts); - } - $result = $collector->getExtractedJournals(); - // app('log')->debug(sprintf('Found %d journals with currency %s.', count($result), $currency->code)); - // do not use array_merge because you want keys to overwrite (otherwise you get double results): - $journals = $result + $journals; + Log::debug('STOP looking for transactions in the foreign currency.'); } - $array = []; + $summarizer = new TransactionSummarizer($this->user); - foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $array[$currencyId] ??= [ - 'sum' => '0', - 'currency_id' => $currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - ]; - $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'])); - - // also do foreign amount: - $foreignId = (int)$journal['foreign_currency_id']; - if (0 !== $foreignId) { - $array[$foreignId] ??= [ - 'sum' => '0', - 'currency_id' => $foreignId, - 'currency_name' => $journal['foreign_currency_name'], - 'currency_symbol' => $journal['foreign_currency_symbol'], - 'currency_code' => $journal['foreign_currency_code'], - 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], - ]; - $array[$foreignId]['sum'] = bcadd($array[$foreignId]['sum'], app('steam')->negative($journal['foreign_amount'])); - } - } - - return $array; + return $summarizer->groupByCurrencyId($journals); } } diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index 86ad7e3f24..7501fcbdde 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -107,11 +107,11 @@ class CategoryRepository implements CategoryRepositoryInterface { app('log')->debug('Now in findCategory()'); app('log')->debug(sprintf('Searching for category with ID #%d...', $categoryId)); - $result = $this->find((int)$categoryId); + $result = $this->find((int) $categoryId); if (null === $result) { app('log')->debug(sprintf('Searching for category with name %s...', $categoryName)); - $result = $this->findByName((string)$categoryName); - if (null === $result && '' !== (string)$categoryName) { + $result = $this->findByName((string) $categoryName); + if (null === $result && '' !== (string) $categoryName) { // create it! $result = $this->store(['name' => $categoryName]); } diff --git a/app/Repositories/Category/NoCategoryRepository.php b/app/Repositories/Category/NoCategoryRepository.php index b4d8a0b417..6cc823a29d 100644 --- a/app/Repositories/Category/NoCategoryRepository.php +++ b/app/Repositories/Category/NoCategoryRepository.php @@ -27,6 +27,7 @@ namespace FireflyIII\Repositories\Category; use Carbon\Carbon; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Report\Summarizer\TransactionSummarizer; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; @@ -55,7 +56,7 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ 'categories' => [], 'currency_id' => $currencyId, @@ -67,13 +68,13 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface // info about the non-existent category: $array[$currencyId]['categories'][0] ??= [ 'id' => 0, - 'name' => (string)trans('firefly.noCategory'), + 'name' => (string) trans('firefly.noCategory'), 'transaction_journals' => [], ]; // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['categories'][0]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->negative($journal['amount']), @@ -108,7 +109,7 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ 'categories' => [], 'currency_id' => $currencyId, @@ -121,12 +122,12 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface // info about the non-existent category: $array[$currencyId]['categories'][0] ??= [ 'id' => 0, - 'name' => (string)trans('firefly.noCategory'), + 'name' => (string) trans('firefly.noCategory'), 'transaction_journals' => [], ]; // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['categories'][0]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->positive($journal['amount']), @@ -143,29 +144,16 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null): array { /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutCategory(); if (null !== $accounts && $accounts->count() > 0) { $collector->setAccounts($accounts); } - $journals = $collector->getExtractedJournals(); - $array = []; + $journals = $collector->getExtractedJournals(); + $summarizer = new TransactionSummarizer($this->user); - foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $array[$currencyId] ??= [ - 'sum' => '0', - 'currency_id' => $currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - ]; - $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'] ?? '0')); - } - - return $array; + return $summarizer->groupByCurrencyId($journals); } /** @@ -184,7 +172,7 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ 'sum' => '0', 'currency_id' => $currencyId, @@ -212,7 +200,7 @@ class NoCategoryRepository implements NoCategoryRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ 'sum' => '0', 'currency_id' => $currencyId, diff --git a/app/Repositories/Category/OperationsRepository.php b/app/Repositories/Category/OperationsRepository.php index 33c9cc7240..26db0fba10 100644 --- a/app/Repositories/Category/OperationsRepository.php +++ b/app/Repositories/Category/OperationsRepository.php @@ -25,11 +25,15 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Category; use Carbon\Carbon; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Report\Summarizer\TransactionSummarizer; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class OperationsRepository @@ -64,9 +68,9 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $categoryId = (int)$journal['category_id']; - $categoryName = (string)$journal['category_name']; + $currencyId = (int) $journal['currency_id']; + $categoryId = (int) $journal['category_id']; + $categoryName = (string) $journal['category_name']; // catch "no category" entries. if (0 === $categoryId) { @@ -76,7 +80,7 @@ class OperationsRepository implements OperationsRepositoryInterface // info about the currency: $array[$currencyId] ??= [ 'categories' => [], - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], @@ -85,24 +89,24 @@ class OperationsRepository implements OperationsRepositoryInterface // info about the categories: $array[$currencyId]['categories'][$categoryId] ??= [ - 'id' => (string)$categoryId, + 'id' => (string) $categoryId, 'name' => $categoryName, 'transaction_journals' => [], ]; // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->negative($journal['amount']), 'date' => $journal['date'], - 'source_account_id' => (string)$journal['source_account_id'], + 'source_account_id' => (string) $journal['source_account_id'], 'budget_name' => $journal['budget_name'], 'source_account_name' => $journal['source_account_name'], - 'destination_account_id' => (string)$journal['destination_account_id'], + 'destination_account_id' => (string) $journal['destination_account_id'], 'destination_account_name' => $journal['destination_account_name'], 'description' => $journal['description'], - 'transaction_group_id' => (string)$journal['transaction_group_id'], + 'transaction_group_id' => (string) $journal['transaction_group_id'], ]; } @@ -148,19 +152,19 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $categoryId = (int)$journal['category_id']; - $categoryName = (string)$journal['category_name']; + $currencyId = (int) $journal['currency_id']; + $categoryId = (int) $journal['category_id']; + $categoryName = (string) $journal['category_name']; // catch "no category" entries. if (0 === $categoryId) { - $categoryName = (string)trans('firefly.no_category'); + $categoryName = (string) trans('firefly.no_category'); } // info about the currency: $array[$currencyId] ??= [ 'categories' => [], - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], @@ -169,23 +173,23 @@ class OperationsRepository implements OperationsRepositoryInterface // info about the categories: $array[$currencyId]['categories'][$categoryId] ??= [ - 'id' => (string)$categoryId, + 'id' => (string) $categoryId, 'name' => $categoryName, 'transaction_journals' => [], ]; // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->positive($journal['amount']), 'date' => $journal['date'], - 'source_account_id' => (string)$journal['source_account_id'], - 'destination_account_id' => (string)$journal['destination_account_id'], + 'source_account_id' => (string) $journal['source_account_id'], + 'destination_account_id' => (string) $journal['destination_account_id'], 'source_account_name' => $journal['source_account_name'], 'destination_account_name' => $journal['destination_account_name'], 'description' => $journal['description'], - 'transaction_group_id' => (string)$journal['transaction_group_id'], + 'transaction_group_id' => (string) $journal['transaction_group_id'], ]; } @@ -210,9 +214,9 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $categoryId = (int)$journal['category_id']; - $categoryName = (string)$journal['category_name']; + $currencyId = (int) $journal['currency_id']; + $categoryId = (int) $journal['category_id']; + $categoryName = (string) $journal['category_name']; // catch "no category" entries. if (0 === $categoryId) { @@ -222,7 +226,7 @@ class OperationsRepository implements OperationsRepositoryInterface // info about the currency: $array[$currencyId] ??= [ 'categories' => [], - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], @@ -231,24 +235,24 @@ class OperationsRepository implements OperationsRepositoryInterface // info about the categories: $array[$currencyId]['categories'][$categoryId] ??= [ - 'id' => (string)$categoryId, + 'id' => (string) $categoryId, 'name' => $categoryName, 'transaction_journals' => [], ]; // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->positive($journal['amount']), 'date' => $journal['date'], - 'source_account_id' => (string)$journal['source_account_id'], + 'source_account_id' => (string) $journal['source_account_id'], 'category_name' => $journal['category_name'], 'source_account_name' => $journal['source_account_name'], - 'destination_account_id' => (string)$journal['destination_account_id'], + 'destination_account_id' => (string) $journal['destination_account_id'], 'destination_account_name' => $journal['destination_account_name'], 'description' => $journal['description'], - 'transaction_group_id' => (string)$journal['transaction_group_id'], + 'transaction_group_id' => (string) $journal['transaction_group_id'], ]; } @@ -273,9 +277,9 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $categoryId = (int)$journal['category_id']; - $categoryName = (string)$journal['category_name']; + $currencyId = (int) $journal['currency_id']; + $categoryId = (int) $journal['category_id']; + $categoryName = (string) $journal['category_name']; // catch "no category" entries. if (0 === $categoryId) { @@ -285,7 +289,7 @@ class OperationsRepository implements OperationsRepositoryInterface // info about the currency: $array[$currencyId] ??= [ 'categories' => [], - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], @@ -294,24 +298,24 @@ class OperationsRepository implements OperationsRepositoryInterface // info about the categories: $array[$currencyId]['categories'][$categoryId] ??= [ - 'id' => (string)$categoryId, + 'id' => (string) $categoryId, 'name' => $categoryName, 'transaction_journals' => [], ]; // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->negative($journal['amount']), 'date' => $journal['date'], - 'source_account_id' => (string)$journal['source_account_id'], + 'source_account_id' => (string) $journal['source_account_id'], 'category_name' => $journal['category_name'], 'source_account_name' => $journal['source_account_name'], - 'destination_account_id' => (string)$journal['destination_account_id'], + 'destination_account_id' => (string) $journal['destination_account_id'], 'destination_account_name' => $journal['destination_account_name'], 'description' => $journal['description'], - 'transaction_group_id' => (string)$journal['transaction_group_id'], + 'transaction_group_id' => (string) $journal['transaction_group_id'], ]; } @@ -324,10 +328,8 @@ class OperationsRepository implements OperationsRepositoryInterface public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array { /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setUser($this->user)->setRange($start, $end) - ->setTypes([TransactionType::WITHDRAWAL]) - ; + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); if (null !== $accounts && $accounts->count() > 0) { $collector->setAccounts($accounts); @@ -337,23 +339,10 @@ class OperationsRepository implements OperationsRepositoryInterface } $collector->setCategories($categories); $collector->withCategoryInformation(); - $journals = $collector->getExtractedJournals(); - $array = []; + $journals = $collector->getExtractedJournals(); + $summarizer = new TransactionSummarizer($this->user); - foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $array[$currencyId] ??= [ - 'sum' => '0', - 'currency_id' => (string)$currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => (int)$journal['currency_decimal_places'], - ]; - $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'])); - } - - return $array; + return $summarizer->groupByCurrencyId($journals); } /** @@ -362,9 +351,9 @@ class OperationsRepository implements OperationsRepositoryInterface public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array { /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end) - ->setTypes([TransactionType::DEPOSIT]) + ->setTypes([TransactionTypeEnum::DEPOSIT->value]) ; if (null !== $accounts && $accounts->count() > 0) { @@ -374,20 +363,52 @@ class OperationsRepository implements OperationsRepositoryInterface $categories = $this->getCategories(); } $collector->setCategories($categories); - $journals = $collector->getExtractedJournals(); - $array = []; + $journals = $collector->getExtractedJournals(); + $convertToNative = Amount::convertToNative($this->user); + $default = Amount::getDefaultCurrency(); + $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + // Almost the same as in \FireflyIII\Repositories\Budget\OperationsRepository::sumExpenses + $amount = '0'; + $currencyId = (int) $journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + if ($convertToNative) { + $amount = Amount::getAmountFromJournal($journal); + if ($default->id !== (int) $journal['currency_id'] && $default->id !== (int) $journal['foreign_currency_id']) { + $currencyId = $default->id; + $currencyName = $default->name; + $currencySymbol = $default->symbol; + $currencyCode = $default->code; + $currencyDecimalPlaces = $default->decimal_places; + } + if ($default->id !== (int) $journal['currency_id'] && $default->id === (int) $journal['foreign_currency_id']) { + $currencyId = $journal['foreign_currency_id']; + $currencyName = $journal['foreign_currency_name']; + $currencySymbol = $journal['foreign_currency_symbol']; + $currencyCode = $journal['foreign_currency_code']; + $currencyDecimalPlaces = $journal['foreign_currency_decimal_places']; + } + Log::debug(sprintf('[a] Add amount %s %s', $currencyCode, $amount)); + } + if (!$convertToNative) { + // ignore the amount in foreign currency. + Log::debug(sprintf('[b] Add amount %s %s', $currencyCode, $journal['amount'])); + $amount = $journal['amount']; + } + $array[$currencyId] ??= [ 'sum' => '0', - 'currency_id' => (string)$currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_decimal_places' => $journal['currency_decimal_places'], + 'currency_id' => (string) $currencyId, + 'currency_name' => $currencyName, + 'currency_symbol' => $currencySymbol, + 'currency_code' => $currencyCode, + 'currency_decimal_places' => $currencyDecimalPlaces, ]; - $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->positive($journal['amount'])); + $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->positive($amount)); } return $array; @@ -415,10 +436,10 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ 'sum' => '0', - 'currency_id' => (string)$currencyId, + 'currency_id' => (string) $currencyId, 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index 5ca4924769..438354f4a5 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -37,6 +37,12 @@ class CurrencyRepository implements CurrencyRepositoryInterface { private User $user; + #[\Override] + public function find(int $currencyId): ?TransactionCurrency + { + return TransactionCurrency::find($currencyId); + } + /** * Find by currency code, return NULL if unfound. */ @@ -89,12 +95,13 @@ class CurrencyRepository implements CurrencyRepositoryInterface { return CurrencyExchangeRate::create( [ - 'user_id' => $this->user->id, - 'from_currency_id' => $fromCurrency->id, - 'to_currency_id' => $toCurrency->id, - 'date' => $date, - 'date_tz' => $date->format('e'), - 'rate' => $rate, + 'user_id' => $this->user->id, + 'user_group_id' => $this->user->user_group_id, + 'from_currency_id' => $fromCurrency->id, + 'to_currency_id' => $toCurrency->id, + 'date' => $date, + 'date_tz' => $date->format('e'), + 'rate' => $rate, ] ); } diff --git a/app/Repositories/Currency/CurrencyRepositoryInterface.php b/app/Repositories/Currency/CurrencyRepositoryInterface.php index e714977abb..09258197fd 100644 --- a/app/Repositories/Currency/CurrencyRepositoryInterface.php +++ b/app/Repositories/Currency/CurrencyRepositoryInterface.php @@ -35,6 +35,8 @@ use Illuminate\Support\Collection; */ interface CurrencyRepositoryInterface { + public function find(int $currencyId): ?TransactionCurrency; + /** * Find by currency code, return NULL if unfound. * diff --git a/app/Repositories/Journal/JournalCLIRepository.php b/app/Repositories/Journal/JournalCLIRepository.php index 9960cb8ac5..53543eb70b 100644 --- a/app/Repositories/Journal/JournalCLIRepository.php +++ b/app/Repositories/Journal/JournalCLIRepository.php @@ -143,7 +143,7 @@ class JournalCLIRepository implements JournalCLIRepositoryInterface } // return when something else: - $return = (string)$value; + $return = (string) $value; $cache->store($return); return $return; @@ -176,8 +176,8 @@ class JournalCLIRepository implements JournalCLIRepositoryInterface /** @var \stdClass $row */ foreach ($result as $row) { - if ((int)$row->transaction_count > 2) { - $journalIds[] = (int)$row->id; + if ((int) $row->transaction_count > 2) { + $journalIds[] = (int) $row->id; } } $journalIds = array_unique($journalIds); diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index e8d19790cb..cac96ff771 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -111,7 +111,7 @@ class JournalRepository implements JournalRepositoryInterface // saves on queries: $amount = $journal->transactions()->where('amount', '>', 0)->get()->sum('amount'); - $amount = (string)$amount; + $amount = (string) $amount; $cache->store($amount); return $amount; @@ -134,7 +134,7 @@ class JournalRepository implements JournalRepositoryInterface /** @var null|Note $note */ $note = $link->notes()->first(); - return (string)$note?->text; + return (string) $note?->text; } /** diff --git a/app/Repositories/LinkType/LinkTypeRepository.php b/app/Repositories/LinkType/LinkTypeRepository.php index 7f49ab5211..f42ece55ca 100644 --- a/app/Repositories/LinkType/LinkTypeRepository.php +++ b/app/Repositories/LinkType/LinkTypeRepository.php @@ -56,13 +56,13 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface public function update(LinkType $linkType, array $data): LinkType { - if (array_key_exists('name', $data) && '' !== (string)$data['name']) { + if (array_key_exists('name', $data) && '' !== (string) $data['name']) { $linkType->name = $data['name']; } - if (array_key_exists('inward', $data) && '' !== (string)$data['inward']) { + if (array_key_exists('inward', $data) && '' !== (string) $data['inward']) { $linkType->inward = $data['inward']; } - if (array_key_exists('outward', $data) && '' !== (string)$data['outward']) { + if (array_key_exists('outward', $data) && '' !== (string) $data['outward']) { $linkType->outward = $data['outward']; } $linkType->save(); @@ -183,7 +183,7 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface */ public function storeLink(array $information, TransactionJournal $inward, TransactionJournal $outward): ?TransactionJournalLink { - $linkType = $this->find((int)($information['link_type_id'] ?? 0)); + $linkType = $this->find((int) ($information['link_type_id'] ?? 0)); if (null === $linkType) { $linkType = $this->findByName($information['link_type_name']); @@ -215,7 +215,7 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface $link->save(); // make note in noteable: - $this->setNoteText($link, (string)$information['notes']); + $this->setNoteText($link, (string) $information['notes']); return $link; } diff --git a/app/Repositories/ObjectGroup/CreatesObjectGroups.php b/app/Repositories/ObjectGroup/CreatesObjectGroups.php index dc843cc2c2..767416456a 100644 --- a/app/Repositories/ObjectGroup/CreatesObjectGroups.php +++ b/app/Repositories/ObjectGroup/CreatesObjectGroups.php @@ -56,7 +56,7 @@ trait CreatesObjectGroups protected function getObjectGroupMaxOrder(): int { - return (int)$this->user->objectGroups()->max('order'); + return (int) $this->user->objectGroups()->max('order'); } protected function hasObjectGroup(string $title): bool diff --git a/app/Repositories/ObjectGroup/ObjectGroupRepository.php b/app/Repositories/ObjectGroup/ObjectGroupRepository.php index 7bb97a861d..e9d880d56a 100644 --- a/app/Repositories/ObjectGroup/ObjectGroupRepository.php +++ b/app/Repositories/ObjectGroup/ObjectGroupRepository.php @@ -141,7 +141,7 @@ class ObjectGroupRepository implements ObjectGroupRepositoryInterface } if (array_key_exists('order', $data)) { - $this->setOrder($objectGroup, (int)$data['order']); + $this->setOrder($objectGroup, (int) $data['order']); } $objectGroup->save(); diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index df3e98c10e..1fea74a370 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -26,12 +26,15 @@ namespace FireflyIII\Repositories\PiggyBank; use FireflyIII\Events\Model\PiggyBank\ChangedAmount; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\PiggyBankFactory; +use FireflyIII\Models\Account; use FireflyIII\Models\Note; use FireflyIII\Models\PiggyBank; -use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; -use Illuminate\Database\QueryException; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; +use Illuminate\Support\Facades\Log; /** * Trait ModifiesPiggyBanks @@ -40,81 +43,99 @@ trait ModifiesPiggyBanks { use CreatesObjectGroups; - public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void + public function addAmountToPiggyBank(PiggyBank $piggyBank, string $amount, TransactionJournal $journal): void { - app('log')->debug(sprintf('addAmountToRepetition: %s', $amount)); + Log::debug(sprintf('addAmountToPiggyBank: %s', $amount)); if (-1 === bccomp($amount, '0')) { - app('log')->debug('Remove amount.'); - $this->removeAmount($repetition->piggyBank, bcmul($amount, '-1'), $journal); + /** @var Transaction $source */ + $source = $journal->transactions()->with(['account'])->where('amount', '<', 0)->first(); + Log::debug('Remove amount.'); + $this->removeAmount($piggyBank, $source->account, bcmul($amount, '-1'), $journal); } if (1 === bccomp($amount, '0')) { - app('log')->debug('Add amount.'); - $this->addAmount($repetition->piggyBank, $amount, $journal); + /** @var Transaction $destination */ + $destination = $journal->transactions()->with(['account'])->where('amount', '>', 0)->first(); + Log::debug('Add amount.'); + $this->addAmount($piggyBank, $destination->account, $amount, $journal); } } - public function removeAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool + public function removeAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool { - $repetition = $this->getRepetition($piggyBank); - if (null === $repetition) { - return false; - } - $repetition->currentamount = bcsub($repetition->currentamount, $amount); - $repetition->save(); + $currentAmount = $this->getCurrentAmount($piggyBank, $account); + $pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot; + $pivot->current_amount = bcsub($currentAmount, $amount); + $pivot->native_current_amount = null; - app('log')->debug('addAmount [a]: Trigger change for negative amount.'); + // also update native_current_amount. + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); + if ($userCurrency->id !== $piggyBank->transaction_currency_id) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $pivot->native_current_amount = $converter->convert($piggyBank->transactionCurrency, $userCurrency, today(), $pivot->current_amount); + } + + $pivot->save(); + + Log::debug('ChangedAmount: removeAmount [a]: Trigger change for negative amount.'); event(new ChangedAmount($piggyBank, bcmul($amount, '-1'), $journal, null)); return true; } - public function addAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool + public function addAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool { - $repetition = $this->getRepetition($piggyBank); - if (null === $repetition) { - return false; - } - $currentAmount = $repetition->currentamount ?? '0'; - $repetition->currentamount = bcadd($currentAmount, $amount); - $repetition->save(); + $currentAmount = $this->getCurrentAmount($piggyBank, $account); + $pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot; + $pivot->current_amount = bcadd($currentAmount, $amount); + $pivot->native_current_amount = null; - app('log')->debug('addAmount [b]: Trigger change for positive amount.'); + // also update native_current_amount. + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); + if ($userCurrency->id !== $piggyBank->transaction_currency_id) { + $converter = new ExchangeRateConverter(); + $converter->setIgnoreSettings(true); + $pivot->native_current_amount = $converter->convert($piggyBank->transactionCurrency, $userCurrency, today(), $pivot->current_amount); + } + + $pivot->save(); + + Log::debug('ChangedAmount: addAmount [b]: Trigger change for positive amount.'); event(new ChangedAmount($piggyBank, $amount, $journal, null)); return true; } - public function canAddAmount(PiggyBank $piggyBank, string $amount): bool + public function canAddAmount(PiggyBank $piggyBank, Account $account, string $amount): bool { - $today = today(config('app.timezone')); - $leftOnAccount = $this->leftOnAccount($piggyBank, $today); - $savedSoFar = $this->getRepetition($piggyBank)->currentamount; + Log::debug('Now in canAddAmount'); + $today = today(config('app.timezone'))->endOfDay(); + $leftOnAccount = $this->leftOnAccount($piggyBank, $account, $today); + $savedSoFar = $this->getCurrentAmount($piggyBank); $maxAmount = $leftOnAccount; - $leftToSave = null; - if (0 !== bccomp($piggyBank->targetamount, '0')) { - $leftToSave = bcsub($piggyBank->targetamount, $savedSoFar); + + Log::debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d H:i:s'))); + Log::debug(sprintf('Saved so far: %s', $savedSoFar)); + + + if (0 !== bccomp($piggyBank->target_amount, '0')) { + $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); $maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount; + Log::debug(sprintf('Left to save: %s', $leftToSave)); + Log::debug(sprintf('Maximum amount: %s', $maxAmount)); } $compare = bccomp($amount, $maxAmount); $result = $compare <= 0; - app('log')->debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d'))); - app('log')->debug(sprintf('Saved so far: %s', $savedSoFar)); - app('log')->debug(sprintf('Left to save: %s', $leftToSave)); - app('log')->debug(sprintf('Maximum amount: %s', $maxAmount)); - app('log')->debug(sprintf('Compare <= 0? %d, so %s', $compare, var_export($result, true))); + Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true))); return $result; } - public function canRemoveAmount(PiggyBank $piggyBank, string $amount): bool + public function canRemoveAmount(PiggyBank $piggyBank, Account $account, string $amount): bool { - $repetition = $this->getRepetition($piggyBank); - if (null === $repetition) { - return false; - } - $savedSoFar = $repetition->currentamount; + $savedSoFar = $this->getCurrentAmount($piggyBank, $account); return bccomp($amount, $savedSoFar) <= 0; } @@ -139,24 +160,24 @@ trait ModifiesPiggyBanks public function setCurrentAmount(PiggyBank $piggyBank, string $amount): PiggyBank { - $repetition = $this->getRepetition($piggyBank); + $repetition = $this->getRepetition($piggyBank); if (null === $repetition) { return $piggyBank; } - $max = $piggyBank->targetamount; - if (1 === bccomp($amount, $max) && 0 !== bccomp($piggyBank->targetamount, '0')) { + $max = $piggyBank->target_amount; + if (1 === bccomp($amount, $max) && 0 !== bccomp($piggyBank->target_amount, '0')) { $amount = $max; } - $difference = bcsub($amount, $repetition->currentamount); - $repetition->currentamount = $amount; + $difference = bcsub($amount, $repetition->current_amount); + $repetition->current_amount = $amount; $repetition->save(); if (-1 === bccomp($difference, '0')) { - app('log')->debug('addAmount [c]: Trigger change for negative amount.'); + Log::debug('ChangedAmount: addAmount [c]: Trigger change for negative amount.'); event(new ChangedAmount($piggyBank, $difference, null, null)); } if (1 === bccomp($difference, '0')) { - app('log')->debug('addAmount [d]: Trigger change for positive amount.'); + Log::debug('ChangedAmount: addAmount [d]: Trigger change for positive amount.'); event(new ChangedAmount($piggyBank, $difference, null, null)); } @@ -178,159 +199,51 @@ trait ModifiesPiggyBanks */ public function store(array $data): PiggyBank { - $order = $this->getMaxOrder() + 1; - if (array_key_exists('order', $data)) { - $order = $data['order']; - } - $data['order'] = 31337; // very high when creating. - $piggyData = $data; - // unset fields just in case. - unset($piggyData['object_group_title'], $piggyData['object_group_id'], $piggyData['notes'], $piggyData['current_amount']); + $factory = new PiggyBankFactory(); + $factory->user = $this->user; - // validate amount: - if (array_key_exists('targetamount', $piggyData) && '' === (string)$piggyData['targetamount']) { - $piggyData['targetamount'] = '0'; - } - - $piggyData['startdate_tz'] = $piggyData['startdate']?->format('e'); - $piggyData['targetdate_tz'] = $piggyData['targetdate']?->format('e'); - - try { - /** @var PiggyBank $piggyBank */ - $piggyBank = PiggyBank::create($piggyData); - } catch (QueryException $e) { - app('log')->error(sprintf('Could not store piggy bank: %s', $e->getMessage()), $piggyData); - - throw new FireflyException('400005: Could not store new piggy bank.', 0, $e); - } - - // reset order then set order: - $this->resetOrder(); - $this->setOrder($piggyBank, $order); - - $this->updateNote($piggyBank, $data['notes']); - - // repetition is auto created. - $repetition = $this->getRepetition($piggyBank); - if (null !== $repetition && array_key_exists('current_amount', $data) && '' !== $data['current_amount']) { - $repetition->currentamount = $data['current_amount']; - $repetition->save(); - } - - $objectGroupTitle = $data['object_group_title'] ?? ''; - if ('' !== $objectGroupTitle) { - $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); - if (null !== $objectGroup) { - $piggyBank->objectGroups()->sync([$objectGroup->id]); - $piggyBank->save(); - } - } - // try also with ID - $objectGroupId = (int)($data['object_group_id'] ?? 0); - if (0 !== $objectGroupId) { - $objectGroup = $this->findObjectGroupById($objectGroupId); - if (null !== $objectGroup) { - $piggyBank->objectGroups()->sync([$objectGroup->id]); - $piggyBank->save(); - } - } - - return $piggyBank; - } - - /** - * Correct order of piggies in case of issues. - */ - public function resetOrder(): void - { - $set = $this->user->piggyBanks()->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']); - $current = 1; - foreach ($set as $piggyBank) { - if ($piggyBank->order !== $current) { - app('log')->debug(sprintf('Piggy bank #%d ("%s") was at place %d but should be on %d', $piggyBank->id, $piggyBank->name, $piggyBank->order, $current)); - $piggyBank->order = $current; - $piggyBank->save(); - } - ++$current; - } - } - - public function setOrder(PiggyBank $piggyBank, int $newOrder): bool - { - $oldOrder = $piggyBank->order; - // app('log')->debug(sprintf('Will move piggy bank #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); - if ($newOrder > $oldOrder) { - $this->user->piggyBanks()->where('piggy_banks.order', '<=', $newOrder)->where('piggy_banks.order', '>', $oldOrder) - ->where('piggy_banks.id', '!=', $piggyBank->id) - ->decrement('piggy_banks.order') - ; - $piggyBank->order = $newOrder; - app('log')->debug(sprintf('[1] Order of piggy #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); - $piggyBank->save(); - - return true; - } - - $this->user->piggyBanks()->where('piggy_banks.order', '>=', $newOrder)->where('piggy_banks.order', '<', $oldOrder) - ->where('piggy_banks.id', '!=', $piggyBank->id) - ->increment('piggy_banks.order') - ; - $piggyBank->order = $newOrder; - app('log')->debug(sprintf('[2] Order of piggy #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); - $piggyBank->save(); - - return true; - } - - private function updateNote(PiggyBank $piggyBank, string $note): bool - { - if ('' === $note) { - $dbNote = $piggyBank->notes()->first(); - $dbNote?->delete(); - - return true; - } - $dbNote = $piggyBank->notes()->first(); - if (null === $dbNote) { - $dbNote = new Note(); - $dbNote->noteable()->associate($piggyBank); - } - $dbNote->text = trim($note); - $dbNote->save(); - - return true; + return $factory->store($data); } public function update(PiggyBank $piggyBank, array $data): PiggyBank { - $piggyBank = $this->updateProperties($piggyBank, $data); + $piggyBank = $this->updateProperties($piggyBank, $data); if (array_key_exists('notes', $data)) { - $this->updateNote($piggyBank, (string)$data['notes']); + $this->updateNote($piggyBank, (string) $data['notes']); } // update the order of the piggy bank: - $oldOrder = $piggyBank->order; - $newOrder = (int)($data['order'] ?? $oldOrder); + $oldOrder = $piggyBank->order; + $newOrder = (int) ($data['order'] ?? $oldOrder); if ($oldOrder !== $newOrder) { $this->setOrder($piggyBank, $newOrder); } - // if the piggy bank is now smaller than the current relevant rep, - // remove money from the rep. - $repetition = $this->getRepetition($piggyBank); - if (null !== $repetition && $repetition->currentamount > $piggyBank->targetamount && 0 !== bccomp($piggyBank->targetamount, '0')) { - $difference = bcsub($piggyBank->targetamount, $repetition->currentamount); + // update the accounts + $factory = new PiggyBankFactory(); + $factory->user = $this->user; + $factory->linkToAccountIds($piggyBank, $data['accounts']); + + + // if the piggy bank is now smaller than the sum of the money saved, + // remove money from all accounts until the piggy bank is the right amount. + $currentAmount = $this->getCurrentAmount($piggyBank); + if (1 === bccomp($currentAmount, $piggyBank->target_amount) && 0 !== bccomp($piggyBank->target_amount, '0')) { + Log::debug(sprintf('Current amount is %s, target amount is %s', $currentAmount, $piggyBank->target_amount)); + $difference = bcsub($piggyBank->target_amount, $currentAmount); // an amount will be removed, create "negative" event: + Log::debug(sprintf('ChangedAmount: is triggered with difference "%s"', $difference)); event(new ChangedAmount($piggyBank, $difference, null, null)); - $repetition->currentamount = $piggyBank->targetamount; - $repetition->save(); + // question is, from which account(s) to remove the difference? + // solution: just start from the top until there is no more money left to remove. + $this->removeAmountFromAll($piggyBank, app('steam')->positive($difference)); } // update using name: if (array_key_exists('object_group_title', $data)) { - $objectGroupTitle = (string)$data['object_group_title']; + $objectGroupTitle = (string) $data['object_group_title']; if ('' !== $objectGroupTitle) { $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); if (null !== $objectGroup) { @@ -346,7 +259,7 @@ trait ModifiesPiggyBanks // try also with ID: if (array_key_exists('object_group_id', $data)) { - $objectGroupId = (int)($data['object_group_id'] ?? 0); + $objectGroupId = (int) ($data['object_group_id'] ?? 0); if (0 !== $objectGroupId) { $objectGroup = $this->findObjectGroupById($objectGroupId); if (null !== $objectGroup) { @@ -366,25 +279,91 @@ trait ModifiesPiggyBanks if (array_key_exists('name', $data) && '' !== $data['name']) { $piggyBank->name = $data['name']; } - if (array_key_exists('account_id', $data) && 0 !== $data['account_id']) { - $piggyBank->account_id = (int)$data['account_id']; + if (array_key_exists('target_amount', $data) && '' !== $data['target_amount']) { + $piggyBank->target_amount = $data['target_amount']; } - if (array_key_exists('targetamount', $data) && '' !== $data['targetamount']) { - $piggyBank->targetamount = $data['targetamount']; + if (array_key_exists('target_amount', $data) && '' === $data['target_amount']) { + $piggyBank->target_amount = '0'; } - if (array_key_exists('targetamount', $data) && '' === $data['targetamount']) { - $piggyBank->targetamount = '0'; + if (array_key_exists('target_date', $data) && '' !== $data['target_date']) { + $piggyBank->target_date = $data['target_date']; + $piggyBank->target_date_tz = $data['target_date']?->format('e'); } - if (array_key_exists('targetdate', $data) && '' !== $data['targetdate']) { - $piggyBank->targetdate = $data['targetdate']; - $piggyBank->targetdate_tz = $data['targetdate']?->format('e'); - } - if (array_key_exists('startdate', $data)) { - $piggyBank->startdate = $data['startdate']; - $piggyBank->startdate_tz = $data['targetdate']?->format('e'); + if (array_key_exists('start_date', $data)) { + $piggyBank->start_date = $data['start_date']; + $piggyBank->start_date_tz = $data['target_date']?->format('e'); } $piggyBank->save(); return $piggyBank; } + + public function updateNote(PiggyBank $piggyBank, string $note): void + { + if ('' === $note) { + $dbNote = $piggyBank->notes()->first(); + $dbNote?->delete(); + + return; + } + $dbNote = $piggyBank->notes()->first(); + if (null === $dbNote) { + $dbNote = new Note(); + $dbNote->noteable()->associate($piggyBank); + } + $dbNote->text = trim($note); + $dbNote->save(); + } + + public function setOrder(PiggyBank $piggyBank, int $newOrder): bool + { + $oldOrder = $piggyBank->order; + // Log::debug(sprintf('Will move piggy bank #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); + if ($newOrder > $oldOrder) { + PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.order', '<=', $newOrder)->where('piggy_banks.order', '>', $oldOrder) + ->where('piggy_banks.id', '!=', $piggyBank->id) + ->distinct()->decrement('piggy_banks.order') + ; + + $piggyBank->order = $newOrder; + Log::debug(sprintf('[1] Order of piggy #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); + $piggyBank->save(); + + return true; + } + PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.order', '>=', $newOrder)->where('piggy_banks.order', '<', $oldOrder) + ->where('piggy_banks.id', '!=', $piggyBank->id) + ->distinct()->increment('piggy_banks.order') + ; + + $piggyBank->order = $newOrder; + Log::debug(sprintf('[2] Order of piggy #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); + $piggyBank->save(); + + return true; + } + + public function removeAmountFromAll(PiggyBank $piggyBank, string $amount): void + { + foreach ($piggyBank->accounts as $account) { + $current = $account->pivot->current_amount; + // if this account contains more than the amount, remove the amount and return. + if (1 === bccomp($current, $amount)) { + $this->removeAmount($piggyBank, $account, $amount); + + return; + } + // if this account contains less than the amount, remove the current amount, update the amount and continue. + if (bccomp($current, $amount) < 1) { + $this->removeAmount($piggyBank, $account, $current); + $amount = bcsub($amount, $current); + } + } + } } diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 99d19353a9..b7f500ca1e 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -25,6 +25,8 @@ namespace FireflyIII\Repositories\PiggyBank; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\PiggyBankFactory; +use FireflyIII\Models\Account; use FireflyIII\Models\Attachment; use FireflyIII\Models\Note; use FireflyIII\Models\PiggyBank; @@ -33,6 +35,7 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; @@ -80,8 +83,11 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function find(int $piggyBankId): ?PiggyBank { - // phpstan doesn't get the Model. - return $this->user->piggyBanks()->find($piggyBankId); // @phpstan-ignore-line + return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']) + ; } /** @@ -89,7 +95,11 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function findByName(string $name): ?PiggyBank { - return $this->user->piggyBanks()->where('piggy_banks.name', $name)->first(['piggy_banks.*']); + return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.name', $name)->first(['piggy_banks.*']) + ; } public function getAttachments(PiggyBank $piggyBank): Collection @@ -110,24 +120,6 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface ); } - /** - * Get current amount saved in piggy bank. - */ - public function getCurrentAmount(PiggyBank $piggyBank): string - { - $rep = $this->getRepetition($piggyBank); - if (null === $rep) { - return '0'; - } - - return $rep->currentamount; - } - - public function getRepetition(PiggyBank $piggyBank): ?PiggyBankRepetition - { - return $piggyBank->piggyBankRepetitions()->first(); - } - public function getEvents(PiggyBank $piggyBank): Collection { return $piggyBank->piggyBankEvents()->orderBy('date', 'DESC')->orderBy('id', 'DESC')->get(); @@ -138,31 +130,30 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface * * @throws FireflyException */ - public function getExactAmount(PiggyBank $piggyBank, PiggyBankRepetition $repetition, TransactionJournal $journal): string + public function getExactAmount(PiggyBank $piggyBank, TransactionJournal $journal): string { - app('log')->debug(sprintf('Now in getExactAmount(%d, %d, %d)', $piggyBank->id, $repetition->id, $journal->id)); + app('log')->debug(sprintf('Now in getExactAmount(%d, %d)', $piggyBank->id, $journal->id)); - $operator = null; - $currency = null; + $operator = null; + $currency = null; /** @var JournalRepositoryInterface $journalRepost */ - $journalRepost = app(JournalRepositoryInterface::class); + $journalRepost = app(JournalRepositoryInterface::class); $journalRepost->setUser($this->user); /** @var AccountRepositoryInterface $accountRepos */ - $accountRepos = app(AccountRepositoryInterface::class); + $accountRepos = app(AccountRepositoryInterface::class); $accountRepos->setUser($this->user); - $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); - $piggyBankCurrency = $accountRepos->getAccountCurrency($piggyBank->account) ?? $defaultCurrency; + $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); - app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBankCurrency->code)); + app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBank->transactionCurrency->code)); /** @var Transaction $source */ - $source = $journal->transactions()->with(['account'])->where('amount', '<', 0)->first(); + $source = $journal->transactions()->with(['account'])->where('amount', '<', 0)->first(); /** @var Transaction $destination */ - $destination = $journal->transactions()->with(['account'])->where('amount', '>', 0)->first(); + $destination = $journal->transactions()->with(['account'])->where('amount', '>', 0)->first(); // matches source, which means amount will be removed from piggy: if ($source->account_id === $piggyBank->account_id) { @@ -184,12 +175,12 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } // currency of the account + the piggy bank currency are almost the same. // which amount from the transaction matches? - $amount = null; - if ((int)$source->transaction_currency_id === $currency->id) { + $amount = null; + if ((int) $source->transaction_currency_id === $currency->id) { app('log')->debug('Use normal amount'); $amount = app('steam')->{$operator}($source->amount); // @phpstan-ignore-line } - if ((int)$source->foreign_currency_id === $currency->id) { + if ((int) $source->foreign_currency_id === $currency->id) { app('log')->debug('Use foreign amount'); $amount = app('steam')->{$operator}($source->foreign_amount); // @phpstan-ignore-line } @@ -200,10 +191,11 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } app('log')->debug(sprintf('The currency is %s and the amount is %s', $currency->code, $amount)); - $room = bcsub($piggyBank->targetamount, $repetition->currentamount); - $compare = bcmul($repetition->currentamount, '-1'); + $currentAmount = $this->getCurrentAmount($piggyBank); + $room = bcsub($piggyBank->target_amount, $currentAmount); + $compare = bcmul($currentAmount, '-1'); - if (0 === bccomp($piggyBank->targetamount, '0')) { + if (0 === bccomp($piggyBank->target_amount, '0')) { // amount is zero? then the "room" is positive amount of we wish to add or remove. $room = app('steam')->positive($amount); app('log')->debug(sprintf('Room is now %s', $room)); @@ -221,16 +213,16 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return $room; } - // amount is negative and $currentamount is smaller than $amount + // amount is negative and $currentAmount is smaller than $amount if (-1 === bccomp($amount, '0') && 1 === bccomp($compare, $amount)) { - app('log')->debug(sprintf('Max amount to remove is %f', $repetition->currentamount)); + app('log')->debug(sprintf('Max amount to remove is %f', $currentAmount)); app('log')->debug(sprintf('Cannot remove %f from piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name)); app('log')->debug(sprintf('New amount is %f', $compare)); return $compare; } - return (string)$amount; + return (string) $amount; } public function setUser(null|Authenticatable|User $user): void @@ -240,11 +232,6 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } } - public function getMaxOrder(): int - { - return (int)$this->user->piggyBanks()->max('piggy_banks.order'); - } - /** * Return note for piggy bank. */ @@ -253,7 +240,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface /** @var null|Note $note */ $note = $piggyBank->notes()->first(); - return (string)$note?->text; + return (string) $note?->text; } /** @@ -261,14 +248,12 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function getPiggyBanksWithAmount(): Collection { - $currency = app('amount')->getDefaultCurrency(); - - $set = $this->getPiggyBanks(); + $set = $this->getPiggyBanks(); /** @var PiggyBank $piggy */ foreach ($set as $piggy) { - $currentAmount = $this->getRepetition($piggy)->currentamount ?? '0'; - $piggy->name = $piggy->name.' ('.app('amount')->formatAnything($currency, $currentAmount, false).')'; + $currentAmount = $this->getCurrentAmount($piggy); + $piggy->name = sprintf('%s (%s)', $piggy->name, app('amount')->formatAnything($piggy->transactionCurrency, $currentAmount, false)); } return $set; @@ -276,37 +261,63 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function getPiggyBanks(): Collection { - return $this->user // @phpstan-ignore-line (phpstan does not recognize objectGroups) - ->piggyBanks() + return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) ->with( [ - 'account', 'objectGroups', ] ) - ->orderBy('order', 'ASC')->get() + ->orderBy('piggy_banks.order', 'ASC')->distinct()->get(['piggy_banks.*']) ; } + /** + * Get current amount saved in piggy bank. + */ + public function getCurrentAmount(PiggyBank $piggyBank, ?Account $account = null): string + { + $sum = '0'; + foreach ($piggyBank->accounts as $current) { + if (null !== $account && $account->id !== $current->id) { + continue; + } + $amount = (string) $current->pivot->current_amount; + $amount = '' === $amount ? '0' : $amount; + $sum = bcadd($sum, $amount); + } + Log::debug(sprintf('Current amount in piggy bank #%d ("%s") is %s', $piggyBank->id, $piggyBank->name, $sum)); + + return $sum; + } + + public function getRepetition(PiggyBank $piggyBank, bool $overrule = false): ?PiggyBankRepetition + { + if (false === $overrule) { + throw new FireflyException('[b] Piggy bank repetitions are EOL.'); + } + Log::warning('Piggy bank repetitions are EOL.'); + + return $piggyBank->piggyBankRepetitions()->first(); + } + /** * Returns the suggested amount the user should save per month, or "". */ public function getSuggestedMonthlyAmount(PiggyBank $piggyBank): string { - $savePerMonth = '0'; - $repetition = $this->getRepetition($piggyBank); - if (null === $repetition) { - return $savePerMonth; - } - if (null !== $piggyBank->targetdate && $repetition->currentamount < $piggyBank->targetamount) { + $savePerMonth = '0'; + $currentAmount = $this->getCurrentAmount($piggyBank); + if (null !== $piggyBank->target_date && $currentAmount < $piggyBank->target_amount) { $now = today(config('app.timezone')); - $startDate = null !== $piggyBank->startdate && $piggyBank->startdate->gte($now) ? $piggyBank->startdate : $now; - $diffInMonths = (int)$startDate->diffInMonths($piggyBank->targetdate); - $remainingAmount = bcsub($piggyBank->targetamount, $repetition->currentamount); + $startDate = null !== $piggyBank->start_date && $piggyBank->start_date->gte($now) ? $piggyBank->start_date : $now; + $diffInMonths = (int) $startDate->diffInMonths($piggyBank->target_date); + $remainingAmount = bcsub($piggyBank->target_amount, $currentAmount); // more than 1 month to go and still need money to save: if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) { - $savePerMonth = bcdiv($remainingAmount, (string)$diffInMonths); + $savePerMonth = bcdiv($remainingAmount, (string) $diffInMonths); } // less than 1 month to go but still need money to save: @@ -321,27 +332,64 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface /** * Get for piggy account what is left to put in piggies. */ - public function leftOnAccount(PiggyBank $piggyBank, Carbon $date): string + public function leftOnAccount(PiggyBank $piggyBank, Account $account, Carbon $date): string { - $balance = app('steam')->balanceIgnoreVirtual($piggyBank->account, $date); + Log::debug(sprintf('leftOnAccount("%s","%s","%s")', $piggyBank->name, $account->name, $date->format('Y-m-d H:i:s'))); + $balance = Steam::finalAccountBalance($account, $date)['balance']; + + Log::debug(sprintf('Balance is: %s', $balance)); /** @var Collection $piggies */ - $piggies = $piggyBank->account->piggyBanks; + $piggies = $account->piggyBanks; /** @var PiggyBank $current */ foreach ($piggies as $current) { - $repetition = $this->getRepetition($current); - if (null !== $repetition) { - $balance = bcsub($balance, $repetition->currentamount); - } + $amount = $this->getCurrentAmount($current, $account); + $balance = bcsub($balance, $amount); + Log::debug(sprintf('Piggy bank: #%d with amount %s, balance is now %s', $current->id, $amount, $balance)); } + Log::debug(sprintf('Final balance is: %s', $balance)); return $balance; } + #[\Override] + public function purgeAll(): void + { + PiggyBank::withTrashed() + ->whereNotNull('piggy_banks.deleted_at') + ->leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->with( + [ + 'objectGroups', + ] + ) + ->delete() + ; + } + + #[\Override] + public function resetOrder(): void + { + $factory = new PiggyBankFactory(); + $factory->user = $this->user; + $factory->resetOrder(); + } + public function searchPiggyBank(string $query, int $limit): Collection { - $search = $this->user->piggyBanks(); + $search = PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->with( + [ + 'objectGroups', + ] + ) + ->orderBy('piggy_banks.order', 'ASC')->distinct() + ; if ('' !== $query) { $search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query)); } @@ -349,6 +397,6 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface ->orderBy('piggy_banks.name', 'ASC') ; - return $search->take($limit)->get(); + return $search->take($limit)->get(['piggy_banks.*']); } } diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index 7c1e12becc..26486227ed 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\PiggyBank; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\TransactionJournal; @@ -37,13 +38,13 @@ use Illuminate\Support\Collection; */ interface PiggyBankRepositoryInterface { - public function addAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool; + public function addAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool; - public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void; + public function addAmountToPiggyBank(PiggyBank $piggyBank, string $amount, TransactionJournal $journal): void; - public function canAddAmount(PiggyBank $piggyBank, string $amount): bool; + public function canAddAmount(PiggyBank $piggyBank, Account $account, string $amount): bool; - public function canRemoveAmount(PiggyBank $piggyBank, string $amount): bool; + public function canRemoveAmount(PiggyBank $piggyBank, Account $account, string $amount): bool; /** * Destroy piggy bank. @@ -66,22 +67,20 @@ interface PiggyBankRepositoryInterface /** * Get current amount saved in piggy bank. */ - public function getCurrentAmount(PiggyBank $piggyBank): string; + public function getCurrentAmount(PiggyBank $piggyBank, ?Account $account = null): string; /** * Get all events. */ public function getEvents(PiggyBank $piggyBank): Collection; + /** + * Get current amount saved in piggy bank. + */ /** * Used for connecting to a piggy bank. */ - public function getExactAmount(PiggyBank $piggyBank, PiggyBankRepetition $repetition, TransactionJournal $journal): string; - - /** - * Highest order of all piggy banks. - */ - public function getMaxOrder(): int; + public function getExactAmount(PiggyBank $piggyBank, TransactionJournal $journal): string; /** * Return note for piggy bank. @@ -98,7 +97,7 @@ interface PiggyBankRepositoryInterface */ public function getPiggyBanksWithAmount(): Collection; - public function getRepetition(PiggyBank $piggyBank): ?PiggyBankRepetition; + public function getRepetition(PiggyBank $piggyBank, bool $overrule = false): ?PiggyBankRepetition; /** * Returns the suggested amount the user should save per month, or "". @@ -108,15 +107,16 @@ interface PiggyBankRepositoryInterface /** * Get for piggy account what is left to put in piggies. */ - public function leftOnAccount(PiggyBank $piggyBank, Carbon $date): string; + public function leftOnAccount(PiggyBank $piggyBank, Account $account, Carbon $date): string; - public function removeAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool; + public function purgeAll(): void; + + public function removeAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool; + + public function removeAmountFromAll(PiggyBank $piggyBank, string $amount): void; public function removeObjectGroup(PiggyBank $piggyBank): PiggyBank; - /** - * Correct order of piggies in case of issues. - */ public function resetOrder(): void; /** @@ -146,4 +146,6 @@ interface PiggyBankRepositoryInterface * Update existing piggy bank. */ public function update(PiggyBank $piggyBank, array $data): PiggyBank; + + public function updateNote(PiggyBank $piggyBank, string $note): void; } diff --git a/app/Repositories/Rule/RuleRepository.php b/app/Repositories/Rule/RuleRepository.php index 16b6e46822..05758578a7 100644 --- a/app/Repositories/Rule/RuleRepository.php +++ b/app/Repositories/Rule/RuleRepository.php @@ -61,7 +61,7 @@ class RuleRepository implements RuleRepositoryInterface public function duplicate(Rule $rule): Rule { $newRule = $rule->replicate(); - $newRule->title = (string)trans('firefly.rule_copy_of', ['title' => $rule->title]); + $newRule->title = (string) trans('firefly.rule_copy_of', ['title' => $rule->title]); $newRule->save(); // replicate all triggers @@ -101,7 +101,7 @@ class RuleRepository implements RuleRepositoryInterface public function getHighestOrderInRuleGroup(RuleGroup $ruleGroup): int { - return (int)$ruleGroup->rules()->max('order'); + return (int) $ruleGroup->rules()->max('order'); } /** @@ -352,7 +352,7 @@ class RuleRepository implements RuleRepositoryInterface public function maxOrder(RuleGroup $ruleGroup): int { - return (int)$ruleGroup->rules()->max('order'); + return (int) $ruleGroup->rules()->max('order'); } private function storeTriggers(Rule $rule, array $data): void @@ -477,13 +477,16 @@ class RuleRepository implements RuleRepositoryInterface // update the order: $this->resetRuleOrder($group); if (array_key_exists('order', $data)) { - $this->moveRule($rule, $group, (int)$data['order']); + $this->moveRule($rule, $group, (int) $data['order']); } // update the triggers: if (array_key_exists('trigger', $data) && 'update-journal' === $data['trigger']) { $this->setRuleTrigger('update-journal', $rule); } + if (array_key_exists('trigger', $data) && 'manual-activation' === $data['trigger']) { + $this->setRuleTrigger('manual-activation', $rule); + } if (array_key_exists('trigger', $data) && 'store-journal' === $data['trigger']) { $this->setRuleTrigger('store-journal', $rule); } diff --git a/app/Repositories/RuleGroup/RuleGroupRepository.php b/app/Repositories/RuleGroup/RuleGroupRepository.php index 84d485c651..a5d6b57b78 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepository.php +++ b/app/Repositories/RuleGroup/RuleGroupRepository.php @@ -303,7 +303,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface { $entry = $this->user->ruleGroups()->max('order'); - return (int)$entry; + return (int) $entry; } public function getRuleGroupsWithRules(?string $filter): Collection @@ -364,7 +364,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface public function maxOrder(): int { - return (int)$this->user->ruleGroups()->where('active', true)->max('order'); + return (int) $this->user->ruleGroups()->where('active', true)->max('order'); } public function searchRuleGroup(string $query, int $limit): Collection @@ -448,7 +448,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface // order if (array_key_exists('order', $data) && $ruleGroup->order !== $data['order']) { $this->resetOrder(); - $this->setOrder($ruleGroup, (int)$data['order']); + $this->setOrder($ruleGroup, (int) $data['order']); } $ruleGroup->save(); diff --git a/app/Repositories/Tag/OperationsRepository.php b/app/Repositories/Tag/OperationsRepository.php index c1cfa72963..32ae337473 100644 --- a/app/Repositories/Tag/OperationsRepository.php +++ b/app/Repositories/Tag/OperationsRepository.php @@ -66,7 +66,7 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; $listedJournals = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ 'tags' => [], 'currency_id' => $currencyId, @@ -78,9 +78,9 @@ class OperationsRepository implements OperationsRepositoryInterface // may have multiple tags: foreach ($journal['tags'] as $tag) { - $tagId = (int)$tag['id']; - $tagName = (string)$tag['name']; - $journalId = (int)$journal['transaction_journal_id']; + $tagId = (int) $tag['id']; + $tagName = (string) $tag['name']; + $journalId = (int) $journal['transaction_journal_id']; if (!in_array($tagId, $tagIds, true)) { continue; } @@ -157,7 +157,7 @@ class OperationsRepository implements OperationsRepositoryInterface $listedJournals = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $array[$currencyId] ??= [ 'tags' => [], 'currency_id' => $currencyId, @@ -169,9 +169,9 @@ class OperationsRepository implements OperationsRepositoryInterface // may have multiple tags: foreach ($journal['tags'] as $tag) { - $tagId = (int)$tag['id']; - $tagName = (string)$tag['name']; - $journalId = (int)$journal['transaction_journal_id']; + $tagId = (int) $tag['id']; + $tagName = (string) $tag['name']; + $journalId = (int) $journal['transaction_journal_id']; if (!in_array($tagId, $tagIds, true)) { continue; @@ -187,7 +187,7 @@ class OperationsRepository implements OperationsRepositoryInterface 'name' => $tagName, 'transaction_journals' => [], ]; - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $array[$currencyId]['tags'][$tagId]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->positive($journal['amount']), 'date' => $journal['date'], diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 756ca3d738..e81173b003 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -259,7 +259,7 @@ class TagRepository implements TagRepositoryInterface if (false === $found) { continue; } - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $sums[$currencyId] ??= [ 'currency_id' => $currencyId, 'currency_name' => $journal['currency_name'], @@ -273,7 +273,7 @@ class TagRepository implements TagRepositoryInterface ]; // add amount to correct type: - $amount = app('steam')->positive((string)$journal['amount']); + $amount = app('steam')->positive((string) $journal['amount']); $type = $journal['transaction_type_type']; if (TransactionType::WITHDRAWAL === $type) { $amount = bcmul($amount, '-1'); @@ -294,7 +294,7 @@ class TagRepository implements TagRepositoryInterface TransactionType::OPENING_BALANCE => '0', ]; // add foreign amount to correct type: - $amount = app('steam')->positive((string)$journal['foreign_amount']); + $amount = app('steam')->positive((string) $journal['foreign_amount']); if (TransactionType::WITHDRAWAL === $type) { $amount = bcmul($amount, '-1'); } diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepository.php b/app/Repositories/TransactionGroup/TransactionGroupRepository.php index 0e55f25dc1..531c36a46a 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepository.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepository.php @@ -219,7 +219,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface 'link' => $entry->outward, 'group' => $entry->destination->transaction_group_id, 'description' => $entry->destination->description, - 'editable' => 1 === (int)$entry->editable, // @phpstan-ignore-line + 'editable' => 1 === (int) $entry->editable, // @phpstan-ignore-line 'amount' => $amount, 'foreign_amount' => $foreignAmount, ]; @@ -232,7 +232,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface 'link' => $entry->inward, 'group' => $entry->source->transaction_group_id, 'description' => $entry->source->description, - 'editable' => 1 === (int)$entry->editable, // @phpstan-ignore-line + 'editable' => 1 === (int) $entry->editable, // @phpstan-ignore-line 'amount' => $amount, 'foreign_amount' => $foreignAmount, ]; diff --git a/app/Repositories/User/UserRepository.php b/app/Repositories/User/UserRepository.php index 59b96f40a6..ef8b158831 100644 --- a/app/Repositories/User/UserRepository.php +++ b/app/Repositories/User/UserRepository.php @@ -201,7 +201,7 @@ class UserRepository implements UserRepositoryInterface // two factor: $return['has_2fa'] = null !== $user->mfa_secret; $return['is_admin'] = $this->hasRole($user, 'owner'); - $return['blocked'] = 1 === (int)$user->blocked; + $return['blocked'] = 1 === (int) $user->blocked; $return['blocked_code'] = $user->blocked_code; $return['accounts'] = $user->accounts()->count(); $return['journals'] = $user->transactionJournals()->count(); diff --git a/app/Repositories/UserGroup/UserGroupRepository.php b/app/Repositories/UserGroup/UserGroupRepository.php index 8ed4f51d83..89baea8799 100644 --- a/app/Repositories/UserGroup/UserGroupRepository.php +++ b/app/Repositories/UserGroup/UserGroupRepository.php @@ -174,6 +174,18 @@ class UserGroupRepository implements UserGroupRepositoryInterface return UserGroup::all(); } + #[\Override] + public function getById(int $id): ?UserGroup + { + return UserGroup::find($id); + } + + #[\Override] + public function getMembershipsFromGroupId(int $groupId): Collection + { + return $this->user->groupMemberships()->where('user_group_id', $groupId)->get(); + } + public function setUser(null|Authenticatable|User $user): void { app('log')->debug(sprintf('Now in %s', __METHOD__)); @@ -207,7 +219,7 @@ class UserGroupRepository implements UserGroupRepositoryInterface $user = User::find($data['id']); app('log')->debug('Found user by ID'); } - if (array_key_exists('email', $data) && '' !== (string)$data['email']) { + if (array_key_exists('email', $data) && '' !== (string) $data['email']) { /** @var null|User $user */ $user = User::whereEmail($data['email'])->first(); app('log')->debug('Found user by email'); @@ -225,13 +237,13 @@ class UserGroupRepository implements UserGroupRepositoryInterface if (1 === $membershipCount) { $lastUserId = $userGroup->groupMemberships()->distinct()->first(['group_memberships.user_id'])->user_id; // if this is also the user we're editing right now, and we remove all of their roles: - if ($lastUserId === (int)$user->id && 0 === count($data['roles'])) { + if ($lastUserId === (int) $user->id && 0 === count($data['roles'])) { app('log')->debug('User is last in this group, refuse to act'); throw new FireflyException('You cannot remove the last member from this user group. Delete the user group instead.'); } // if this is also the user we're editing right now, and do not grant them the owner role: - if ($lastUserId === (int)$user->id && count($data['roles']) > 0 && !in_array(UserRoleEnum::OWNER->value, $data['roles'], true)) { + if ($lastUserId === (int) $user->id && count($data['roles']) > 0 && !in_array(UserRoleEnum::OWNER->value, $data['roles'], true)) { app('log')->debug('User needs to have owner role in this group, refuse to act'); throw new FireflyException('The last member in this user group must get or keep the "owner" role.'); @@ -295,16 +307,4 @@ class UserGroupRepository implements UserGroupRepositoryInterface $this->user->user_group_id = $userGroup->id; $this->user->save(); } - - #[\Override] - public function getMembershipsFromGroupId(int $groupId): Collection - { - return $this->user->groupMemberships()->where('user_group_id', $groupId)->get(); - } - - #[\Override] - public function getById(int $id): ?UserGroup - { - return UserGroup::find($id); - } } diff --git a/app/Repositories/UserGroup/UserGroupRepositoryInterface.php b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php index 5a22f89d63..0bbeddf828 100644 --- a/app/Repositories/UserGroup/UserGroupRepositoryInterface.php +++ b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php @@ -36,13 +36,13 @@ interface UserGroupRepositoryInterface { public function destroy(UserGroup $userGroup): void; - public function getMembershipsFromGroupId(int $groupId): Collection; - public function get(): Collection; + public function getAll(): Collection; + public function getById(int $id): ?UserGroup; - public function getAll(): Collection; + public function getMembershipsFromGroupId(int $groupId): Collection; public function setUser(null|Authenticatable|User $user): void; diff --git a/app/Repositories/UserGroups/Account/AccountRepository.php b/app/Repositories/UserGroups/Account/AccountRepository.php index 7cfe21d561..bc66665c98 100644 --- a/app/Repositories/UserGroups/Account/AccountRepository.php +++ b/app/Repositories/UserGroups/Account/AccountRepository.php @@ -117,6 +117,12 @@ class AccountRepository implements AccountRepositoryInterface return $account; } + #[\Override] + public function getAccountBalances(Account $account): Collection + { + return $account->accountBalances; + } + public function getAccountCurrency(Account $account): ?TransactionCurrency { $type = $account->accountType->type; @@ -164,6 +170,15 @@ class AccountRepository implements AccountRepositoryInterface return $account; } + #[\Override] + public function getAccountTypes(Collection $accounts): Collection + { + return AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id') + ->whereIn('accounts.id', $accounts->pluck('id')->toArray()) + ->get(['accounts.id', 'account_types.type']) + ; + } + public function getAccountsById(array $accountIds): Collection { $query = $this->userGroup->accounts(); @@ -218,6 +233,57 @@ class AccountRepository implements AccountRepositoryInterface return $query->get(['accounts.*']); } + #[\Override] + public function getLastActivity(Collection $accounts): array + { + return Transaction::whereIn('account_id', $accounts->pluck('id')->toArray()) + ->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id') + ->groupBy('transactions.account_id') + ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line + ; + } + + #[\Override] + public function getMetaValues(Collection $accounts, array $fields): Collection + { + $query = AccountMeta::whereIn('account_id', $accounts->pluck('id')->toArray()); + if (count($fields) > 0) { + $query->whereIn('name', $fields); + } + + return $query->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']); + } + + #[\Override] + public function getObjectGroups(Collection $accounts): array + { + $groupIds = []; + $return = []; + $set = DB::table('object_groupables')->where('object_groupable_type', Account::class) + ->whereIn('object_groupable_id', $accounts->pluck('id')->toArray())->get() + ; + + /** @var \stdClass $row */ + foreach ($set as $row) { + $groupIds[] = $row->object_group_id; + } + $groupIds = array_unique($groupIds); + $groups = ObjectGroup::whereIn('id', $groupIds)->get(); + + /** @var \stdClass $row */ + foreach ($set as $row) { + if (!array_key_exists($row->object_groupable_id, $return)) { + /** @var null|ObjectGroup $group */ + $group = $groups->firstWhere('id', '=', $row->object_group_id); + if (null !== $group) { + $return[$row->object_groupable_id] = ['title' => $group->title, 'order' => $group->order, 'id' => $group->id]; + } + } + } + + return $return; + } + public function resetAccountOrder(): void { $sets = [ @@ -298,6 +364,15 @@ class AccountRepository implements AccountRepositoryInterface return $query->get(['accounts.*']); } + #[\Override] + public function update(Account $account, array $data): Account + { + /** @var AccountUpdateService $service */ + $service = app(AccountUpdateService::class); + + return $service->update($account, $data); + } + public function searchAccount(string $query, array $types, int $page, int $limit): Collection { // search by group, not by user @@ -331,79 +406,4 @@ class AccountRepository implements AccountRepositoryInterface return $dbQuery->get(['accounts.*']); } - - #[\Override] - public function update(Account $account, array $data): Account - { - /** @var AccountUpdateService $service */ - $service = app(AccountUpdateService::class); - - return $service->update($account, $data); - } - - #[\Override] - public function getMetaValues(Collection $accounts, array $fields): Collection - { - $query = AccountMeta::whereIn('account_id', $accounts->pluck('id')->toArray()); - if (count($fields) > 0) { - $query->whereIn('name', $fields); - } - - return $query->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']); - } - - #[\Override] - public function getAccountTypes(Collection $accounts): Collection - { - return AccountType::leftJoin('accounts', 'accounts.account_type_id', '=', 'account_types.id') - ->whereIn('accounts.id', $accounts->pluck('id')->toArray()) - ->get(['accounts.id', 'account_types.type']) - ; - } - - #[\Override] - public function getLastActivity(Collection $accounts): array - { - return Transaction::whereIn('account_id', $accounts->pluck('id')->toArray()) - ->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id') - ->groupBy('transactions.account_id') - ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) as date_max')])->toArray() // @phpstan-ignore-line - ; - } - - #[\Override] - public function getObjectGroups(Collection $accounts): array - { - $groupIds = []; - $return = []; - $set = DB::table('object_groupables')->where('object_groupable_type', Account::class) - ->whereIn('object_groupable_id', $accounts->pluck('id')->toArray())->get() - ; - - /** @var \stdClass $row */ - foreach ($set as $row) { - $groupIds[] = $row->object_group_id; - } - $groupIds = array_unique($groupIds); - $groups = ObjectGroup::whereIn('id', $groupIds)->get(); - - /** @var \stdClass $row */ - foreach ($set as $row) { - if (!array_key_exists($row->object_groupable_id, $return)) { - /** @var null|ObjectGroup $group */ - $group = $groups->firstWhere('id', '=', $row->object_group_id); - if (null !== $group) { - $return[$row->object_groupable_id] = ['title' => $group->title, 'order' => $group->order, 'id' => $group->id]; - } - } - } - - return $return; - } - - #[\Override] - public function getAccountBalances(Account $account): Collection - { - return $account->accountBalances; - } } diff --git a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php index b10a26f267..17b3fad59b 100644 --- a/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php +++ b/app/Repositories/UserGroups/Account/AccountRepositoryInterface.php @@ -37,12 +37,6 @@ interface AccountRepositoryInterface { public function countAccounts(array $types): int; - public function getAccountTypes(Collection $accounts): Collection; - - public function getLastActivity(Collection $accounts): array; - - public function getMetaValues(Collection $accounts, array $fields): Collection; - public function find(int $accountId): ?Account; public function findByAccountNumber(string $number, array $types): ?Account; @@ -51,9 +45,11 @@ interface AccountRepositoryInterface public function findByName(string $name, array $types): ?Account; + public function getAccountBalances(Account $account): Collection; + public function getAccountCurrency(Account $account): ?TransactionCurrency; - public function getAccountBalances(Account $account): Collection; + public function getAccountTypes(Collection $accounts): Collection; public function getAccountsById(array $accountIds): Collection; @@ -66,11 +62,15 @@ interface AccountRepositoryInterface public function getActiveAccountsByType(array $types): Collection; + public function getLastActivity(Collection $accounts): array; + /** * Return meta value for account. Null if not found. */ public function getMetaValue(Account $account, string $field): ?string; + public function getMetaValues(Collection $accounts, array $fields): Collection; + public function getObjectGroups(Collection $accounts): array; public function getUserGroup(): UserGroup; diff --git a/app/Repositories/UserGroups/Bill/BillRepository.php b/app/Repositories/UserGroups/Bill/BillRepository.php index a6d2be535b..a6fc1e71b2 100644 --- a/app/Repositories/UserGroups/Bill/BillRepository.php +++ b/app/Repositories/UserGroups/Bill/BillRepository.php @@ -81,12 +81,12 @@ class BillRepository implements BillRepositoryInterface $currencyId = $bill->transaction_currency_id; $return[$currencyId] ??= [ - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_name' => $currency->name, 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, 'currency_decimal_places' => $currency->decimal_places, - 'native_currency_id' => (string)$default->id, + 'native_currency_id' => (string) $default->id, 'native_currency_name' => $default->name, 'native_currency_symbol' => $default->symbol, 'native_currency_code' => $default->code, @@ -101,9 +101,9 @@ class BillRepository implements BillRepositoryInterface $sourceTransaction = $transactionJournal->transactions()->where('amount', '<', 0)->first(); if (null !== $sourceTransaction) { $amount = $sourceTransaction->amount; - if ((int)$sourceTransaction->foreign_currency_id === $currency->id) { + if ((int) $sourceTransaction->foreign_currency_id === $currency->id) { // use foreign amount instead! - $amount = (string)$sourceTransaction->foreign_amount; + $amount = (string) $sourceTransaction->foreign_amount; } // convert to native currency $nativeAmount = $amount; @@ -111,9 +111,9 @@ class BillRepository implements BillRepositoryInterface // get rate and convert. $nativeAmount = $converter->convert($currency, $default, $transactionJournal->date, $amount); } - if ((int)$sourceTransaction->foreign_currency_id === $default->id) { + if ((int) $sourceTransaction->foreign_currency_id === $default->id) { // ignore conversion, use foreign amount - $nativeAmount = (string)$sourceTransaction->foreign_amount; + $nativeAmount = (string) $sourceTransaction->foreign_amount; } $return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], $amount); $return[$currencyId]['native_sum'] = bcadd($return[$currencyId]['native_sum'], $nativeAmount); @@ -154,12 +154,12 @@ class BillRepository implements BillRepositoryInterface $average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2'); $nativeAverage = $converter->convert($currency, $default, $start, $average); $return[$currencyId] ??= [ - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_name' => $currency->name, 'currency_symbol' => $currency->symbol, 'currency_code' => $currency->code, 'currency_decimal_places' => $currency->decimal_places, - 'native_currency_id' => (string)$default->id, + 'native_currency_id' => (string) $default->id, 'native_currency_name' => $default->name, 'native_currency_symbol' => $default->symbol, 'native_currency_code' => $default->code, @@ -167,8 +167,8 @@ class BillRepository implements BillRepositoryInterface 'sum' => '0', 'native_sum' => '0', ]; - $return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], bcmul($average, (string)$total)); - $return[$currencyId]['native_sum'] = bcadd($return[$currencyId]['native_sum'], bcmul($nativeAverage, (string)$total)); + $return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], bcmul($average, (string) $total)); + $return[$currencyId]['native_sum'] = bcadd($return[$currencyId]['native_sum'], bcmul($nativeAverage, (string) $total)); } } $converter->summarize(); diff --git a/app/Repositories/UserGroups/Budget/BudgetRepository.php b/app/Repositories/UserGroups/Budget/BudgetRepository.php index 9f95eee2ab..f86e58e601 100644 --- a/app/Repositories/UserGroups/Budget/BudgetRepository.php +++ b/app/Repositories/UserGroups/Budget/BudgetRepository.php @@ -42,4 +42,13 @@ class BudgetRepository implements BudgetRepositoryInterface ->get() ; } + + public function getBudgets(): Collection + { + return $this->userGroup->budgets() + ->orderBy('order', 'ASC') + ->orderBy('name', 'ASC') + ->get() + ; + } } diff --git a/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php b/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php index 6b52c939be..c6929275b5 100644 --- a/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php @@ -35,6 +35,8 @@ interface BudgetRepositoryInterface { public function getActiveBudgets(): Collection; + public function getBudgets(): Collection; + public function setUser(User $user): void; public function setUserGroup(UserGroup $userGroup): void; diff --git a/app/Repositories/UserGroups/Budget/OperationsRepository.php b/app/Repositories/UserGroups/Budget/OperationsRepository.php index 1b115e1aba..3d4c5f0108 100644 --- a/app/Repositories/UserGroups/Budget/OperationsRepository.php +++ b/app/Repositories/UserGroups/Budget/OperationsRepository.php @@ -60,9 +60,9 @@ class OperationsRepository implements OperationsRepositoryInterface $array = []; foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; - $budgetId = (int)$journal['budget_id']; - $budgetName = (string)$journal['budget_name']; + $currencyId = (int) $journal['currency_id']; + $budgetId = (int) $journal['budget_id']; + $budgetName = (string) $journal['budget_name']; // catch "no budget" entries. if (0 === $budgetId) { @@ -88,7 +88,7 @@ class OperationsRepository implements OperationsRepositoryInterface // add journal to array: // only a subset of the fields. - $journalId = (int)$journal['transaction_journal_id']; + $journalId = (int) $journal['transaction_journal_id']; $final = [ 'amount' => app('steam')->negative($journal['amount']), 'currency_id' => $journal['currency_id'], diff --git a/app/Repositories/UserGroups/Currency/CurrencyRepository.php b/app/Repositories/UserGroups/Currency/CurrencyRepository.php index e12a5c7b4c..147e6599da 100644 --- a/app/Repositories/UserGroups/Currency/CurrencyRepository.php +++ b/app/Repositories/UserGroups/Currency/CurrencyRepository.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\UserGroups\Currency; +use FireflyIII\Events\Preferences\UserGroupChangedDefaultCurrency; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Models\AccountMeta; @@ -38,6 +39,7 @@ use FireflyIII\Services\Internal\Destroy\CurrencyDestroyService; use FireflyIII\Services\Internal\Update\CurrencyUpdateService; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class CurrencyRepository @@ -77,7 +79,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // is being used in accounts: - $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((string)$currency->id))->count(); + $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((string) $currency->id))->count(); if ($meta > 0) { app('log')->info(sprintf('Used in %d accounts as currency_id, return true. ', $meta)); @@ -85,7 +87,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // second search using integer check. - $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((int)$currency->id))->count(); + $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((int) $currency->id))->count(); if ($meta > 0) { app('log')->info(sprintf('Used in %d accounts as currency_id, return true. ', $meta)); @@ -179,7 +181,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $entry->id === $current->id; }); $isDefault = $local->contains(static function (TransactionCurrency $entry) use ($current) { - return 1 === (int)$entry->pivot->group_default && $entry->id === $current->id; + return 1 === (int) $entry->pivot->group_default && $entry->id === $current->id; }); $current->userGroupEnabled = $hasId; $current->userGroupDefault = $isDefault; @@ -193,7 +195,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface $all = $this->userGroup->currencies()->orderBy('code', 'ASC')->withPivot(['group_default'])->get(); $all->map(static function (TransactionCurrency $current) { $current->userGroupEnabled = true; - $current->userGroupDefault = 1 === (int)$current->pivot->group_default; + $current->userGroupDefault = 1 === (int) $current->pivot->group_default; return $current; }); @@ -260,10 +262,10 @@ class CurrencyRepository implements CurrencyRepositoryInterface public function findCurrencyNull(?int $currencyId, ?string $currencyCode): ?TransactionCurrency { app('log')->debug('Now in findCurrencyNull()'); - $result = $this->find((int)$currencyId); + $result = $this->find((int) $currencyId); if (null === $result) { app('log')->debug(sprintf('Searching for currency with code %s...', $currencyCode)); - $result = $this->findByCode((string)$currencyCode); + $result = $this->findByCode((string) $currencyCode); } if (null !== $result && false === $result->enabled) { app('log')->debug(sprintf('Also enabled currency %s', $result->code)); @@ -306,16 +308,6 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $currency->code === config('firefly.default_currency', 'EUR'); } - public function makeDefault(TransactionCurrency $currency): void - { - app('log')->debug(sprintf('Enabled + made default currency %s for user #%d', $currency->code, $this->userGroup->id)); - $this->userGroup->currencies()->detach($currency->id); - foreach ($this->userGroup->currencies()->get() as $item) { - $this->userGroup->currencies()->updateExistingPivot($item->id, ['group_default' => false]); - } - $this->userGroup->currencies()->syncWithoutDetaching([$currency->id => ['group_default' => true]]); - } - public function searchCurrency(string $search, int $limit): Collection { $query = TransactionCurrency::where('enabled', true); @@ -354,9 +346,6 @@ class CurrencyRepository implements CurrencyRepositoryInterface if (false === $enabled && true === $default) { $enabled = true; } - if (false === $default) { - app('log')->warning(sprintf('Set default=false will NOT do anything for currency %s', $currency->code)); - } // update currency with current user specific settings $currency->refreshForUser($this->user); @@ -375,12 +364,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface // currency must be made default. if (true === $default) { - app('log')->debug(sprintf('Enabled + made default currency %s for user #%d', $currency->code, $this->userGroup->id)); - $this->userGroup->currencies()->detach($currency->id); - foreach ($this->userGroup->currencies()->get() as $item) { - $this->userGroup->currencies()->updateExistingPivot($item->id, ['group_default' => false]); - } - $this->userGroup->currencies()->syncWithoutDetaching([$currency->id => ['group_default' => true]]); + $this->makeDefault($currency); } /** @var CurrencyUpdateService $service */ @@ -388,4 +372,20 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $service->update($currency, $data); } + + public function makeDefault(TransactionCurrency $currency): void + { + $current = app('amount')->getDefaultCurrencyByUserGroup($this->userGroup); + app('log')->debug(sprintf('Enabled + made default currency %s for user #%d', $currency->code, $this->userGroup->id)); + $this->userGroup->currencies()->detach($currency->id); + foreach ($this->userGroup->currencies()->get() as $item) { + $this->userGroup->currencies()->updateExistingPivot($item->id, ['group_default' => false]); + } + $this->userGroup->currencies()->syncWithoutDetaching([$currency->id => ['group_default' => true]]); + if ($current->id !== $currency->id) { + Log::debug('Trigger on a different default currency.'); + // clear all native amounts through an event. + event(new UserGroupChangedDefaultCurrency($this->userGroup)); + } + } } diff --git a/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepository.php b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepository.php new file mode 100644 index 0000000000..12150987ac --- /dev/null +++ b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepository.php @@ -0,0 +1,106 @@ +toRawSql(); + return + $this->userGroup->currencyExchangeRates() + ->where(function (Builder $q1) use ($from, $to): void { + $q1->where(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ; + })->orWhere(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $to->id) + ->where('to_currency_id', $from->id) + ; + }); + }) + ->orderBy('date', 'DESC') + ->get(['currency_exchange_rates.*']) + ; + + } + + #[\Override] + public function getSpecificRateOnDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): ?CurrencyExchangeRate + { + return + $this->userGroup->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', $date->format('Y-m-d')) + ->first() + ; + } + + #[\Override] + public function deleteRate(CurrencyExchangeRate $rate): void + { + $this->userGroup->currencyExchangeRates()->where('id', $rate->id)->delete(); + } + + #[\Override] + public function updateExchangeRate(CurrencyExchangeRate $object, string $rate, ?Carbon $date = null): CurrencyExchangeRate + { + $object->rate = $rate; + if (null !== $date) { + $object->date = $date; + } + $object->save(); + + return $object; + } + + #[\Override] + public function storeExchangeRate(TransactionCurrency $from, TransactionCurrency $to, string $rate, Carbon $date): CurrencyExchangeRate + { + $object = new CurrencyExchangeRate(); + $object->user_id = auth()->user()->id; + $object->user_group_id = $this->userGroup->id; + $object->from_currency_id = $from->id; + $object->to_currency_id = $to->id; + $object->rate = $rate; + $object->date = $date; + $object->date_tz = $date->format('e'); + $object->save(); + + return $object; + } +} diff --git a/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepositoryInterface.php b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepositoryInterface.php new file mode 100644 index 0000000000..3acd4d3d06 --- /dev/null +++ b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepositoryInterface.php @@ -0,0 +1,43 @@ +userGroup->piggyBanks() + return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_group_id', $this->userGroup->id) ->with( [ - 'account', 'objectGroups', ] ) - ->orderBy('order', 'ASC')->get() + ->orderBy('piggy_banks.order', 'ASC')->distinct()->get(['piggy_banks.*']) ; } } diff --git a/app/Rules/Account/IsUniqueAccount.php b/app/Rules/Account/IsUniqueAccount.php index bc9bf925ff..5a3fa3db9b 100644 --- a/app/Rules/Account/IsUniqueAccount.php +++ b/app/Rules/Account/IsUniqueAccount.php @@ -35,8 +35,14 @@ use Illuminate\Contracts\Validation\ValidationRule; */ class IsUniqueAccount implements ValidationRule, DataAwareRule { + protected array $data = []; protected \Closure $fail; - protected array $data = []; + + #[\Override] + public function setData(array $data): void + { + $this->data = $data; + } #[\Override] public function validate(string $attribute, mixed $value, \Closure $fail): void @@ -103,28 +109,6 @@ class IsUniqueAccount implements ValidationRule, DataAwareRule return null === $result; } - /** - * TODO duplicate from old validation class. - */ - private function validateAccountAnonymously(): bool - { - if (!array_key_exists('user_id', $this->data)) { - $this->fail('No user ID provided.'); - - return false; - } - - /** @var User $user */ - $user = User::find($this->data['user_id']); - $type = AccountType::find($this->data['account_type_id'])->first(); - $value = $this->data['name']; - - /** @var null|Account $result */ - $result = $user->accounts()->where('account_type_id', $type->id)->where('name', $value)->first(); - - return null === $result; - } - /** * TODO Duplicate from old validation class. * @@ -196,9 +180,25 @@ class IsUniqueAccount implements ValidationRule, DataAwareRule return 0 === auth()->user()->accounts()->where('name', $value)->count(); } - #[\Override] - public function setData(array $data): void + /** + * TODO duplicate from old validation class. + */ + private function validateAccountAnonymously(): bool { - $this->data = $data; + if (!array_key_exists('user_id', $this->data)) { + $this->fail('No user ID provided.'); + + return false; + } + + /** @var User $user */ + $user = User::find($this->data['user_id']); + $type = AccountType::find($this->data['account_type_id'])->first(); + $value = $this->data['name']; + + /** @var null|Account $result */ + $result = $user->accounts()->where('account_type_id', $type->id)->where('name', $value)->first(); + + return null === $result; } } diff --git a/app/Rules/Admin/IsValidDiscordUrl.php b/app/Rules/Admin/IsValidDiscordUrl.php new file mode 100644 index 0000000000..45603fa93d --- /dev/null +++ b/app/Rules/Admin/IsValidDiscordUrl.php @@ -0,0 +1,32 @@ +translate(); + $message = sprintf('IsValidDiscordUrl: "%s" is not a discord URL.', substr($value, 0, 255)); + Log::debug($message); + Log::channel('audit')->info($message); + } + } +} diff --git a/app/Rules/Admin/IsValidSlackOrDiscordUrl.php b/app/Rules/Admin/IsValidSlackOrDiscordUrl.php new file mode 100644 index 0000000000..936063e9d3 --- /dev/null +++ b/app/Rules/Admin/IsValidSlackOrDiscordUrl.php @@ -0,0 +1,32 @@ +translate(); + $message = sprintf('IsValidSlackUrl: "%s" is not a discord or slack URL.', substr($value, 0, 255)); + Log::debug($message); + Log::channel('audit')->info($message); + } + } +} diff --git a/app/Rules/Admin/IsValidSlackUrl.php b/app/Rules/Admin/IsValidSlackUrl.php new file mode 100644 index 0000000000..938e330c4a --- /dev/null +++ b/app/Rules/Admin/IsValidSlackUrl.php @@ -0,0 +1,32 @@ +translate(); + $message = sprintf('IsValidSlackUrl: "%s" is not a slack URL.', substr($value, 0, 255)); + Log::debug($message); + Log::channel('audit')->info($message); + } + } +} diff --git a/app/Rules/BelongsUser.php b/app/Rules/BelongsUser.php index 06e250cf63..b223de2165 100644 --- a/app/Rules/BelongsUser.php +++ b/app/Rules/BelongsUser.php @@ -49,15 +49,15 @@ class BelongsUser implements ValidationRule app('log')->debug(sprintf('Going to validate %s', $attribute)); $result = match ($attribute) { - 'piggy_bank_id' => $this->validatePiggyBankId((int)$value), + 'piggy_bank_id' => $this->validatePiggyBankId((int) $value), 'piggy_bank_name' => $this->validatePiggyBankName($value), - 'bill_id' => $this->validateBillId((int)$value), - 'transaction_journal_id' => $this->validateJournalId((int)$value), + 'bill_id' => $this->validateBillId((int) $value), + 'transaction_journal_id' => $this->validateJournalId((int) $value), 'bill_name' => $this->validateBillName($value), - 'budget_id' => $this->validateBudgetId((int)$value), - 'category_id' => $this->validateCategoryId((int)$value), + 'budget_id' => $this->validateBudgetId((int) $value), + 'category_id' => $this->validateCategoryId((int) $value), 'budget_name' => $this->validateBudgetName($value), - 'source_id', 'destination_id' => $this->validateAccountId((int)$value), + 'source_id', 'destination_id' => $this->validateAccountId((int) $value), default => throw new FireflyException(sprintf('Rule BelongsUser cannot handle "%s"', $attribute)), }; if (false === $result) { @@ -110,7 +110,7 @@ class BelongsUser implements ValidationRule } $count = 0; foreach ($objects as $object) { - $objectValue = trim((string)$object->{$field}); // @phpstan-ignore-line + $objectValue = trim((string) $object->{$field}); // @phpstan-ignore-line app('log')->debug(sprintf('Comparing object "%s" with value "%s"', $objectValue, $value)); if ($objectValue === $value) { ++$count; diff --git a/app/Rules/BelongsUserGroup.php b/app/Rules/BelongsUserGroup.php index a6e5e2c4a7..dc30295e27 100644 --- a/app/Rules/BelongsUserGroup.php +++ b/app/Rules/BelongsUserGroup.php @@ -63,15 +63,15 @@ class BelongsUserGroup implements ValidationRule app('log')->debug(sprintf('Group: Going to validate "%s"', $attribute)); $result = match ($attribute) { - 'piggy_bank_id' => $this->validatePiggyBankId((int)$value), + 'piggy_bank_id' => $this->validatePiggyBankId((int) $value), 'piggy_bank_name' => $this->validatePiggyBankName($value), - 'bill_id' => $this->validateBillId((int)$value), - 'transaction_journal_id' => $this->validateJournalId((int)$value), + 'bill_id' => $this->validateBillId((int) $value), + 'transaction_journal_id' => $this->validateJournalId((int) $value), 'bill_name' => $this->validateBillName($value), - 'budget_id' => $this->validateBudgetId((int)$value), - 'category_id' => $this->validateCategoryId((int)$value), + 'budget_id' => $this->validateBudgetId((int) $value), + 'category_id' => $this->validateCategoryId((int) $value), 'budget_name' => $this->validateBudgetName($value), - 'source_id', 'destination_id' => $this->validateAccountId((int)$value), + 'source_id', 'destination_id' => $this->validateAccountId((int) $value), default => throw new FireflyException(sprintf('Rule BelongsUser cannot handle "%s"', $attribute)), }; if (false === $result) { @@ -124,7 +124,7 @@ class BelongsUserGroup implements ValidationRule } $count = 0; foreach ($objects as $object) { - $objectValue = trim((string)$object->{$field}); // @phpstan-ignore-line + $objectValue = trim((string) $object->{$field}); // @phpstan-ignore-line app('log')->debug(sprintf('Comparing object "%s" with value "%s"', $objectValue, $value)); if ($objectValue === $value) { ++$count; diff --git a/app/Rules/IsAllowedGroupAction.php b/app/Rules/IsAllowedGroupAction.php index d79c9564b1..383edce267 100644 --- a/app/Rules/IsAllowedGroupAction.php +++ b/app/Rules/IsAllowedGroupAction.php @@ -28,16 +28,15 @@ use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\Account; use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; use FireflyIII\User; -use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Support\Facades\Log; class IsAllowedGroupAction implements ValidationRule { + private array $acceptedRoles; private string $className; private string $methodName; - - private array $acceptedRoles; private UserGroupRepositoryInterface $repository; public function __construct(string $className, string $methodName) @@ -71,7 +70,7 @@ class IsAllowedGroupAction implements ValidationRule break; } } - $this->validateUserGroup((int)$value, $fail); + $this->validateUserGroup((int) $value, $fail); } private function validateUserGroup(int $userGroupId, \Closure $fail): void diff --git a/app/Rules/IsAssetAccountId.php b/app/Rules/IsAssetAccountId.php index 5c4e715f36..20e2458fae 100644 --- a/app/Rules/IsAssetAccountId.php +++ b/app/Rules/IsAssetAccountId.php @@ -37,7 +37,7 @@ class IsAssetAccountId implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $accountId = (int)$value; + $accountId = (int) $value; /** @var null|Account $account */ $account = Account::with('accountType')->find($accountId); diff --git a/app/Rules/IsDateOrTime.php b/app/Rules/IsDateOrTime.php index 60ccb3a963..32d0176aad 100644 --- a/app/Rules/IsDateOrTime.php +++ b/app/Rules/IsDateOrTime.php @@ -39,7 +39,7 @@ class IsDateOrTime implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $value = (string)$value; + $value = (string) $value; if ('' === $value) { $fail('validation.date_or_time')->translate(); diff --git a/app/Rules/IsFilterValueIn.php b/app/Rules/IsFilterValueIn.php index 8601007a68..2e4b92d0d6 100644 --- a/app/Rules/IsFilterValueIn.php +++ b/app/Rules/IsFilterValueIn.php @@ -29,7 +29,7 @@ use Illuminate\Contracts\Validation\ValidationRule; class IsFilterValueIn implements ValidationRule { private string $key; - private array $values; + private array $values; public function __construct(string $key, array $values) { diff --git a/app/Rules/IsTransferAccount.php b/app/Rules/IsTransferAccount.php index e61b07ee47..8c255d5405 100644 --- a/app/Rules/IsTransferAccount.php +++ b/app/Rules/IsTransferAccount.php @@ -45,15 +45,15 @@ class IsTransferAccount implements ValidationRule $validator->setTransactionType(TransactionType::TRANSFER); $validator->setUser(auth()->user()); - $validAccount = $validator->validateSource(['name' => (string)$value]); + $validAccount = $validator->validateSource(['name' => (string) $value]); if (true === $validAccount) { app('log')->debug('Found account based on name. Return true.'); // found by name, use repos to return. return; } - $validAccount = $validator->validateSource(['id' => (int)$value]); - app('log')->debug(sprintf('Search by id (%d), result is %s.', (int)$value, var_export($validAccount, true))); + $validAccount = $validator->validateSource(['id' => (int) $value]); + app('log')->debug(sprintf('Search by id (%d), result is %s.', (int) $value, var_export($validAccount, true))); if (false === $validAccount) { $fail('validation.not_transfer_account')->translate(); diff --git a/app/Rules/IsValidAmount.php b/app/Rules/IsValidAmount.php index 598dec8965..c3bfa2f371 100644 --- a/app/Rules/IsValidAmount.php +++ b/app/Rules/IsValidAmount.php @@ -17,7 +17,7 @@ class IsValidAmount implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $value = (string)$value; + $value = (string) $value; // must not be empty: if ($this->emptyString($value)) { diff --git a/app/Rules/IsValidAttachmentModel.php b/app/Rules/IsValidAttachmentModel.php index 11dda64a16..dcdec6409f 100644 --- a/app/Rules/IsValidAttachmentModel.php +++ b/app/Rules/IsValidAttachmentModel.php @@ -78,14 +78,14 @@ class IsValidAttachmentModel implements ValidationRule return; } $result = match ($this->model) { - Account::class => $this->validateAccount((int)$value), - Bill::class => $this->validateBill((int)$value), - Budget::class => $this->validateBudget((int)$value), - Category::class => $this->validateCategory((int)$value), - PiggyBank::class => $this->validatePiggyBank((int)$value), - Tag::class => $this->validateTag((int)$value), - Transaction::class => $this->validateTransaction((int)$value), - TransactionJournal::class => $this->validateJournal((int)$value), + Account::class => $this->validateAccount((int) $value), + Bill::class => $this->validateBill((int) $value), + Budget::class => $this->validateBudget((int) $value), + Category::class => $this->validateCategory((int) $value), + PiggyBank::class => $this->validatePiggyBank((int) $value), + Tag::class => $this->validateTag((int) $value), + Transaction::class => $this->validateTransaction((int) $value), + TransactionJournal::class => $this->validateJournal((int) $value), default => false, }; diff --git a/app/Rules/IsValidBulkClause.php b/app/Rules/IsValidBulkClause.php index 1a79add025..8250c169ac 100644 --- a/app/Rules/IsValidBulkClause.php +++ b/app/Rules/IsValidBulkClause.php @@ -38,7 +38,7 @@ class IsValidBulkClause implements ValidationRule public function __construct(string $type) { $this->rules = config(sprintf('bulk.%s', $type)); - $this->error = (string)trans('firefly.belongs_user'); + $this->error = (string) trans('firefly.belongs_user'); } public function message(): string @@ -51,7 +51,7 @@ class IsValidBulkClause implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $result = $this->basicValidation((string)$value); + $result = $this->basicValidation((string) $value); if (false === $result) { $fail($this->error); } @@ -65,14 +65,14 @@ class IsValidBulkClause implements ValidationRule try { $array = json_decode($value, true, 8, JSON_THROW_ON_ERROR); } catch (\JsonException $e) { - $this->error = (string)trans('validation.json'); + $this->error = (string) trans('validation.json'); return false; } $clauses = ['where', 'update']; foreach ($clauses as $clause) { if (!array_key_exists($clause, $array)) { - $this->error = (string)trans(sprintf('validation.missing_%s', $clause)); + $this->error = (string) trans(sprintf('validation.missing_%s', $clause)); return false; } @@ -83,7 +83,7 @@ class IsValidBulkClause implements ValidationRule */ foreach ($array[$clause] as $arrayKey => $arrayValue) { if (!array_key_exists($arrayKey, $this->rules[$clause])) { - $this->error = (string)trans(sprintf('validation.invalid_%s_key', $clause)); + $this->error = (string) trans(sprintf('validation.invalid_%s_key', $clause)); return false; } diff --git a/app/Rules/IsValidPositiveAmount.php b/app/Rules/IsValidPositiveAmount.php index 7a6b559c7f..94a0524737 100644 --- a/app/Rules/IsValidPositiveAmount.php +++ b/app/Rules/IsValidPositiveAmount.php @@ -17,7 +17,7 @@ class IsValidPositiveAmount implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $value = (string)$value; + $value = (string) $value; // must not be empty: if ($this->emptyString($value)) { $fail('validation.filled')->translate(); diff --git a/app/Rules/IsValidZeroOrMoreAmount.php b/app/Rules/IsValidZeroOrMoreAmount.php index 1d2866d06f..15e1f2ce08 100644 --- a/app/Rules/IsValidZeroOrMoreAmount.php +++ b/app/Rules/IsValidZeroOrMoreAmount.php @@ -17,7 +17,7 @@ class IsValidZeroOrMoreAmount implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $value = (string)$value; + $value = (string) $value; // must not be empty: if ($this->emptyString($value)) { $fail('validation.filled')->translate(); diff --git a/app/Rules/LessThanPiggyTarget.php b/app/Rules/LessThanPiggyTarget.php index 433d02dd2e..a0fc41c38c 100644 --- a/app/Rules/LessThanPiggyTarget.php +++ b/app/Rules/LessThanPiggyTarget.php @@ -36,7 +36,7 @@ class LessThanPiggyTarget implements ValidationRule */ public function message(): string { - return (string)trans('validation.current_target_amount'); + return (string) trans('validation.current_target_amount'); } /** diff --git a/app/Rules/UniqueAccountNumber.php b/app/Rules/UniqueAccountNumber.php index c641f2c48f..e2e22592ca 100644 --- a/app/Rules/UniqueAccountNumber.php +++ b/app/Rules/UniqueAccountNumber.php @@ -65,7 +65,7 @@ class UniqueAccountNumber implements ValidationRule */ public function message(): string { - return (string)trans('validation.unique_account_number_for_user'); + return (string) trans('validation.unique_account_number_for_user'); } /** diff --git a/app/Rules/UniqueIban.php b/app/Rules/UniqueIban.php index b19e8caaa5..4991d0f37a 100644 --- a/app/Rules/UniqueIban.php +++ b/app/Rules/UniqueIban.php @@ -68,13 +68,13 @@ class UniqueIban implements ValidationRule */ public function message(): string { - return (string)trans('validation.unique_iban_for_user'); + return (string) trans('validation.unique_iban_for_user'); } public function validate(string $attribute, mixed $value, \Closure $fail): void { if (!$this->passes($attribute, $value)) { - $fail((string)trans('validation.unique_iban_for_user')); + $fail((string) trans('validation.unique_iban_for_user')); } } diff --git a/app/Rules/ValidRecurrenceRepetitionType.php b/app/Rules/ValidRecurrenceRepetitionType.php index 9e0d39a2a7..06a762cc15 100644 --- a/app/Rules/ValidRecurrenceRepetitionType.php +++ b/app/Rules/ValidRecurrenceRepetitionType.php @@ -38,7 +38,7 @@ class ValidRecurrenceRepetitionType implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $value = (string)$value; + $value = (string) $value; if ('daily' === $value) { return; } diff --git a/app/Rules/ValidRecurrenceRepetitionValue.php b/app/Rules/ValidRecurrenceRepetitionValue.php index 0d1ff35948..046d7bcf8d 100644 --- a/app/Rules/ValidRecurrenceRepetitionValue.php +++ b/app/Rules/ValidRecurrenceRepetitionValue.php @@ -37,7 +37,7 @@ class ValidRecurrenceRepetitionValue implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { - $value = (string)$value; + $value = (string) $value; if ('daily' === $value) { return; @@ -68,7 +68,7 @@ class ValidRecurrenceRepetitionValue implements ValidationRule private function validateMonthly(string $value): bool { - $dayOfMonth = (int)substr($value, 8); + $dayOfMonth = (int) substr($value, 8); return $dayOfMonth > 0 && $dayOfMonth < 32; } @@ -79,8 +79,8 @@ class ValidRecurrenceRepetitionValue implements ValidationRule if (2 !== count($parameters)) { return false; } - $nthDay = (int)($parameters[0] ?? 0.0); - $dayOfWeek = (int)($parameters[1] ?? 0.0); + $nthDay = (int) ($parameters[0] ?? 0.0); + $dayOfWeek = (int) ($parameters[1] ?? 0.0); if ($nthDay < 1 || $nthDay > 5) { return false; } @@ -90,7 +90,7 @@ class ValidRecurrenceRepetitionValue implements ValidationRule private function validateWeekly(string $value): bool { - $dayOfWeek = (int)substr($value, 7); + $dayOfWeek = (int) substr($value, 7); return $dayOfWeek > 0 && $dayOfWeek < 8; } diff --git a/app/Services/FireflyIIIOrg/Update/UpdateRequest.php b/app/Services/FireflyIIIOrg/Update/UpdateRequest.php index 196643dc19..1e46bdc273 100644 --- a/app/Services/FireflyIIIOrg/Update/UpdateRequest.php +++ b/app/Services/FireflyIIIOrg/Update/UpdateRequest.php @@ -40,7 +40,7 @@ class UpdateRequest implements UpdateRequestInterface app('log')->debug(sprintf('Now in getUpdateInformation(%s)', $channel)); $information = [ 'level' => 'error', - 'message' => (string)trans('firefly.unknown_error'), + 'message' => (string) trans('firefly.unknown_error'), ]; // try to get array from update server: @@ -65,7 +65,7 @@ class UpdateRequest implements UpdateRequestInterface 'version' => config('firefly.version'), 'date' => today(config('app.timezone'))->startOfDay(), 'level' => 'error', - 'message' => (string)trans('firefly.unknown_error'), + 'message' => (string) trans('firefly.unknown_error'), ]; $url = config('firefly.update_endpoint'); @@ -91,12 +91,12 @@ class UpdateRequest implements UpdateRequestInterface if (200 !== $res->getStatusCode()) { app('log')->error(sprintf('Response status from server is %d.', $res->getStatusCode())); - app('log')->error((string)$res->getBody()); + app('log')->error((string) $res->getBody()); $return['message'] = sprintf('Error: %d', $res->getStatusCode()); return $return; } - $body = (string)$res->getBody(); + $body = (string) $res->getBody(); try { $json = json_decode($body, true, 512, JSON_THROW_ON_ERROR); @@ -135,8 +135,8 @@ class UpdateRequest implements UpdateRequestInterface private function parseResult(array $information): array { app('log')->debug('Now in parseResult()', $information); - $current = (string)config('firefly.version'); - $latest = (string)$information['version']; + $current = (string) config('firefly.version'); + $latest = (string) $information['version']; // strip the 'v' from the version if it's there. if (str_starts_with($latest, 'v')) { @@ -189,13 +189,13 @@ class UpdateRequest implements UpdateRequestInterface if ($devDate->lte($information['date'])) { Log::debug(sprintf('This development release is older, release = %s, latest version %s = %s', $devDate->format('Y-m-d'), $latest, $information['date']->format('Y-m-d'))); $return['level'] = 'info'; - $return['message'] = (string)trans('firefly.update_current_dev_older', ['version' => $current, 'new_version' => $latest]); + $return['message'] = (string) trans('firefly.update_current_dev_older', ['version' => $current, 'new_version' => $latest]); return $return; } Log::debug(sprintf('This development release is newer, release = %s, latest version %s = %s', $devDate->format('Y-m-d'), $latest, $information['date']->format('Y-m-d'))); $return['level'] = 'info'; - $return['message'] = (string)trans('firefly.update_current_dev_newer', ['version' => $current, 'new_version' => $latest]); + $return['message'] = (string) trans('firefly.update_current_dev_newer', ['version' => $current, 'new_version' => $latest]); return $return; } @@ -204,7 +204,7 @@ class UpdateRequest implements UpdateRequestInterface { $return = [ 'level' => 'info', - 'message' => (string)trans('firefly.update_newer_version_alert', ['your_version' => $current, 'new_version' => $latest]), + 'message' => (string) trans('firefly.update_newer_version_alert', ['your_version' => $current, 'new_version' => $latest]), ]; app('log')->debug('User is running a newer version', $return); @@ -215,7 +215,7 @@ class UpdateRequest implements UpdateRequestInterface { $return = [ 'level' => 'info', - 'message' => (string)trans('firefly.update_current_version_alert', ['version' => $current]), + 'message' => (string) trans('firefly.update_current_version_alert', ['version' => $current]), ]; app('log')->debug('User is the current version.', $return); @@ -225,12 +225,12 @@ class UpdateRequest implements UpdateRequestInterface private function releasedNewAlpha(string $current, string $latest, Carbon $date): array { app('log')->debug('New release is also a alpha!'); - $message = (string)trans( + $message = (string) trans( 'firefly.update_new_version_alert', [ 'your_version' => $current, 'new_version' => $latest, - 'date' => $date->isoFormat((string)trans('config.month_and_day_js')), + 'date' => $date->isoFormat((string) trans('config.month_and_day_js')), ] ); @@ -243,12 +243,12 @@ class UpdateRequest implements UpdateRequestInterface private function releasedNewBeta(string $current, string $latest, Carbon $date): array { app('log')->debug('New release is also a beta!'); - $message = (string)trans( + $message = (string) trans( 'firefly.update_new_version_alert', [ 'your_version' => $current, 'new_version' => $latest, - 'date' => $date->isoFormat((string)trans('config.month_and_day_js')), + 'date' => $date->isoFormat((string) trans('config.month_and_day_js')), ] ); @@ -261,12 +261,12 @@ class UpdateRequest implements UpdateRequestInterface private function releasedNewVersion(string $current, string $latest, Carbon $date): array { app('log')->debug('New release is old enough.'); - $message = (string)trans( + $message = (string) trans( 'firefly.update_new_version_alert', [ 'your_version' => $current, 'new_version' => $latest, - 'date' => $date->isoFormat((string)trans('config.month_and_day_js')), + 'date' => $date->isoFormat((string) trans('config.month_and_day_js')), ] ); app('log')->debug('New release is here!', [$message]); diff --git a/app/Services/Internal/Destroy/AccountDestroyService.php b/app/Services/Internal/Destroy/AccountDestroyService.php index 71f3965404..f07c1f3e4f 100644 --- a/app/Services/Internal/Destroy/AccountDestroyService.php +++ b/app/Services/Internal/Destroy/AccountDestroyService.php @@ -119,7 +119,7 @@ class AccountDestroyService /** @var \stdClass $row */ foreach ($collection as $row) { - if ((int)$row->the_count > 1) { + if ((int) $row->the_count > 1) { $journalId = $row->transaction_journal_id; $journal = $user->transactionJournals()->find($journalId); if (null !== $journal) { @@ -168,7 +168,7 @@ class AccountDestroyService /** @var RecurrenceDestroyService $destroyService */ $destroyService = app(RecurrenceDestroyService::class); foreach ($recurrences as $recurrenceId) { - $destroyService->destroyById((int)$recurrenceId); + $destroyService->destroyById((int) $recurrenceId); } } } diff --git a/app/Services/Internal/Support/AccountServiceTrait.php b/app/Services/Internal/Support/AccountServiceTrait.php index b0dc70c13d..868ffd8b27 100644 --- a/app/Services/Internal/Support/AccountServiceTrait.php +++ b/app/Services/Internal/Support/AccountServiceTrait.php @@ -144,7 +144,7 @@ trait AccountServiceTrait $data[$field] = $data[$field]->toAtomString(); } - $factory->crud($account, $field, (string)$data[$field]); + $factory->crud($account, $field, (string) $data[$field]); } } } @@ -174,7 +174,7 @@ trait AccountServiceTrait */ public function validOBData(array $data): bool { - $data['opening_balance'] = (string)($data['opening_balance'] ?? ''); + $data['opening_balance'] = (string) ($data['opening_balance'] ?? ''); if ('' !== $data['opening_balance'] && 0 === bccomp($data['opening_balance'], '0')) { $data['opening_balance'] = ''; } @@ -201,7 +201,7 @@ trait AccountServiceTrait if (is_array($language)) { $language = 'en_US'; } - $language = (string)$language; + $language = (string) $language; $sourceId = null; $sourceName = null; $destId = null; @@ -434,7 +434,7 @@ trait AccountServiceTrait if (is_array($language)) { $language = 'en_US'; } - $language = (string)$language; + $language = (string) $language; // set source and/or destination based on whether the amount is positive or negative. // first, assume the amount is positive and go from there: @@ -623,7 +623,7 @@ trait AccountServiceTrait if (is_array($language)) { $language = 'en_US'; } - $language = (string)$language; + $language = (string) $language; $sourceId = null; $sourceName = null; $destId = null; diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php index 25f78a3518..468b3f4299 100644 --- a/app/Services/Internal/Support/CreditRecalculateService.php +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Services\Internal\Support; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\AccountMetaFactory; use FireflyIII\Models\Account; @@ -80,8 +81,8 @@ class CreditRecalculateService try { $this->findByJournal($journal); } catch (FireflyException $e) { - app('log')->error($e->getTraceAsString()); - app('log')->error(sprintf('Could not find work account for transaction group #%d.', $this->group->id)); + Log::error($e->getTraceAsString()); + Log::error(sprintf('Could not find work account for transaction group #%d.', $this->group->id)); } } } @@ -158,13 +159,13 @@ class CreditRecalculateService private function processWorkAccount(Account $account): void { - app('log')->debug(sprintf('Now processing account #%d ("%s"). All amounts with 2 decimals!', $account->id, $account->name)); + Log::debug(sprintf('Now processing account #%d ("%s"). All amounts with 2 decimals!', $account->id, $account->name)); // get opening balance (if present) $this->repository->setUser($account->user); $direction = (string) $this->repository->getMetaValue($account, 'liability_direction'); $openingBalance = $this->repository->getOpeningBalance($account); if (null !== $openingBalance) { - app('log')->debug(sprintf('Found opening balance transaction journal #%d', $openingBalance->id)); + // Log::debug(sprintf('Found opening balance transaction journal #%d', $openingBalance->id)); // if account direction is "debit" ("I owe this amount") the opening balance must always be AWAY from the account: if ('debit' === $direction) { $this->validateOpeningBalance($account, $openingBalance); @@ -172,7 +173,7 @@ class CreditRecalculateService } $startOfDebt = $this->repository->getOpeningBalanceAmount($account) ?? '0'; $leftOfDebt = app('steam')->positive($startOfDebt); - app('log')->debug(sprintf('Start of debt is "%s", so initial left of debt is "%s"', app('steam')->bcround($startOfDebt, 2), app('steam')->bcround($leftOfDebt, 2))); + // Log::debug(sprintf('Start of debt is "%s", so initial left of debt is "%s"', app('steam')->bcround($startOfDebt, 2), app('steam')->bcround($leftOfDebt, 2))); /** @var AccountMetaFactory $factory */ $factory = app(AccountMetaFactory::class); @@ -180,7 +181,7 @@ class CreditRecalculateService // amount is positive or negative, doesn't matter. $factory->crud($account, 'start_of_debt', $startOfDebt); - app('log')->debug(sprintf('Debt direction is "%s"', $direction)); + // Log::debug(sprintf('Debt direction is "%s"', $direction)); // now loop all transactions (except opening balance and credit thing) $transactions = $account->transactions() @@ -189,15 +190,15 @@ class CreditRecalculateService ->get(['transactions.*']) ; $total = $transactions->count(); - app('log')->debug(sprintf('Found %d transaction(s) to process.', $total)); + // Log::debug(sprintf('Found %d transaction(s) to process.', $total)); /** @var Transaction $transaction */ foreach ($transactions as $index => $transaction) { - app('log')->debug(sprintf('[%d/%d] Processing transaction.', $index + 1, $total)); + // Log::debug(sprintf('[%d/%d] Processing transaction.', $index + 1, $total)); $leftOfDebt = $this->processTransaction($account, $direction, $transaction, $leftOfDebt); } $factory->crud($account, 'current_debt', $leftOfDebt); - app('log')->debug(sprintf('Done processing account #%d ("%s")', $account->id, $account->name)); + Log::debug(sprintf('Done processing account #%d ("%s")', $account->id, $account->name)); } /** @@ -211,25 +212,25 @@ class CreditRecalculateService /** @var Transaction $dest */ $dest = $openingBalance->transactions()->where('amount', '>', 0)->first(); if ($source->account_id !== $account->id) { - app('log')->info(sprintf('Liability #%d has a reversed opening balance. Will fix this now.', $account->id)); - app('log')->debug(sprintf('Source amount "%s" is now "%s"', $source->amount, app('steam')->positive($source->amount))); - app('log')->debug(sprintf('Destination amount "%s" is now "%s"', $dest->amount, app('steam')->negative($dest->amount))); + Log::info(sprintf('Liability #%d has a reversed opening balance. Will fix this now.', $account->id)); + Log::debug(sprintf('Source amount "%s" is now "%s"', $source->amount, app('steam')->positive($source->amount))); + Log::debug(sprintf('Destination amount "%s" is now "%s"', $dest->amount, app('steam')->negative($dest->amount))); $source->amount = app('steam')->positive($source->amount); $dest->amount = app('steam')->negative($source->amount); if (null !== $source->foreign_amount && '' !== $source->foreign_amount) { $source->foreign_amount = app('steam')->positive($source->foreign_amount); - app('log')->debug(sprintf('Source foreign amount "%s" is now "%s"', $source->foreign_amount, app('steam')->positive($source->foreign_amount))); + Log::debug(sprintf('Source foreign amount "%s" is now "%s"', $source->foreign_amount, app('steam')->positive($source->foreign_amount))); } if (null !== $dest->foreign_amount && '' !== $dest->foreign_amount) { $dest->foreign_amount = app('steam')->negative($dest->foreign_amount); - app('log')->debug(sprintf('Destination amount "%s" is now "%s"', $dest->foreign_amount, app('steam')->negative($dest->foreign_amount))); + Log::debug(sprintf('Destination amount "%s" is now "%s"', $dest->foreign_amount, app('steam')->negative($dest->foreign_amount))); } $source->save(); $dest->save(); return; } - app('log')->debug('Opening balance is valid'); + Log::debug('Opening balance is valid'); } /** @@ -245,7 +246,7 @@ class CreditRecalculateService // here be null pointers. if (null === $journal) { - app('log')->warning(sprintf('Transaction #%d has no journal.', $transaction->id)); + Log::warning(sprintf('Transaction #%d has no journal.', $transaction->id)); return $leftOfDebt; } @@ -254,19 +255,19 @@ class CreditRecalculateService $foreignCurrency = $transaction->foreignCurrency; $accountCurrency = $this->repository->getAccountCurrency($account); $type = $journal->transactionType->type; - app('log')->debug(sprintf('Left of debt is: %s', app('steam')->bcround($leftOfDebt, 2))); + // Log::debug(sprintf('Left of debt is: %s', app('steam')->bcround($leftOfDebt, 2))); if ('' === $direction) { - app('log')->warning('Direction is empty, so do nothing.'); + // Log::warning('Direction is empty, so do nothing.'); return $leftOfDebt; } - if (TransactionType::LIABILITY_CREDIT === $type || TransactionType::OPENING_BALANCE === $type) { - app('log')->warning(sprintf('Transaction type is "%s", so do nothing.', $type)); + if (TransactionTypeEnum::LIABILITY_CREDIT->value === $type || TransactionTypeEnum::OPENING_BALANCE->value === $type) { + Log::warning(sprintf('Transaction type is "%s", so do nothing.', $type)); return $leftOfDebt; } - Log::debug(sprintf('Liability direction is "%s"', $direction)); + // Log::debug(sprintf('Liability direction is "%s"', $direction)); // amount to use depends on the currency: $usedAmount = $this->getAmountToUse($transaction, $accountCurrency, $foreignCurrency); @@ -276,91 +277,80 @@ class CreditRecalculateService if ($isSameAccount && $isCredit && $this->isWithdrawalIn($usedAmount, $type)) { // case 1 $usedAmount = app('steam')->positive($usedAmount); - $result = bcadd($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcadd($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isWithdrawalOut($usedAmount, $type)) { // case 2 $usedAmount = app('steam')->positive($usedAmount); - $result = bcsub($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcsub($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isDepositOut($usedAmount, $type)) { // case 3 $usedAmount = app('steam')->positive($usedAmount); - $result = bcsub($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcsub($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isDepositIn($usedAmount, $type)) { // case 4 $usedAmount = app('steam')->positive($usedAmount); - $result = bcadd($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcadd($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isTransferIn($usedAmount, $type)) { // case 5 $usedAmount = app('steam')->positive($usedAmount); - $result = bcadd($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcadd($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isWithdrawalIn($usedAmount, $type)) { // case 6 $usedAmount = app('steam')->positive($usedAmount); - $result = bcsub($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcsub($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isDepositOut($usedAmount, $type)) { // case 7 $usedAmount = app('steam')->positive($usedAmount); - $result = bcadd($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcadd($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isWithdrawalOut($usedAmount, $type)) { // case 8 $usedAmount = app('steam')->positive($usedAmount); - $result = bcadd($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcadd($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isTransferIn($usedAmount, $type)) { // case 9 $usedAmount = app('steam')->positive($usedAmount); - $result = bcsub($leftOfDebt, $usedAmount); - // 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS. - app('log')->debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcsub($leftOfDebt, $usedAmount); + // 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS. + // Log::debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isTransferOut($usedAmount, $type)) { // case 10 $usedAmount = app('steam')->positive($usedAmount); - $result = bcadd($leftOfDebt, $usedAmount); - // 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE. - app('log')->debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcadd($leftOfDebt, $usedAmount); + // 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE. + // Log::debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } // in any other case, remove amount from left of debt. if (in_array($type, [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER], true)) { $usedAmount = app('steam')->negative($usedAmount); - $result = bcadd($leftOfDebt, $usedAmount); - app('log')->debug(sprintf('Case X (all other cases): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); - return $result; + return bcadd($leftOfDebt, $usedAmount); + // Log::debug(sprintf('Case X (all other cases): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } - app('log')->warning(sprintf('[-1] Catch-all, should not happen. Left of debt = %s', app('steam')->bcround($leftOfDebt, 2))); + Log::warning(sprintf('[-1] Catch-all, should not happen. Left of debt = %s', app('steam')->bcround($leftOfDebt, 2))); return $leftOfDebt; } @@ -368,10 +358,10 @@ class CreditRecalculateService private function getAmountToUse(Transaction $transaction, TransactionCurrency $accountCurrency, ?TransactionCurrency $foreignCurrency): string { $usedAmount = $transaction->amount; - app('log')->debug(sprintf('Amount of transaction is %s', app('steam')->bcround($usedAmount, 2))); + // Log::debug(sprintf('Amount of transaction is %s', app('steam')->bcround($usedAmount, 2))); if (null !== $foreignCurrency && $foreignCurrency->id === $accountCurrency->id) { $usedAmount = $transaction->foreign_amount; - app('log')->debug(sprintf('Overruled by foreign amount. Amount of transaction is now %s', app('steam')->bcround($usedAmount, 2))); + // Log::debug(sprintf('Overruled by foreign amount. Amount of transaction is now %s', app('steam')->bcround($usedAmount, 2))); } return $usedAmount; diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php index f9f113c25f..90091b8477 100644 --- a/app/Services/Internal/Support/JournalServiceTrait.php +++ b/app/Services/Internal/Support/JournalServiceTrait.php @@ -75,7 +75,7 @@ trait JournalServiceTrait // if $result (find by name) is NULL, but IBAN is set, any result of the search by NAME can't overrule // this account. In such a case, the name search must be retried with a new name. - if (null !== $nameResult && null === $numberResult && null === $ibanResult && '' !== (string)$data['iban'] && '' !== (string)$nameResult->iban) { + if (null !== $nameResult && null === $numberResult && null === $ibanResult && '' !== (string) $data['iban'] && '' !== (string) $nameResult->iban) { $data['name'] = sprintf('%s (%s)', $data['name'], $data['iban']); app('log')->debug(sprintf('Search again using the new name, "%s".', $data['name'])); $result = $this->findAccountByName(null, $data, $expectedTypes[$transactionType]); @@ -115,7 +115,7 @@ trait JournalServiceTrait { // first attempt, find by ID. if (null !== $data['id']) { - $search = $this->accountRepository->find((int)$data['id']); + $search = $this->accountRepository->find((int) $data['id']); if (null !== $search && in_array($search->accountType->type, $types, true)) { app('log')->debug( sprintf('Found "account_id" object: #%d, "%s" of type %s (1)', $search->id, $search->name, $search->accountType->type) @@ -176,10 +176,10 @@ trait JournalServiceTrait return null; } // find by preferred type. - $source = $this->accountRepository->findByAccountNumber((string)$data['number'], [$types[0]]); + $source = $this->accountRepository->findByAccountNumber((string) $data['number'], [$types[0]]); // or any expected type. - $source ??= $this->accountRepository->findByAccountNumber((string)$data['number'], $types); + $source ??= $this->accountRepository->findByAccountNumber((string) $data['number'], $types); if (null !== $source) { app('log')->debug(sprintf('Found account: #%d, %s', $source->id, $source->name)); @@ -261,17 +261,17 @@ trait JournalServiceTrait throw new FireflyException(sprintf('TransactionFactory: Cannot create asset account with these values: %s', json_encode($data))); } // fix name of account if only IBAN is given: - if ('' === (string)$data['name'] && '' !== (string)$data['iban']) { + if ('' === (string) $data['name'] && '' !== (string) $data['iban']) { app('log')->debug(sprintf('Account name is now IBAN ("%s")', $data['iban'])); $data['name'] = $data['iban']; } // fix name of account if only number is given: - if ('' === (string)$data['name'] && '' !== (string)$data['number']) { + if ('' === (string) $data['name'] && '' !== (string) $data['number']) { app('log')->debug(sprintf('Account name is now account number ("%s")', $data['number'])); $data['name'] = $data['number']; } // if name is still NULL, return NULL. - if ('' === (string)$data['name']) { + if ('' === (string) $data['name']) { app('log')->debug('Account name is still NULL, return NULL.'); return null; @@ -310,7 +310,7 @@ trait JournalServiceTrait private function getCashAccount(?Account $account, array $data, array $types): ?Account { // return cash account. - if (null === $account && '' === (string)$data['name'] + if (null === $account && '' === (string) $data['name'] && in_array(AccountType::CASH, $types, true)) { $account = $this->accountRepository->getCashAccount(); } @@ -390,7 +390,7 @@ trait JournalServiceTrait protected function storeNotes(TransactionJournal $journal, ?string $notes): void { - $notes = (string)$notes; + $notes = (string) $notes; $note = $journal->notes()->first(); if ('' !== $notes) { if (null === $note) { @@ -422,7 +422,7 @@ trait JournalServiceTrait } app('log')->debug('Start of loop.'); foreach ($tags as $string) { - $string = (string)$string; + $string = (string) $string; app('log')->debug(sprintf('Now at tag "%s"', $string)); if ('' !== $string) { $tag = $this->tagFactory->findOrCreate($string); diff --git a/app/Services/Internal/Support/RecurringTransactionTrait.php b/app/Services/Internal/Support/RecurringTransactionTrait.php index 438563c98b..e9ba8437f0 100644 --- a/app/Services/Internal/Support/RecurringTransactionTrait.php +++ b/app/Services/Internal/Support/RecurringTransactionTrait.php @@ -127,7 +127,7 @@ trait RecurringTransactionTrait if (!$validator->validateDestination(['id' => $destination->id])) { throw new FireflyException(sprintf('Destination invalid: %s', $validator->destError)); } - if (array_key_exists('foreign_amount', $array) && '' === (string)$array['foreign_amount']) { + if (array_key_exists('foreign_amount', $array) && '' === (string) $array['foreign_amount']) { unset($array['foreign_amount']); } // TODO typeOverrule. The account validator may have a different opinion on the type of the transaction. @@ -139,25 +139,25 @@ trait RecurringTransactionTrait 'source_id' => $source->id, 'destination_id' => $destination->id, 'amount' => $array['amount'], - 'foreign_amount' => array_key_exists('foreign_amount', $array) ? (string)$array['foreign_amount'] : null, + 'foreign_amount' => array_key_exists('foreign_amount', $array) ? (string) $array['foreign_amount'] : null, 'description' => $array['description'], ] ); $transaction->save(); if (array_key_exists('budget_id', $array)) { - $this->setBudget($transaction, (int)$array['budget_id']); + $this->setBudget($transaction, (int) $array['budget_id']); } if (array_key_exists('bill_id', $array)) { - $this->setBill($transaction, (int)$array['bill_id']); + $this->setBill($transaction, (int) $array['bill_id']); } if (array_key_exists('category_id', $array)) { - $this->setCategory($transaction, (int)$array['category_id']); + $this->setCategory($transaction, (int) $array['category_id']); } // same for piggy bank if (array_key_exists('piggy_bank_id', $array)) { - $this->updatePiggyBank($transaction, (int)$array['piggy_bank_id']); + $this->updatePiggyBank($transaction, (int) $array['piggy_bank_id']); } if (array_key_exists('tags', $array) && is_array($array['tags'])) { @@ -169,8 +169,8 @@ trait RecurringTransactionTrait protected function findAccount(array $expectedTypes, ?int $accountId, ?string $accountName): Account { $result = null; - $accountId = (int)$accountId; - $accountName = (string)$accountName; + $accountId = (int) $accountId; + $accountName = (string) $accountName; /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); @@ -283,9 +283,9 @@ trait RecurringTransactionTrait protected function updatePiggyBank(RecurrenceTransaction $transaction, int $piggyId): void { /** @var PiggyBankFactory $factory */ - $factory = app(PiggyBankFactory::class); - $factory->setUser($transaction->recurrence->user); - $piggyBank = $factory->find($piggyId, null); + $factory = app(PiggyBankFactory::class); + $factory->user = $transaction->recurrence->user; + $piggyBank = $factory->find($piggyId, null); if (null !== $piggyBank) { /** @var null|RecurrenceMeta $entry */ $entry = $transaction->recurrenceTransactionMeta()->where('name', 'piggy_bank_id')->first(); diff --git a/app/Services/Internal/Update/AccountUpdateService.php b/app/Services/Internal/Update/AccountUpdateService.php index 3026d8acd7..44b0a89d51 100644 --- a/app/Services/Internal/Update/AccountUpdateService.php +++ b/app/Services/Internal/Update/AccountUpdateService.php @@ -75,7 +75,7 @@ class AccountUpdateService // find currency, or use default currency instead. if (array_key_exists('currency_id', $data) || array_key_exists('currency_code', $data)) { - $currency = $this->getCurrency((int)($data['currency_id'] ?? null), (string)($data['currency_code'] ?? null)); + $currency = $this->getCurrency((int) ($data['currency_id'] ?? null), (string) ($data['currency_code'] ?? null)); unset($data['currency_code'], $data['currency_id']); $data['currency_id'] = $currency->id; } @@ -94,7 +94,7 @@ class AccountUpdateService // update note: if (array_key_exists('notes', $data) && null !== $data['notes']) { - $this->updateNote($account, (string)$data['notes']); + $this->updateNote($account, (string) $data['notes']); } // update preferences if inactive: @@ -120,7 +120,7 @@ class AccountUpdateService $account->active = $data['active']; } if (array_key_exists('iban', $data)) { - $account->iban = app('steam')->filterSpaces((string)$data['iban']); + $account->iban = app('steam')->filterSpaces((string) $data['iban']); } // set liability, but account must already be a liability. @@ -132,7 +132,7 @@ class AccountUpdateService // set liability, alternative method used in v1 layout: if ($this->isLiability($account) && array_key_exists('account_type_id', $data)) { - $type = AccountType::find((int)$data['account_type_id']); + $type = AccountType::find((int) $data['account_type_id']); if (null !== $type && in_array($type->type, config('firefly.valid_liabilities'), true)) { $account->account_type_id = $type->id; @@ -305,9 +305,9 @@ class AccountUpdateService $removeAccountId = $account->id; $new = []; foreach ($array as $value) { - if ((int)$value !== $removeAccountId) { + if ((int) $value !== $removeAccountId) { app('log')->debug(sprintf('Will include: %d', $value)); - $new[] = (int)$value; + $new[] = (int) $value; } } app('log')->debug('Final new array is', $new); diff --git a/app/Services/Internal/Update/BillUpdateService.php b/app/Services/Internal/Update/BillUpdateService.php index 25a9054dd7..ddd23ddcd1 100644 --- a/app/Services/Internal/Update/BillUpdateService.php +++ b/app/Services/Internal/Update/BillUpdateService.php @@ -54,7 +54,7 @@ class BillUpdateService if (array_key_exists('currency_id', $data) || array_key_exists('currency_code', $data)) { $factory = app(TransactionCurrencyFactory::class); - $currency = $factory->find((int)($data['currency_id'] ?? null), $data['currency_code'] ?? null) ?? + $currency = $factory->find((int) ($data['currency_id'] ?? null), $data['currency_code'] ?? null) ?? app('amount')->getDefaultCurrencyByUserGroup($bill->user->userGroup); // enable the currency if it isn't. @@ -76,14 +76,14 @@ class BillUpdateService ]; // update note: if (array_key_exists('notes', $data)) { - $this->updateNote($bill, (string)$data['notes']); + $this->updateNote($bill, (string) $data['notes']); } // update order. if (array_key_exists('order', $data)) { // update the order of the piggy bank: $oldOrder = $bill->order; - $newOrder = (int)($data['order'] ?? $oldOrder); + $newOrder = (int) ($data['order'] ?? $oldOrder); if ($oldOrder !== $newOrder) { $this->updateOrder($bill, $oldOrder, $newOrder); } @@ -113,7 +113,7 @@ class BillUpdateService } if (array_key_exists('object_group_id', $data)) { // try also with ID: - $objectGroupId = (int)($data['object_group_id'] ?? 0); + $objectGroupId = (int) ($data['object_group_id'] ?? 0); if (0 !== $objectGroupId) { $objectGroup = $this->findObjectGroupById($objectGroupId); if (null !== $objectGroup) { @@ -135,21 +135,21 @@ class BillUpdateService */ private function updateBillProperties(Bill $bill, array $data): Bill { - if (array_key_exists('name', $data) && '' !== (string)$data['name']) { + if (array_key_exists('name', $data) && '' !== (string) $data['name']) { $bill->name = $data['name']; } - if (array_key_exists('amount_min', $data) && '' !== (string)$data['amount_min']) { + if (array_key_exists('amount_min', $data) && '' !== (string) $data['amount_min']) { $bill->amount_min = $data['amount_min']; } - if (array_key_exists('amount_max', $data) && '' !== (string)$data['amount_max']) { + if (array_key_exists('amount_max', $data) && '' !== (string) $data['amount_max']) { $bill->amount_max = $data['amount_max']; } - if (array_key_exists('date', $data) && '' !== (string)$data['date']) { + if (array_key_exists('date', $data) && '' !== (string) $data['date']) { $bill->date = $data['date']; $bill->date_tz = $data['date']->format('e'); } - if (array_key_exists('repeat_freq', $data) && '' !== (string)$data['repeat_freq']) { + if (array_key_exists('repeat_freq', $data) && '' !== (string) $data['repeat_freq']) { $bill->repeat_freq = $data['repeat_freq']; } if (array_key_exists('skip', $data)) { diff --git a/app/Services/Internal/Update/CurrencyUpdateService.php b/app/Services/Internal/Update/CurrencyUpdateService.php index f116bebf97..1ef57f722c 100644 --- a/app/Services/Internal/Update/CurrencyUpdateService.php +++ b/app/Services/Internal/Update/CurrencyUpdateService.php @@ -33,15 +33,15 @@ class CurrencyUpdateService { public function update(TransactionCurrency $currency, array $data): TransactionCurrency { - if (array_key_exists('code', $data) && '' !== (string)$data['code']) { + if (array_key_exists('code', $data) && '' !== (string) $data['code']) { $currency->code = e($data['code']); } - if (array_key_exists('symbol', $data) && '' !== (string)$data['symbol']) { + if (array_key_exists('symbol', $data) && '' !== (string) $data['symbol']) { $currency->symbol = e($data['symbol']); } - if (array_key_exists('name', $data) && '' !== (string)$data['name']) { + if (array_key_exists('name', $data) && '' !== (string) $data['name']) { $currency->name = e($data['name']); } diff --git a/app/Services/Internal/Update/GroupUpdateService.php b/app/Services/Internal/Update/GroupUpdateService.php index 49ec44aacd..2d30599e54 100644 --- a/app/Services/Internal/Update/GroupUpdateService.php +++ b/app/Services/Internal/Update/GroupUpdateService.php @@ -108,7 +108,7 @@ class GroupUpdateService /** @var string $deletedId */ foreach ($result as $deletedId) { /** @var TransactionJournal $journal */ - $journal = $transactionGroup->transactionJournals()->find((int)$deletedId); + $journal = $transactionGroup->transactionJournals()->find((int) $deletedId); /** @var JournalDestroyService $service */ $service = app(JournalDestroyService::class); @@ -163,7 +163,7 @@ class GroupUpdateService */ foreach ($transactions as $index => $transaction) { app('log')->debug(sprintf('Now at #%d of %d', $index + 1, count($transactions)), $transaction); - $journalId = (int)($transaction['transaction_journal_id'] ?? 0); + $journalId = (int) ($transaction['transaction_journal_id'] ?? 0); /** @var null|TransactionJournal $journal */ $journal = $transactionGroup->transactionJournals()->find($journalId); diff --git a/app/Services/Internal/Update/RecurrenceUpdateService.php b/app/Services/Internal/Update/RecurrenceUpdateService.php index 4686b350c5..90575b664d 100644 --- a/app/Services/Internal/Update/RecurrenceUpdateService.php +++ b/app/Services/Internal/Update/RecurrenceUpdateService.php @@ -74,7 +74,7 @@ class RecurrenceUpdateService $recurrence->repetitions = 0; } if (array_key_exists('nr_of_repetitions', $info)) { - if (0 !== (int)$info['nr_of_repetitions']) { + if (0 !== (int) $info['nr_of_repetitions']) { $recurrence->repeat_until = null; } $recurrence->repetitions = $info['nr_of_repetitions']; @@ -212,7 +212,7 @@ class RecurrenceUpdateService // First, make sure to loop all existing transactions and match them to a counterpart in the submitted transactions array. foreach ($originalTransactions as $i => $originalTransaction) { foreach ($transactions as $ii => $submittedTransaction) { - if (array_key_exists('id', $submittedTransaction) && (int)$originalTransaction['id'] === (int)$submittedTransaction['id']) { + if (array_key_exists('id', $submittedTransaction) && (int) $originalTransaction['id'] === (int) $submittedTransaction['id']) { app('log')->debug(sprintf('Match original transaction #%d with an entry in the submitted array.', $originalTransaction['id'])); $combinations[] = [ 'original' => $originalTransaction, @@ -241,7 +241,7 @@ class RecurrenceUpdateService // anything left in the original transactions array can be deleted. foreach ($originalTransactions as $original) { app('log')->debug(sprintf('Original transaction #%d is unmatched, delete it!', $original['id'])); - $this->deleteTransaction($recurrence, (int)$original['id']); + $this->deleteTransaction($recurrence, (int) $original['id']); } // anything left is new. $this->createTransactions($recurrence, $transactions); @@ -268,7 +268,7 @@ class RecurrenceUpdateService $foreignCurrency = null; if (array_key_exists('currency_id', $submitted) || array_key_exists('currency_code', $submitted)) { $currency = $currencyFactory->find( - array_key_exists('currency_id', $submitted) ? (int)$submitted['currency_id'] : null, + array_key_exists('currency_id', $submitted) ? (int) $submitted['currency_id'] : null, array_key_exists('currency_code', $submitted) ? $submitted['currency_code'] : null ); } @@ -280,7 +280,7 @@ class RecurrenceUpdateService } if (array_key_exists('foreign_currency_id', $submitted) || array_key_exists('foreign_currency_code', $submitted)) { $foreignCurrency = $currencyFactory->find( - array_key_exists('foreign_currency_id', $submitted) ? (int)$submitted['foreign_currency_id'] : null, + array_key_exists('foreign_currency_id', $submitted) ? (int) $submitted['foreign_currency_id'] : null, array_key_exists('foreign_currency_code', $submitted) ? $submitted['foreign_currency_code'] : null ); } @@ -309,29 +309,29 @@ class RecurrenceUpdateService } // update meta data if (array_key_exists('budget_id', $submitted)) { - $this->setBudget($transaction, (int)$submitted['budget_id']); + $this->setBudget($transaction, (int) $submitted['budget_id']); } if (array_key_exists('bill_id', $submitted)) { - $this->setBill($transaction, (int)$submitted['bill_id']); + $this->setBill($transaction, (int) $submitted['bill_id']); } // reset category if name is set but empty: // can be removed when v1 is retired. - if (array_key_exists('category_name', $submitted) && '' === (string)$submitted['category_name']) { + if (array_key_exists('category_name', $submitted) && '' === (string) $submitted['category_name']) { app('log')->debug('Category name is submitted but is empty. Set category to be empty.'); $submitted['category_name'] = null; $submitted['category_id'] = 0; } if (array_key_exists('category_id', $submitted)) { - app('log')->debug(sprintf('Category ID is submitted, set category to be %d.', (int)$submitted['category_id'])); - $this->setCategory($transaction, (int)$submitted['category_id']); + app('log')->debug(sprintf('Category ID is submitted, set category to be %d.', (int) $submitted['category_id'])); + $this->setCategory($transaction, (int) $submitted['category_id']); } if (array_key_exists('tags', $submitted) && is_array($submitted['tags'])) { $this->updateTags($transaction, $submitted['tags']); } if (array_key_exists('piggy_bank_id', $submitted)) { - $this->updatePiggyBank($transaction, (int)$submitted['piggy_bank_id']); + $this->updatePiggyBank($transaction, (int) $submitted['piggy_bank_id']); } } diff --git a/app/Services/Webhook/StandardWebhookSender.php b/app/Services/Webhook/StandardWebhookSender.php index 76533dbbd4..720d9bea8e 100644 --- a/app/Services/Webhook/StandardWebhookSender.php +++ b/app/Services/Webhook/StandardWebhookSender.php @@ -128,7 +128,7 @@ class StandardWebhookSender implements WebhookSenderInterface if (method_exists($e, 'hasResponse') && method_exists($e, 'getResponse')) { $attempt->status_code = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0; app('log')->error(sprintf('The status code of the error response is: %d', $attempt->status_code)); - $body = (string)($e->hasResponse() ? $e->getResponse()->getBody() : ''); + $body = (string) ($e->hasResponse() ? $e->getResponse()->getBody() : ''); app('log')->error(sprintf('The body of the error response is: %s', $body)); } $attempt->logs = $logs; diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 28c96c93d7..ac4dc9cbe0 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -24,8 +24,11 @@ declare(strict_types=1); namespace FireflyIII\Support; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\UserGroup; +use FireflyIII\Support\Facades\Preferences; use FireflyIII\User; use Illuminate\Support\Collection; @@ -45,11 +48,59 @@ class Amount return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured); } - public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string + /** + * Experimental function to see if we can quickly and quietly get the amount from a journal. + * This depends on the user's default currency and the wish to have it converted. + */ + public function getAmountFromJournal(array $journal): string { - $format = TransactionCurrency::find($currencyId); + $convertToNative = $this->convertToNative(); + $currency = $this->getDefaultCurrency(); + $field = $convertToNative && $currency->id !== $journal['currency_id'] ? 'native_amount' : 'amount'; + $amount = $journal[$field] ?? '0'; + // Log::debug(sprintf('Field is %s, amount is %s', $field, $amount)); + // fallback, the transaction has a foreign amount in $currency. + if ($convertToNative && null !== $journal['foreign_amount'] && $currency->id === (int) $journal['foreign_currency_id']) { + $amount = $journal['foreign_amount']; + // Log::debug(sprintf('Overruled, amount is now %s', $amount)); + } - return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured); + return (string) $amount; + } + + public function convertToNative(?User $user = null): bool + { + if (null === $user) { + return Preferences::get('convert_to_native', false)->data && config('cer.enabled'); + // Log::debug(sprintf('convertToNative [a]: %s', var_export($result, true))); + } + + return Preferences::getForUser($user, 'convert_to_native', false)->data && config('cer.enabled'); + // Log::debug(sprintf('convertToNative [b]: %s', var_export($result, true))); + } + + /** + * Experimental function to see if we can quickly and quietly get the amount from a journal. + * This depends on the user's default currency and the wish to have it converted. + */ + public function getAmountFromJournalObject(TransactionJournal $journal): string + { + $convertToNative = $this->convertToNative(); + $currency = $this->getDefaultCurrency(); + $field = $convertToNative && $currency->id !== $journal->transaction_currency_id ? 'native_amount' : 'amount'; + + /** @var null|Transaction $sourceTransaction */ + $sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first(); + if (null === $sourceTransaction) { + return '0'; + } + $amount = $sourceTransaction->{$field} ?? '0'; + if ((int) $sourceTransaction->foreign_currency_id === $currency->id) { + // use foreign amount instead! + $amount = (string) $sourceTransaction->foreign_amount; // hard coded to be foreign amount. + } + + return $amount; } /** @@ -70,7 +121,7 @@ class Amount $fmt->setSymbol(\NumberFormatter::CURRENCY_SYMBOL, $symbol); $fmt->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces); $fmt->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces); - $result = (string)$fmt->format((float)$rounded); // intentional float + $result = (string) $fmt->format((float) $rounded); // intentional float if (true === $coloured) { if (1 === bccomp($rounded, '0')) { @@ -86,6 +137,13 @@ class Amount return $result; } + public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string + { + $format = TransactionCurrency::find($currencyId); + + return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured); + } + public function getAllCurrencies(): Collection { return TransactionCurrency::orderBy('code', 'ASC')->get(); @@ -101,10 +159,15 @@ class Amount public function getDefaultCurrency(): TransactionCurrency { - /** @var User $user */ - $user = auth()->user(); + if (auth()->check()) { + /** @var User $user */ + $user = auth()->user(); + if (null !== $user->userGroup) { + return $this->getDefaultCurrencyByUserGroup($user->userGroup); + } + } - return $this->getDefaultCurrencyByUserGroup($user->userGroup); + return $this->getSystemCurrency(); } public function getDefaultCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency diff --git a/app/Support/Authentication/RemoteUserGuard.php b/app/Support/Authentication/RemoteUserGuard.php index e20faac9b2..61b6d4c88b 100644 --- a/app/Support/Authentication/RemoteUserGuard.php +++ b/app/Support/Authentication/RemoteUserGuard.php @@ -86,7 +86,7 @@ class RemoteUserGuard implements Guard $header = config('auth.guard_email'); if (null !== $header) { - $emailAddress = (string)(request()->server($header) ?? apache_request_headers()[$header] ?? null); + $emailAddress = (string) (request()->server($header) ?? apache_request_headers()[$header] ?? null); $preference = app('preferences')->getForUser($retrievedUser, 'remote_guard_alt_email'); if ('' !== $emailAddress && null === $preference && $emailAddress !== $userID) { diff --git a/app/Support/Authentication/RemoteUserProvider.php b/app/Support/Authentication/RemoteUserProvider.php index a218dd5bfe..548b6bab15 100644 --- a/app/Support/Authentication/RemoteUserProvider.php +++ b/app/Support/Authentication/RemoteUserProvider.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Support\Authentication; -use FireflyIII\Console\Commands\Integrity\CreateGroupMemberships; +use FireflyIII\Console\Commands\Correction\CreateGroupMemberships; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Role; use FireflyIII\User; diff --git a/app/Support/Binder/TagList.php b/app/Support/Binder/TagList.php index d87c8c69b9..3dd4835f54 100644 --- a/app/Support/Binder/TagList.php +++ b/app/Support/Binder/TagList.php @@ -68,7 +68,7 @@ class TagList implements BinderInterface return true; } - if (in_array((string)$tag->id, $list, true)) { + if (in_array((string) $tag->id, $list, true)) { Log::debug(sprintf('TagList: (id) found tag #%d ("%s") in list.', $tag->id, $tag->tag)); return true; diff --git a/app/Support/Binder/TagOrId.php b/app/Support/Binder/TagOrId.php index e742fb674d..bc511e5018 100644 --- a/app/Support/Binder/TagOrId.php +++ b/app/Support/Binder/TagOrId.php @@ -42,7 +42,7 @@ class TagOrId implements BinderInterface $result = $repository->findByTag($value); if (null === $result) { - $result = $repository->find((int)$value); + $result = $repository->find((int) $value); } if (null !== $result) { return $result; diff --git a/app/Support/Binder/UserGroupAccount.php b/app/Support/Binder/UserGroupAccount.php index c395655e87..12d7eff4a2 100644 --- a/app/Support/Binder/UserGroupAccount.php +++ b/app/Support/Binder/UserGroupAccount.php @@ -41,7 +41,7 @@ class UserGroupAccount implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $account = Account::where('id', (int)$value) + $account = Account::where('id', (int) $value) ->where('user_group_id', $user->user_group_id) ->first() ; diff --git a/app/Support/Binder/UserGroupBill.php b/app/Support/Binder/UserGroupBill.php index bd2489965e..551846d693 100644 --- a/app/Support/Binder/UserGroupBill.php +++ b/app/Support/Binder/UserGroupBill.php @@ -41,7 +41,7 @@ class UserGroupBill implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $currency = Bill::where('id', (int)$value) + $currency = Bill::where('id', (int) $value) ->where('user_group_id', $user->user_group_id) ->first() ; diff --git a/app/Support/Binder/UserGroupExchangeRate.php b/app/Support/Binder/UserGroupExchangeRate.php new file mode 100644 index 0000000000..74a65c9348 --- /dev/null +++ b/app/Support/Binder/UserGroupExchangeRate.php @@ -0,0 +1,52 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Binder; + +use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\User; +use Illuminate\Routing\Route; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Class UserGroupTransaction. + */ +class UserGroupExchangeRate implements BinderInterface +{ + public static function routeBinder(string $value, Route $route): CurrencyExchangeRate + { + if (auth()->check()) { + /** @var User $user */ + $user = auth()->user(); + $rate = CurrencyExchangeRate::where('id', (int) $value) + ->where('user_group_id', $user->user_group_id) + ->first() + ; + if (null !== $rate) { + return $rate; + } + } + + throw new NotFoundHttpException(); + } +} diff --git a/app/Support/Binder/UserGroupTransaction.php b/app/Support/Binder/UserGroupTransaction.php index fbbf5c1f43..d9131400f3 100644 --- a/app/Support/Binder/UserGroupTransaction.php +++ b/app/Support/Binder/UserGroupTransaction.php @@ -38,7 +38,7 @@ class UserGroupTransaction implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $group = TransactionGroup::where('id', (int)$value) + $group = TransactionGroup::where('id', (int) $value) ->where('user_group_id', $user->user_group_id) ->first() ; diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php index dc1619bddf..8a8ba020b7 100644 --- a/app/Support/CacheProperties.php +++ b/app/Support/CacheProperties.php @@ -81,7 +81,7 @@ class CacheProperties $content .= json_encode($property, JSON_THROW_ON_ERROR); } catch (\JsonException $e) { // @ignoreException - $content .= hash('sha256', (string)time()); + $content .= hash('sha256', (string) time()); } } $this->hash = substr(hash('sha256', $content), 0, 16); diff --git a/app/Support/Chart/Budget/FrontpageChartGenerator.php b/app/Support/Chart/Budget/FrontpageChartGenerator.php index 2e09e909a0..5c96be4488 100644 --- a/app/Support/Chart/Budget/FrontpageChartGenerator.php +++ b/app/Support/Chart/Budget/FrontpageChartGenerator.php @@ -26,11 +26,13 @@ namespace FireflyIII\Support\Chart\Budget; use Carbon\Carbon; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\User; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class FrontpageChartGenerator @@ -43,6 +45,8 @@ class FrontpageChartGenerator private Carbon $end; private string $monthAndDayFormat; private Carbon $start; + public bool $convertToNative = false; + public TransactionCurrency $default; /** * FrontpageChartGenerator constructor. @@ -62,11 +66,12 @@ class FrontpageChartGenerator */ public function generate(): array { + Log::debug('Now in generate for budget chart.'); $budgets = $this->budgetRepository->getActiveBudgets(); $data = [ - ['label' => (string)trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'], - ['label' => (string)trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'], - ['label' => (string)trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'], + ['label' => (string) trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'], + ['label' => (string) trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'], + ['label' => (string) trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'], ]; // loop al budgets: @@ -74,6 +79,7 @@ class FrontpageChartGenerator foreach ($budgets as $budget) { $data = $this->processBudget($data, $budget); } + Log::debug('DONE with generate budget chart.'); return $data; } @@ -85,15 +91,21 @@ class FrontpageChartGenerator */ private function processBudget(array $data, Budget $budget): array { + Log::debug(sprintf('Now processing budget #%d ("%s")', $budget->id, $budget->name)); // get all limits: $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); - + Log::debug(sprintf('Found %d limit(s) for budget #%d.', $limits->count(), $budget->id)); // if no limits if (0 === $limits->count()) { - return $this->noBudgetLimits($data, $budget); - } + $result = $this->noBudgetLimits($data, $budget); + Log::debug(sprintf('Now DONE processing budget #%d ("%s")', $budget->id, $budget->name)); - return $this->budgetLimits($data, $budget, $limits); + return $result; + } + $result = $this->budgetLimits($data, $budget, $limits); + Log::debug(sprintf('Now DONE processing budget #%d ("%s")', $budget->id, $budget->name)); + + return $result; } /** @@ -120,10 +132,13 @@ class FrontpageChartGenerator */ private function budgetLimits(array $data, Budget $budget, Collection $limits): array { + Log::debug('Start processing budget limits.'); + /** @var BudgetLimit $limit */ foreach ($limits as $limit) { $data = $this->processLimit($data, $budget, $limit); } + Log::debug('Done processing budget limits.'); return $data; } @@ -134,14 +149,29 @@ class FrontpageChartGenerator */ private function processLimit(array $data, Budget $budget, BudgetLimit $limit): array { - $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection([$budget]), $limit->transactionCurrency); + $useNative = $this->convertToNative && $this->default->id !== $limit->transaction_currency_id; + $currency = $limit->transactionCurrency; + if ($useNative) { + Log::debug(sprintf('Processing limit #%d with (native) %s %s', $limit->id, $this->default->code, $limit->native_amount)); + } + if (!$useNative) { + Log::debug(sprintf('Processing limit #%d with %s %s', $limit->id, $limit->transactionCurrency->code, $limit->amount)); + } + + $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection([$budget]), $currency); + Log::debug(sprintf('Spent array has %d entries.', count($spent))); /** @var array $entry */ foreach ($spent as $entry) { // only spent the entry where the entry's currency matches the budget limit's currency - if ($entry['currency_id'] === $limit->transaction_currency_id) { + // or when useNative is true. + if ($entry['currency_id'] === $limit->transaction_currency_id || $useNative) { + Log::debug(sprintf('Process spent row (%s)', $entry['currency_code'])); $data = $this->processRow($data, $budget, $limit, $entry); } + if (!($entry['currency_id'] === $limit->transaction_currency_id || $useNative)) { + Log::debug(sprintf('Skipping spent row (%s).', $entry['currency_code'])); + } } return $data; @@ -156,6 +186,7 @@ class FrontpageChartGenerator private function processRow(array $data, Budget $budget, BudgetLimit $limit, array $entry): array { $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + Log::debug(sprintf('Title is "%s"', $title)); if ($limit->start_date->startOfDay()->ne($this->start->startOfDay()) || $limit->end_date->startOfDay()->ne($this->end->startOfDay())) { $title = sprintf( '%s (%s) (%s - %s)', @@ -165,11 +196,27 @@ class FrontpageChartGenerator $limit->end_date->isoFormat($this->monthAndDayFormat) ); } - $sumSpent = bcmul($entry['sum'], '-1'); // spent + $useNative = $this->convertToNative && $this->default->id !== $limit->transaction_currency_id; + $amount = $limit->amount; + Log::debug(sprintf('Amount is "%s".', $amount)); + if ($useNative && $limit->transaction_currency_id !== $this->default->id) { + $amount = $limit->native_amount; + Log::debug(sprintf('Amount is now "%s".', $amount)); + } - $data[0]['entries'][$title] = 1 === bccomp($sumSpent, $limit->amount) ? $limit->amount : $sumSpent; // spent - $data[1]['entries'][$title] = 1 === bccomp($limit->amount, $sumSpent) ? bcadd($entry['sum'], $limit->amount) : '0'; // left to spent - $data[2]['entries'][$title] = 1 === bccomp($limit->amount, $sumSpent) ? '0' : bcmul(bcadd($entry['sum'], $limit->amount), '-1'); // overspent + + $sumSpent = bcmul($entry['sum'], '-1'); // spent + $data[0]['entries'][$title] ??= '0'; + $data[1]['entries'][$title] ??= '0'; + $data[2]['entries'][$title] ??= '0'; + + $data[0]['entries'][$title] = bcadd($data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent + $data[1]['entries'][$title] = bcadd($data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd($entry['sum'], $amount) : '0'); // left to spent + $data[2]['entries'][$title] = bcadd($data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd($entry['sum'], $amount), '-1')); // overspent + + Log::debug(sprintf('Amount [spent] is now %s.', $data[0]['entries'][$title])); + Log::debug(sprintf('Amount [left] is now %s.', $data[1]['entries'][$title])); + Log::debug(sprintf('Amount [overspent] is now %s.', $data[2]['entries'][$title])); return $data; } @@ -194,6 +241,6 @@ class FrontpageChartGenerator $this->opsRepository->setUser($user); $locale = app('steam')->getLocale(); - $this->monthAndDayFormat = (string)trans('config.month_and_day_js', [], $locale); + $this->monthAndDayFormat = (string) trans('config.month_and_day_js', [], $locale); } } diff --git a/app/Support/Chart/Category/FrontpageChartGenerator.php b/app/Support/Chart/Category/FrontpageChartGenerator.php index b570dd7552..d9d8da6884 100644 --- a/app/Support/Chart/Category/FrontpageChartGenerator.php +++ b/app/Support/Chart/Category/FrontpageChartGenerator.php @@ -25,7 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Support\Chart\Category; use Carbon\Carbon; -use FireflyIII\Models\AccountType; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Models\Category; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; @@ -33,6 +33,7 @@ use FireflyIII\Repositories\Category\NoCategoryRepositoryInterface; use FireflyIII\Repositories\Category\OperationsRepositoryInterface; use FireflyIII\Support\Http\Controllers\AugumentData; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class FrontpageChartGenerator @@ -65,10 +66,9 @@ class FrontpageChartGenerator public function generate(): array { + Log::debug('Now in generate()'); $categories = $this->repository->getCategories(); - $accounts = $this->accountRepos->getAccountsByType( - [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::ASSET, AccountType::DEFAULT] - ); + $accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); // get expenses + income per category: $collection = []; @@ -95,15 +95,17 @@ class FrontpageChartGenerator private function collectExpenses(Category $category, Collection $accounts): array { + Log::debug(sprintf('Collect expenses for category #%d ("%s")', $category->id, $category->name)); $spent = $this->opsRepos->sumExpenses($this->start, $this->end, $accounts, new Collection([$category])); $tempData = []; foreach ($spent as $currency) { + Log::debug(sprintf('Spent %s %s', $currency['currency_code'], $currency['sum'])); $this->addCurrency($currency); $tempData[] = [ 'name' => $category->name, 'sum' => $currency['sum'], - 'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']), - 'currency_id' => (int)$currency['currency_id'], + 'sum_float' => round((float) $currency['sum'], $currency['currency_decimal_places']), + 'currency_id' => (int) $currency['currency_id'], ]; } @@ -112,7 +114,7 @@ class FrontpageChartGenerator private function addCurrency(array $currency): void { - $currencyId = (int)$currency['currency_id']; + $currencyId = (int) $currency['currency_id']; $this->currencies[$currencyId] ??= [ 'currency_id' => $currencyId, @@ -132,8 +134,8 @@ class FrontpageChartGenerator $tempData[] = [ 'name' => trans('firefly.no_category'), 'sum' => $currency['sum'], - 'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places'] ?? 2), // intentional float - 'currency_id' => (int)$currency['currency_id'], + 'sum_float' => round((float) $currency['sum'], $currency['currency_decimal_places'] ?? 2), // intentional float + 'currency_id' => (int) $currency['currency_id'], ]; } @@ -151,7 +153,7 @@ class FrontpageChartGenerator foreach ($this->currencies as $currencyId => $currency) { $key = sprintf('spent-%d', $currencyId); $return[$key] = [ - 'label' => sprintf('%s (%s)', (string)trans('firefly.spent'), $currency['currency_name']), + 'label' => sprintf('%s (%s)', (string) trans('firefly.spent'), $currency['currency_name']), 'type' => 'bar', 'currency_symbol' => $currency['currency_symbol'], 'entries' => $names, diff --git a/app/Support/Chart/Category/WholePeriodChartGenerator.php b/app/Support/Chart/Category/WholePeriodChartGenerator.php index b64448cd88..239015bf20 100644 --- a/app/Support/Chart/Category/WholePeriodChartGenerator.php +++ b/app/Support/Chart/Category/WholePeriodChartGenerator.php @@ -25,7 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Support\Chart\Category; use Carbon\Carbon; -use FireflyIII\Models\AccountType; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Models\Category; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Category\OperationsRepositoryInterface; @@ -36,6 +36,8 @@ use Illuminate\Support\Collection; */ class WholePeriodChartGenerator { + public bool $convertToNative; + public function generate(Category $category, Carbon $start, Carbon $end): array { $collection = new Collection([$category]); @@ -46,7 +48,7 @@ class WholePeriodChartGenerator /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); - $types = [AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]; + $types = [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; $accounts = $accountRepository->getAccountsByType($types); $step = $this->calculateStep($start, $end); $chartData = []; @@ -71,14 +73,14 @@ class WholePeriodChartGenerator $code = $currency['currency_code']; $name = $currency['currency_name']; $chartData[sprintf('spent-in-%s', $code)] = [ - 'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $name]), + 'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $name]), 'entries' => [], 'type' => 'bar', 'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red ]; $chartData[sprintf('earned-in-%s', $code)] = [ - 'label' => (string)trans('firefly.box_earned_in_currency', ['currency' => $name]), + 'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $name]), 'entries' => [], 'type' => 'bar', 'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php index 966f003203..66585c415f 100644 --- a/app/Support/Chart/ChartData.php +++ b/app/Support/Chart/ChartData.php @@ -35,15 +35,6 @@ class ChartData $this->series = []; } - public function render(): array - { - if (0 === count($this->series)) { - throw new FireflyException('No series added to chart'); - } - - return $this->series; - } - /** * @throws FireflyException */ @@ -64,4 +55,13 @@ class ChartData $this->series[] = $data; } + + public function render(): array + { + if (0 === count($this->series)) { + throw new FireflyException('No series added to chart'); + } + + return $this->series; + } } diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php index 6ac8d0440a..a2889ddc06 100644 --- a/app/Support/Cronjobs/AutoBudgetCronjob.php +++ b/app/Support/Cronjobs/AutoBudgetCronjob.php @@ -37,7 +37,7 @@ class AutoBudgetCronjob extends AbstractCronjob { /** @var Configuration $config */ $config = app('fireflyconfig')->get('last_ab_job', 0); - $lastTime = (int)$config->data; + $lastTime = (int) $config->data; $diff = time() - $lastTime; $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { @@ -78,7 +78,7 @@ class AutoBudgetCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Auto-budget cron job fired successfully.'; - app('fireflyconfig')->set('last_ab_job', (int)$this->date->format('U')); + app('fireflyconfig')->set('last_ab_job', (int) $this->date->format('U')); app('log')->info('Done with auto budget cron job task.'); } } diff --git a/app/Support/Cronjobs/BillWarningCronjob.php b/app/Support/Cronjobs/BillWarningCronjob.php index 364a144c28..720ca72c6a 100644 --- a/app/Support/Cronjobs/BillWarningCronjob.php +++ b/app/Support/Cronjobs/BillWarningCronjob.php @@ -43,7 +43,7 @@ class BillWarningCronjob extends AbstractCronjob /** @var Configuration $config */ $config = app('fireflyconfig')->get('last_bw_job', 0); - $lastTime = (int)$config->data; + $lastTime = (int) $config->data; $diff = time() - $lastTime; $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); @@ -91,8 +91,8 @@ class BillWarningCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Bill warning cron job fired successfully.'; - app('fireflyconfig')->set('last_bw_job', (int)$this->date->format('U')); - app('log')->info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U'))); + app('fireflyconfig')->set('last_bw_job', (int) $this->date->format('U')); + app('log')->info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); app('log')->info('Done with bill warning cron job task.'); } } diff --git a/app/Support/Cronjobs/ExchangeRatesCronjob.php b/app/Support/Cronjobs/ExchangeRatesCronjob.php index 2572b8ba1e..fd2c90a473 100644 --- a/app/Support/Cronjobs/ExchangeRatesCronjob.php +++ b/app/Support/Cronjobs/ExchangeRatesCronjob.php @@ -37,7 +37,7 @@ class ExchangeRatesCronjob extends AbstractCronjob { /** @var Configuration $config */ $config = app('fireflyconfig')->get('last_cer_job', 0); - $lastTime = (int)$config->data; + $lastTime = (int) $config->data; $diff = time() - $lastTime; $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { @@ -79,7 +79,7 @@ class ExchangeRatesCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Exchange rates cron job fired successfully.'; - app('fireflyconfig')->set('last_cer_job', (int)$this->date->format('U')); + app('fireflyconfig')->set('last_cer_job', (int) $this->date->format('U')); app('log')->info('Done with exchange rates job task.'); } } diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index 3b78a941de..2379b937c2 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -43,7 +43,7 @@ class RecurringCronjob extends AbstractCronjob /** @var Configuration $config */ $config = app('fireflyconfig')->get('last_rt_job', 0); - $lastTime = (int)$config->data; + $lastTime = (int) $config->data; $diff = time() - $lastTime; $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); @@ -88,8 +88,8 @@ class RecurringCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Recurring transactions cron job fired successfully.'; - app('fireflyconfig')->set('last_rt_job', (int)$this->date->format('U')); - app('log')->info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U'))); + app('fireflyconfig')->set('last_rt_job', (int) $this->date->format('U')); + app('log')->info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); app('log')->info('Done with recurring cron job task.'); } } diff --git a/app/Support/Domain.php b/app/Support/Domain.php index c37a70e431..b193717884 100644 --- a/app/Support/Domain.php +++ b/app/Support/Domain.php @@ -30,7 +30,7 @@ class Domain { public static function getBindables(): array { - return config('firefly.bindables'); + return config('bindables.bindables'); } public static function getRuleActions(): array diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 23a583f4ee..737c41a081 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -290,6 +290,27 @@ class ExpandedForm return $html; } + /** + * @throws FireflyException + */ + public function passwordWithValue(string $name, string $value, ?array $options = null): string + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + + try { + $html = view('form.password', compact('classes', 'value', 'name', 'label', 'options'))->render(); + } catch (\Throwable $e) { + app('log')->debug(sprintf('Could not render passwordWithValue(): %s', $e->getMessage())); + $html = 'Could not render passwordWithValue.'; + + throw new FireflyException($html, 0, $e); + } + + return $html; + } + /** * Function to render a percentage. * diff --git a/app/Support/Export/ExportDataGenerator.php b/app/Support/Export/ExportDataGenerator.php index 2ac986eb7f..7fb74e610b 100644 --- a/app/Support/Export/ExportDataGenerator.php +++ b/app/Support/Export/ExportDataGenerator.php @@ -456,10 +456,10 @@ class ExportDataGenerator $piggy->account->accountType->type, $piggy->name, $currency?->code, - $piggy->targetamount, - $repetition?->currentamount, - $piggy->startdate?->format('Y-m-d'), - $piggy->targetdate?->format('Y-m-d'), + $piggy->target_amount, + $repetition?->current_amount, + $piggy->start_date?->format('Y-m-d'), + $piggy->target_date?->format('Y-m-d'), $piggy->order, $piggy->active, ]; diff --git a/app/Support/FireflyConfig.php b/app/Support/FireflyConfig.php index c4078dbfd0..e44549dbd7 100644 --- a/app/Support/FireflyConfig.php +++ b/app/Support/FireflyConfig.php @@ -25,7 +25,10 @@ namespace FireflyIII\Support; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Configuration; +use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Contracts\Encryption\EncryptException; use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\Log; /** * Class FireflyConfig. @@ -46,6 +49,29 @@ class FireflyConfig return 1 === Configuration::where('name', $name)->count(); } + public function getEncrypted(string $name, $default = null): ?Configuration + { + $result = $this->get($name, $default); + if (null === $result) { + return null; + } + if ('' === $result->data) { + Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name)); + + return $result; + } + + try { + $result->data = decrypt($result->data); + } catch (DecryptException $e) { + Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage())); + + return $result; + } + + return $result; + } + /** * @param null|bool|int|string $default * @@ -78,10 +104,7 @@ class FireflyConfig return $this->set($name, $default); } - /** - * @param mixed $value - */ - public function set(string $name, $value): Configuration + public function set(string $name, mixed $value): Configuration { try { $config = Configuration::whereName($name)->whereNull('deleted_at')->first(); @@ -134,4 +157,17 @@ class FireflyConfig { return $this->set($name, $value); } + + public function setEncrypted(string $name, mixed $value): Configuration + { + try { + $encrypted = encrypt($value); + } catch (EncryptException $e) { + Log::error(sprintf('Could not encrypt configuration value "%s": %s', $name, $e->getMessage())); + + throw new FireflyException(sprintf('Could not encrypt configuration value "%s". Cowardly refuse to continue.', $name)); + } + + return $this->set($name, $encrypted); + } } diff --git a/app/Support/Form/AccountForm.php b/app/Support/Form/AccountForm.php index b7bc140a79..912861010d 100644 --- a/app/Support/Form/AccountForm.php +++ b/app/Support/Form/AccountForm.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Support\Form; +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -50,8 +51,8 @@ class AccountForm $repository = $this->getAccountRepository(); $grouped = $this->getAccountsGrouped($types, $repository); $cash = $repository->getCashAccount(); - $key = (string)trans('firefly.cash_account_type'); - $grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash')); + $key = (string) trans('firefly.cash_account_type'); + $grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash')); return $this->select($name, $grouped, $value, $options); } @@ -67,7 +68,7 @@ class AccountForm /** @var Account $account */ foreach ($accountList as $account) { - $role = (string)$repository->getMetaValue($account, 'account_role'); + $role = (string) $repository->getMetaValue($account, 'account_role'); if (in_array($account->accountType->type, $liabilityTypes, true)) { $role = sprintf('l_%s', $account->accountType->type); } @@ -80,7 +81,7 @@ class AccountForm $role = 'revenue_account'; } } - $key = (string)trans(sprintf('firefly.opt_group_%s', $role)); + $key = (string) trans(sprintf('firefly.opt_group_%s', $role)); $grouped[$key][$account->id] = $account->name; } @@ -97,8 +98,8 @@ class AccountForm $grouped = $this->getAccountsGrouped($types, $repository); $cash = $repository->getCashAccount(); - $key = (string)trans('firefly.cash_account_type'); - $grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash')); + $key = (string) trans('firefly.cash_account_type'); + $grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash')); return $this->select($name, $grouped, $value, $options); } @@ -141,12 +142,25 @@ class AccountForm */ public function assetAccountList(string $name, $value = null, ?array $options = null): string { - $types = [AccountType::ASSET, AccountType::DEFAULT]; + $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]; $grouped = $this->getAccountsGrouped($types); return $this->select($name, $grouped, $value, $options); } + /** + * Basic list of asset accounts. + * + * @param mixed $value + */ + public function assetLiabilityMultiAccountList(string $name, $value = null, ?array $options = null): string + { + $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value]; + $grouped = $this->getAccountsGrouped($types); + + return $this->multiSelect($name, $grouped, $value, $options); + } + /** * Same list but all liabilities as well. * @@ -154,7 +168,7 @@ class AccountForm */ public function longAccountList(string $name, $value = null, ?array $options = null): string { - $types = [AccountType::ASSET, AccountType::DEFAULT, AccountType::MORTGAGE, AccountType::DEBT, AccountType::CREDITCARD, AccountType::LOAN]; + $types = [AccountType::ASSET, AccountType::DEFAULT, AccountType::MORTGAGE, AccountType::DEBT, AccountType::LOAN]; $grouped = $this->getAccountsGrouped($types); return $this->select($name, $grouped, $value, $options); diff --git a/app/Support/Form/CurrencyForm.php b/app/Support/Form/CurrencyForm.php index 16cc72b0d8..7115f25fd3 100644 --- a/app/Support/Form/CurrencyForm.php +++ b/app/Support/Form/CurrencyForm.php @@ -69,7 +69,7 @@ class CurrencyForm $preFilled = []; } $key = 'amount_currency_id_'.$name; - $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $defaultCurrency->id; + $sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $defaultCurrency->id; app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId)); @@ -138,7 +138,7 @@ class CurrencyForm $preFilled = []; } $key = 'amount_currency_id_'.$name; - $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $defaultCurrency->id; + $sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $defaultCurrency->id; app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId)); @@ -204,7 +204,7 @@ class CurrencyForm // get all currencies: $list = $currencyRepos->get(); $array = [ - 0 => (string)trans('firefly.no_currency'), + 0 => (string) trans('firefly.no_currency'), ]; /** @var TransactionCurrency $currency */ diff --git a/app/Support/Form/FormSupport.php b/app/Support/Form/FormSupport.php index f91931cd60..62978ac598 100644 --- a/app/Support/Form/FormSupport.php +++ b/app/Support/Form/FormSupport.php @@ -34,23 +34,21 @@ use Illuminate\Support\MessageBag; */ trait FormSupport { - /** - * @param mixed $selected - */ - public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string + public function multiSelect(string $name, ?array $list = null, $selected = null, ?array $options = null): string { $list ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); $selected = $this->fillFieldValue($name, $selected); + unset($options['autocomplete'], $options['placeholder']); try { - $html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); + $html = view('form.multi-select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); } catch (\Throwable $e) { - app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage())); - $html = 'Could not render select.'; + app('log')->debug(sprintf('Could not render multi-select(): %s', $e->getMessage())); + $html = 'Could not render multi-select.'; } return $html; @@ -64,7 +62,7 @@ trait FormSupport } $name = str_replace('[]', '', $name); - return (string)trans('form.'.$name); + return (string) trans('form.'.$name); } /** @@ -119,6 +117,28 @@ trait FormSupport return $value; } + /** + * @param mixed $selected + */ + public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string + { + $list ??= []; + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $selected = $this->fillFieldValue($name, $selected); + unset($options['autocomplete'], $options['placeholder']); + + try { + $html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); + } catch (\Throwable $e) { + app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage())); + $html = 'Could not render select.'; + } + + return $html; + } + protected function getAccountRepository(): AccountRepositoryInterface { return app(AccountRepositoryInterface::class); diff --git a/app/Support/Form/PiggyBankForm.php b/app/Support/Form/PiggyBankForm.php index 0818d96157..78919b30bf 100644 --- a/app/Support/Form/PiggyBankForm.php +++ b/app/Support/Form/PiggyBankForm.php @@ -47,7 +47,7 @@ class PiggyBankForm /** @var PiggyBankRepositoryInterface $repository */ $repository = app(PiggyBankRepositoryInterface::class); $piggyBanks = $repository->getPiggyBanksWithAmount(); - $title = (string)trans('firefly.default_group_title_name'); + $title = (string) trans('firefly.default_group_title_name'); $array = []; $subList = [ 0 => [ @@ -55,7 +55,7 @@ class PiggyBankForm 'title' => $title, ], 'piggies' => [ - (string)trans('firefly.none_in_select_list'), + (string) trans('firefly.none_in_select_list'), ], ], ]; diff --git a/app/Support/Form/RuleForm.php b/app/Support/Form/RuleForm.php index 9566f0301f..6baee553be 100644 --- a/app/Support/Form/RuleForm.php +++ b/app/Support/Form/RuleForm.php @@ -66,12 +66,12 @@ class RuleForm // get all currencies: $list = $groupRepos->get(); $array = [ - 0 => (string)trans('firefly.none_in_select_list'), + 0 => (string) trans('firefly.none_in_select_list'), ]; /** @var RuleGroup $group */ foreach ($list as $group) { - if (array_key_exists('hidden', $options) && (int)$options['hidden'] !== $group->id) { + if (array_key_exists('hidden', $options) && (int) $options['hidden'] !== $group->id) { $array[$group->id] = $group->title; } } diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index 8d59995b2f..b1c97d0883 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -38,6 +38,7 @@ class AccountBalanceGrouped { private array $accountIds; private string $carbonFormat; + private ExchangeRateConverter $converter; private array $currencies = []; private array $data = []; private TransactionCurrency $default; @@ -45,7 +46,6 @@ class AccountBalanceGrouped private array $journals = []; private string $preferredRange; private Carbon $start; - private ExchangeRateConverter $converter; public function __construct() { @@ -139,51 +139,6 @@ class AccountBalanceGrouped $converter->summarize(); } - public function setAccounts(Collection $accounts): void - { - $this->accountIds = $accounts->pluck('id')->toArray(); - } - - public function setDefault(TransactionCurrency $default): void - { - $this->default = $default; - $defaultCurrencyId = $default->id; - $this->currencies = [$default->id => $default]; // currency cache - $this->data[$defaultCurrencyId] = [ - 'currency_id' => (string) $defaultCurrencyId, - 'currency_symbol' => $default->symbol, - 'currency_code' => $default->code, - 'currency_name' => $default->name, - 'currency_decimal_places' => $default->decimal_places, - 'native_currency_id' => (string) $defaultCurrencyId, - 'native_currency_symbol' => $default->symbol, - 'native_currency_code' => $default->code, - 'native_currency_name' => $default->name, - 'native_currency_decimal_places' => $default->decimal_places, - ]; - } - - public function setEnd(Carbon $end): void - { - $this->end = $end; - } - - public function setJournals(array $journals): void - { - $this->journals = $journals; - } - - public function setPreferredRange(string $preferredRange): void - { - $this->preferredRange = $preferredRange; - $this->carbonFormat = app('navigation')->preferredCarbonFormatByPeriod($preferredRange); - } - - public function setStart(Carbon $start): void - { - $this->start = $start; - } - private function processJournal(array $journal): void { // format the date according to the period @@ -292,4 +247,49 @@ class AccountBalanceGrouped return $rate; } + + public function setAccounts(Collection $accounts): void + { + $this->accountIds = $accounts->pluck('id')->toArray(); + } + + public function setDefault(TransactionCurrency $default): void + { + $this->default = $default; + $defaultCurrencyId = $default->id; + $this->currencies = [$default->id => $default]; // currency cache + $this->data[$defaultCurrencyId] = [ + 'currency_id' => (string) $defaultCurrencyId, + 'currency_symbol' => $default->symbol, + 'currency_code' => $default->code, + 'currency_name' => $default->name, + 'currency_decimal_places' => $default->decimal_places, + 'native_currency_id' => (string) $defaultCurrencyId, + 'native_currency_symbol' => $default->symbol, + 'native_currency_code' => $default->code, + 'native_currency_name' => $default->name, + 'native_currency_decimal_places' => $default->decimal_places, + ]; + } + + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + public function setJournals(array $journals): void + { + $this->journals = $journals; + } + + public function setPreferredRange(string $preferredRange): void + { + $this->preferredRange = $preferredRange; + $this->carbonFormat = app('navigation')->preferredCarbonFormatByPeriod($preferredRange); + } + + public function setStart(Carbon $start): void + { + $this->start = $start; + } } diff --git a/app/Support/Http/Api/AccountFilter.php b/app/Support/Http/Api/AccountFilter.php index cb8e510b1f..75c69eb13f 100644 --- a/app/Support/Http/Api/AccountFilter.php +++ b/app/Support/Http/Api/AccountFilter.php @@ -32,57 +32,58 @@ use FireflyIII\Models\AccountType; */ trait AccountFilter { - protected array $types = [ - 'all' => [ - AccountTypeEnum::DEFAULT->value, - AccountType::CASH, - AccountType::ASSET, - AccountType::EXPENSE, - AccountType::REVENUE, - AccountType::INITIAL_BALANCE, - AccountType::BENEFICIARY, - AccountType::IMPORT, - AccountType::RECONCILIATION, - AccountType::LOAN, - AccountType::DEBT, - AccountType::MORTGAGE, - ], - 'asset' => [AccountType::DEFAULT, AccountType::ASSET], - 'cash' => [AccountType::CASH], - 'expense' => [AccountType::EXPENSE, AccountType::BENEFICIARY], - 'revenue' => [AccountType::REVENUE], - 'special' => [AccountType::CASH, AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION], - 'hidden' => [AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION], - 'liability' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD], - 'liabilities' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD], - AccountType::DEFAULT => [AccountType::DEFAULT], - AccountType::CASH => [AccountType::CASH], - AccountType::ASSET => [AccountType::ASSET], - AccountType::EXPENSE => [AccountType::EXPENSE], - AccountType::REVENUE => [AccountType::REVENUE], - AccountType::INITIAL_BALANCE => [AccountType::INITIAL_BALANCE], - AccountType::BENEFICIARY => [AccountType::BENEFICIARY], - AccountType::IMPORT => [AccountType::IMPORT], - AccountType::RECONCILIATION => [AccountType::RECONCILIATION], - AccountType::LOAN => [AccountType::LOAN], - AccountType::MORTGAGE => [AccountType::MORTGAGE], - AccountType::DEBT => [AccountType::DEBT], - AccountType::CREDITCARD => [AccountType::CREDITCARD], - 'default account' => [AccountType::DEFAULT], - 'cash account' => [AccountType::CASH], - 'asset account' => [AccountType::ASSET], - 'expense account' => [AccountType::EXPENSE], - 'revenue account' => [AccountType::REVENUE], - 'initial balance account' => [AccountType::INITIAL_BALANCE], - 'reconciliation' => [AccountType::RECONCILIATION], - 'loan' => [AccountType::LOAN], - 'mortgage' => [AccountType::MORTGAGE], - 'debt' => [AccountType::DEBT], - 'credit card' => [AccountType::CREDITCARD], - 'credit-card' => [AccountType::CREDITCARD], - 'creditcard' => [AccountType::CREDITCARD], - 'cc' => [AccountType::CREDITCARD], - ]; + protected array $types + = [ + 'all' => [ + AccountTypeEnum::DEFAULT->value, + AccountType::CASH, + AccountType::ASSET, + AccountType::EXPENSE, + AccountType::REVENUE, + AccountType::INITIAL_BALANCE, + AccountType::BENEFICIARY, + AccountType::IMPORT, + AccountType::RECONCILIATION, + AccountType::LOAN, + AccountType::DEBT, + AccountType::MORTGAGE, + ], + 'asset' => [AccountType::DEFAULT, AccountType::ASSET], + 'cash' => [AccountType::CASH], + 'expense' => [AccountType::EXPENSE, AccountType::BENEFICIARY], + 'revenue' => [AccountType::REVENUE], + 'special' => [AccountType::CASH, AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION], + 'hidden' => [AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION], + 'liability' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD], + 'liabilities' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD], + AccountType::DEFAULT => [AccountType::DEFAULT], + AccountType::CASH => [AccountType::CASH], + AccountType::ASSET => [AccountType::ASSET], + AccountType::EXPENSE => [AccountType::EXPENSE], + AccountType::REVENUE => [AccountType::REVENUE], + AccountType::INITIAL_BALANCE => [AccountType::INITIAL_BALANCE], + AccountType::BENEFICIARY => [AccountType::BENEFICIARY], + AccountType::IMPORT => [AccountType::IMPORT], + AccountType::RECONCILIATION => [AccountType::RECONCILIATION], + AccountType::LOAN => [AccountType::LOAN], + AccountType::MORTGAGE => [AccountType::MORTGAGE], + AccountType::DEBT => [AccountType::DEBT], + AccountType::CREDITCARD => [AccountType::CREDITCARD], + 'default account' => [AccountType::DEFAULT], + 'cash account' => [AccountType::CASH], + 'asset account' => [AccountType::ASSET], + 'expense account' => [AccountType::EXPENSE], + 'revenue account' => [AccountType::REVENUE], + 'initial balance account' => [AccountType::INITIAL_BALANCE], + 'reconciliation' => [AccountType::RECONCILIATION], + 'loan' => [AccountType::LOAN], + 'mortgage' => [AccountType::MORTGAGE], + 'debt' => [AccountType::DEBT], + 'credit card' => [AccountType::CREDITCARD], + 'credit-card' => [AccountType::CREDITCARD], + 'creditcard' => [AccountType::CREDITCARD], + 'cc' => [AccountType::CREDITCARD], + ]; /** * All the available types. diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index b4f1bb2bfe..c3938bf413 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -48,10 +48,10 @@ trait CleansChartData */ foreach ($data as $index => $array) { if (array_key_exists('currency_id', $array)) { - $array['currency_id'] = (string)$array['currency_id']; + $array['currency_id'] = (string) $array['currency_id']; } if (array_key_exists('native_currency_id', $array)) { - $array['native_currency_id'] = (string)$array['native_currency_id']; + $array['native_currency_id'] = (string) $array['native_currency_id']; } if (!array_key_exists('start', $array)) { throw new FireflyException(sprintf('Data-set "%s" is missing the "start"-variable.', $index)); diff --git a/app/Support/Http/Api/ConvertsExchangeRates.php b/app/Support/Http/Api/ConvertsExchangeRates.php index 465de66dc8..cd0141171b 100644 --- a/app/Support/Http/Api/ConvertsExchangeRates.php +++ b/app/Support/Http/Api/ConvertsExchangeRates.php @@ -56,9 +56,9 @@ trait ConvertsExchangeRates /** @var TransactionCurrency $native */ $native = app('amount')->getDefaultCurrency(); - $currency = $this->getCurrency((int)$set['currency_id']); + $currency = $this->getCurrency((int) $set['currency_id']); if ($native->id === $currency->id) { - $set['native_currency_id'] = (string)$currency->id; + $set['native_currency_id'] = (string) $currency->id; $set['native_currency_code'] = $currency->code; $set['native_currency_symbol'] = $currency->symbol; $set['native_currency_decimal_places'] = $currency->decimal_places; @@ -69,8 +69,8 @@ trait ConvertsExchangeRates $carbon = Carbon::createFromFormat(\DateTimeInterface::ATOM, $date); $rate = $this->getRate($currency, $native, $carbon); $rate = '0' === $rate ? '1' : $rate; - app('log')->debug(sprintf('bcmul("%s", "%s")', (string)$entry, $rate)); - $set['entries'][$date] = (float)bcmul((string)$entry, $rate); + app('log')->debug(sprintf('bcmul("%s", "%s")', (string) $entry, $rate)); + $set['entries'][$date] = (float) bcmul((string) $entry, $rate); } return $set; @@ -128,12 +128,12 @@ trait ConvertsExchangeRates /** @var array $entry */ foreach ($entries as $entry) { - $currency = $this->getCurrency((int)$entry['id']); + $currency = $this->getCurrency((int) $entry['id']); if ($currency->id !== $native->id) { $amount = $this->convertAmount($entry['sum'], $currency, $native); $entry['converted'] = true; $entry['native_sum'] = $amount; - $entry['native_currency_id'] = (string)$native->id; + $entry['native_currency_id'] = (string) $native->id; $entry['native_currency_name'] = $native->name; $entry['native_currency_symbol'] = $native->symbol; $entry['native_currency_code'] = $native->code; @@ -142,7 +142,7 @@ trait ConvertsExchangeRates if ($currency->id === $native->id) { $entry['converted'] = false; $entry['native_sum'] = $entry['sum']; - $entry['native_currency_id'] = (string)$native->id; + $entry['native_currency_id'] = (string) $native->id; $entry['native_currency_name'] = $native->name; $entry['native_currency_symbol'] = $native->symbol; $entry['native_currency_code'] = $native->code; diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index 1b1ddc4465..c3d847179d 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -28,7 +28,9 @@ use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\UserGroup; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; @@ -39,14 +41,24 @@ class ExchangeRateConverter { // use ConvertsExchangeRates; private array $fallback = []; + private bool $ignoreSettings = false; private bool $isPrepared = false; private bool $noPreparedRates = false; private array $prepared = []; private int $queryCount = 0; - public function enabled(): bool + private UserGroup $userGroup; + + public function __construct() { - return false !== config('cer.enabled'); + if (auth()->check()) { + $this->userGroup = auth()->user()->userGroup; + } + } + + public function setUserGroup(UserGroup $userGroup): void + { + $this->userGroup = $userGroup; } /** @@ -61,7 +73,12 @@ class ExchangeRateConverter } $rate = $this->getCurrencyRate($from, $to, $date); - return bcmul($amount, $rate); + return Steam::bcround(bcmul($amount, $rate), $to->decimal_places); + } + + public function enabled(): bool + { + return false !== config('cer.enabled') || true === $this->ignoreSettings; } /** @@ -108,7 +125,7 @@ class ExchangeRateConverter if (null !== $rate) { $rate = bcdiv('1', $rate); Cache::forever($key, $rate); - Log::debug(sprintf('ExchangeRateConverter: Return DB rate from #%d to #%d on %s.', $from->id, $to->id, $date->format('Y-m-d'))); + Log::debug(sprintf('ExchangeRateConverter: Return inverse DB rate from #%d to #%d on %s.', $from->id, $to->id, $date->format('Y-m-d'))); return $rate; } @@ -132,6 +149,11 @@ class ExchangeRateConverter return $rate; } + private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string + { + return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d')); + } + private function getFromDB(int $from, int $to, string $date): ?string { if ($from === $to) { @@ -160,8 +182,7 @@ class ExchangeRateConverter } /** @var null|CurrencyExchangeRate $result */ - $result = auth()->user() - ->currencyExchangeRates() + $result = $this->userGroup->currencyExchangeRates() ->where('from_currency_id', $from) ->where('to_currency_id', $to) ->where('date', '<=', $date) @@ -266,7 +287,7 @@ class ExchangeRateConverter $start->startOfDay(); $end->endOfDay(); Log::debug(sprintf('Preparing for %s to %s between %s and %s', $from->code, $to->code, $start->format('Y-m-d'), $end->format('Y-m-d'))); - $set = auth()->user() + $set = $this->userGroup ->currencyExchangeRates() ->where('from_currency_id', $from->id) ->where('to_currency_id', $to->id) @@ -332,6 +353,11 @@ class ExchangeRateConverter Log::debug(sprintf('Fallback rate %s > %s = %s', $to->code, $from->code, bcdiv('1', $fallback))); } + public function setIgnoreSettings(bool $ignoreSettings): void + { + $this->ignoreSettings = $ignoreSettings; + } + public function summarize(): void { if (false === $this->enabled()) { @@ -339,9 +365,4 @@ class ExchangeRateConverter } Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount)); } - - private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string - { - return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d')); - } } diff --git a/app/Support/Http/Api/ParsesQueryFilters.php b/app/Support/Http/Api/ParsesQueryFilters.php deleted file mode 100644 index d59e0fd6a6..0000000000 --- a/app/Support/Http/Api/ParsesQueryFilters.php +++ /dev/null @@ -1,75 +0,0 @@ -filter()?->value($field, date('Y-m-d')); - - if (is_array($value)) { - Log::error(sprintf('Multiple values for date field "%s". Using first value.', $field)); - $value = $value[0]; - } - - try { - $date = Carbon::createFromFormat('Y-m-d', $value, config('app.timezone')); - } catch (InvalidFormatException $e) { - Log::debug(sprintf('Invalid date format in request. Using today: %s', $e->getMessage())); - } - - return $date; - } - - private function arrayOfStrings(QueryParameters $parameters, string $field): array - { - $array = $parameters->filter()?->value($field, []) ?? []; - - return is_string($array) ? [$array] : $array; - } - - private function integerFromQueryParams(QueryParameters $parameters, string $field, int $default): int - { - return (int) ($parameters->page()[$field] ?? $default); - } - - private function stringFromQueryParams(QueryParameters $parameters, string $field, string $default): string - { - return (string) ($parameters->page()[$field] ?? $default); - } - - private function stringFromFilterParams(QueryParameters $parameters, string $field, string $default): string - { - return (string)$parameters->filter()?->value($field, $default) ?? $default; - } -} diff --git a/app/Support/Http/Api/SummaryBalanceGrouped.php b/app/Support/Http/Api/SummaryBalanceGrouped.php index ce8e8a1ce6..f415075e73 100644 --- a/app/Support/Http/Api/SummaryBalanceGrouped.php +++ b/app/Support/Http/Api/SummaryBalanceGrouped.php @@ -59,7 +59,7 @@ class SummaryBalanceGrouped $return[] = [ 'key' => sprintf('%s-in-native', $title), 'value' => $this->amounts[$key]['native'] ?? '0', - 'currency_id' => (string)$this->default->id, + 'currency_id' => (string) $this->default->id, 'currency_code' => $this->default->code, 'currency_symbol' => $this->default->symbol, 'currency_decimal_places' => $this->default->decimal_places, @@ -72,7 +72,7 @@ class SummaryBalanceGrouped // skip native entries. continue; } - $currencyId = (int)$currencyId; + $currencyId = (int) $currencyId; $currency = $this->currencies[$currencyId] ?? $this->currencyRepository->find($currencyId); $this->currencies[$currencyId] = $currency; // create objects for big array. @@ -86,7 +86,7 @@ class SummaryBalanceGrouped $return[] = [ 'key' => sprintf('%s-in-%s', $title, $currency->code), 'value' => $this->amounts[$key][$currencyId] ?? '0', - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, @@ -108,12 +108,12 @@ class SummaryBalanceGrouped /** @var array $journal */ foreach ($journals as $journal) { // transaction info: - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; $amount = bcmul($journal['amount'], $multiplier); $currency = $this->currencies[$currencyId] ?? TransactionCurrency::find($currencyId); $this->currencies[$currencyId] = $currency; $nativeAmount = $converter->convert($currency, $this->default, $journal['date'], $amount); - if ((int)$journal['foreign_currency_id'] === $this->default->id) { + if ((int) $journal['foreign_currency_id'] === $this->default->id) { // use foreign amount instead $nativeAmount = $journal['foreign_amount']; } diff --git a/app/Support/Http/Controllers/AugumentData.php b/app/Support/Http/Controllers/AugumentData.php index 6064fee9a2..ca37bf480e 100644 --- a/app/Support/Http/Controllers/AugumentData.php +++ b/app/Support/Http/Controllers/AugumentData.php @@ -110,7 +110,7 @@ trait AugumentData $return = []; foreach ($accountIds as $combinedId) { $parts = explode('-', $combinedId); - $accountId = (int)$parts[0]; + $accountId = (int) $parts[0]; if (array_key_exists($accountId, $grouped)) { $return[$accountId] = $grouped[$accountId][0]['name']; } @@ -135,7 +135,7 @@ trait AugumentData $return[$budgetId] = $grouped[$budgetId][0]['name']; } } - $return[0] = (string)trans('firefly.no_budget'); + $return[0] = (string) trans('firefly.no_budget'); return $return; } @@ -152,12 +152,12 @@ trait AugumentData $return = []; foreach ($categoryIds as $combinedId) { $parts = explode('-', $combinedId); - $categoryId = (int)$parts[0]; + $categoryId = (int) $parts[0]; if (array_key_exists($categoryId, $grouped)) { $return[$categoryId] = $grouped[$categoryId][0]['name']; } } - $return[0] = (string)trans('firefly.no_category'); + $return[0] = (string) trans('firefly.no_category'); return $return; } @@ -179,6 +179,7 @@ trait AugumentData $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($budget->id); + $cache->addProperty($this->convertToNative); $cache->addProperty('get-limits'); if ($cache->has()) { @@ -186,15 +187,18 @@ trait AugumentData } $set = $blRepository->getBudgetLimits($budget, $start, $end); - $limits = new Collection(); + $budgetCollection = new Collection([$budget]); + // merge sets based on a key, in case of convert to native + $limits = new Collection(); + /** @var BudgetLimit $entry */ foreach ($set as $entry) { $currency = $entry->transactionCurrency; - - if (null === $currency) { - $currency = app('amount')->getDefaultCurrency(); + if ($this->convertToNative) { + // the sumExpenses method already handles this. + $currency = $this->defaultCurrency; } // clone because these objects change each other. @@ -214,7 +218,7 @@ trait AugumentData } $cache->store($limits); - return $set; + return $limits; } /** @@ -259,7 +263,7 @@ trait AugumentData ]; // loop to support multi currency foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; // if not set, set to zero: if (!array_key_exists($currencyId, $sum['per_currency'])) { diff --git a/app/Support/Http/Controllers/BasicDataSupport.php b/app/Support/Http/Controllers/BasicDataSupport.php index 1cb0079291..2030c58f85 100644 --- a/app/Support/Http/Controllers/BasicDataSupport.php +++ b/app/Support/Http/Controllers/BasicDataSupport.php @@ -38,7 +38,9 @@ trait BasicDataSupport */ protected function isInArray(array $array, int $entryId) { - return $array[$entryId] ?? '0'; + $key = $this->convertToNative ? 'native_balance' : 'balance'; + + return $array[$entryId][$key] ?? '0'; } /** diff --git a/app/Support/Http/Controllers/ChartGeneration.php b/app/Support/Http/Controllers/ChartGeneration.php index a5df295ee4..e1c9aea6ca 100644 --- a/app/Support/Http/Controllers/ChartGeneration.php +++ b/app/Support/Http/Controllers/ChartGeneration.php @@ -30,6 +30,8 @@ use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Collection; /** @@ -45,33 +47,34 @@ trait ChartGeneration protected function accountBalanceChart(Collection $accounts, Carbon $start, Carbon $end): array // chart helper method. { // chart properties for cache: - $cache = new CacheProperties(); + $convertToNative = Amount::convertToNative(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('chart.account.account-balance-chart'); $cache->addProperty($accounts); + $cache->addProperty($convertToNative); if ($cache->has()) { return $cache->get(); } app('log')->debug('Regenerate chart.account.account-balance-chart from scratch.'); - $locale = app('steam')->getLocale(); + $locale = app('steam')->getLocale(); /** @var GeneratorInterface $generator */ - $generator = app(GeneratorInterface::class); + $generator = app(GeneratorInterface::class); /** @var AccountRepositoryInterface $accountRepos */ - $accountRepos = app(AccountRepositoryInterface::class); + $accountRepos = app(AccountRepositoryInterface::class); - $default = app('amount')->getDefaultCurrency(); - $chartData = []; + $default = app('amount')->getDefaultCurrency(); + $chartData = []; /** @var Account $account */ foreach ($accounts as $account) { - // TODO we can use getAccountCurrency instead. - $currency = $accountRepos->getAccountCurrency($account); - if (null === $currency) { - $currency = $default; - } + $currency = $accountRepos->getAccountCurrency($account) ?? $default; + $useNative = $convertToNative && $default->id !== $currency->id; + $field = $useNative ? 'native_balance' : 'balance'; + $currency = $useNative ? $default : $currency; $currentSet = [ 'label' => $account->name, 'currency_symbol' => $currency->symbol, @@ -79,19 +82,19 @@ trait ChartGeneration ]; $currentStart = clone $start; - $range = app('steam')->balanceInRange($account, $start, clone $end); + $range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToNative); $previous = array_values($range)[0]; while ($currentStart <= $end) { $format = $currentStart->format('Y-m-d'); - $label = trim($currentStart->isoFormat((string)trans('config.month_and_day_js', [], $locale))); + $label = trim($currentStart->isoFormat((string) trans('config.month_and_day_js', [], $locale))); $balance = $range[$format] ?? $previous; $previous = $balance; $currentStart->addDay(); - $currentSet['entries'][$label] = $balance; + $currentSet['entries'][$label] = $balance[$field]; } $chartData[] = $currentSet; } - $data = $generator->multiSet($chartData); + $data = $generator->multiSet($chartData); $cache->store($data); return $data; diff --git a/app/Support/Http/Controllers/CreateStuff.php b/app/Support/Http/Controllers/CreateStuff.php index 06832c32c5..59fe677edd 100644 --- a/app/Support/Http/Controllers/CreateStuff.php +++ b/app/Support/Http/Controllers/CreateStuff.php @@ -71,7 +71,7 @@ trait CreateStuff /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $assetAccount = [ - 'name' => (string)trans('firefly.cash_wallet', [], $language), + 'name' => (string) trans('firefly.cash_wallet', [], $language), 'iban' => null, 'account_type_name' => 'asset', 'virtual_balance' => 0, @@ -106,7 +106,7 @@ trait CreateStuff Log::alert('NO OAuth keys were found. They have been created.'); - file_put_contents($publicKey, (string)$key->getPublicKey()); + file_put_contents($publicKey, (string) $key->getPublicKey()); file_put_contents($privateKey, $key->toString('PKCS1')); } @@ -118,7 +118,7 @@ trait CreateStuff /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $savingsAccount = [ - 'name' => (string)trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language), + 'name' => (string) trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language), 'iban' => null, 'account_type_name' => 'asset', 'account_type_id' => null, diff --git a/app/Support/Http/Controllers/DateCalculation.php b/app/Support/Http/Controllers/DateCalculation.php index 9dc319cb47..32a4c58841 100644 --- a/app/Support/Http/Controllers/DateCalculation.php +++ b/app/Support/Http/Controllers/DateCalculation.php @@ -40,13 +40,13 @@ trait DateCalculation */ public function activeDaysLeft(Carbon $start, Carbon $end): int { - $difference = (int)($start->diffInDays($end, true) + 1); + $difference = (int) ($start->diffInDays($end, true) + 1); $today = today(config('app.timezone'))->startOfDay(); if ($start->lte($today) && $end->gte($today)) { $difference = $today->diffInDays($end) + 1; } - return (int)(0 === $difference ? 1 : $difference); + return (int) (0 === $difference ? 1 : $difference); } /** @@ -63,7 +63,7 @@ trait DateCalculation $difference = $start->diffInDays($today, true) + 1; } - return (int)$difference; + return (int) $difference; } protected function calculateStep(Carbon $start, Carbon $end): string diff --git a/app/Support/Http/Controllers/GetConfigurationData.php b/app/Support/Http/Controllers/GetConfigurationData.php index a66f937d62..5619de0ef0 100644 --- a/app/Support/Http/Controllers/GetConfigurationData.php +++ b/app/Support/Http/Controllers/GetConfigurationData.php @@ -48,7 +48,7 @@ trait GetConfigurationData E_COMPILE_ERROR | E_RECOVERABLE_ERROR | E_ERROR | E_CORE_ERROR => 'E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR', ]; - return $array[$value] ?? (string)$value; + return $array[$value] ?? (string) $value; } /** @@ -64,7 +64,7 @@ trait GetConfigurationData $currentStep = $options; // get the text: - $currentStep['intro'] = (string)trans('intro.'.$route.'_'.$key); + $currentStep['intro'] = (string) trans('intro.'.$route.'_'.$key); // save in array: $steps[] = $currentStep; @@ -133,41 +133,41 @@ trait GetConfigurationData $todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange); if ($todayStart->ne($start) || $todayEnd->ne($end)) { - $ranges[ucfirst((string)trans('firefly.today'))] = [$todayStart, $todayEnd]; + $ranges[ucfirst((string) trans('firefly.today'))] = [$todayStart, $todayEnd]; } // last seven days: $seven = today(config('app.timezone'))->subDays(7); - $index = (string)trans('firefly.last_seven_days'); + $index = (string) trans('firefly.last_seven_days'); $ranges[$index] = [$seven, new Carbon()]; // last 30 days: $thirty = today(config('app.timezone'))->subDays(30); - $index = (string)trans('firefly.last_thirty_days'); + $index = (string) trans('firefly.last_thirty_days'); $ranges[$index] = [$thirty, new Carbon()]; // month to date: $monthBegin = today(config('app.timezone'))->startOfMonth(); - $index = (string)trans('firefly.month_to_date'); + $index = (string) trans('firefly.month_to_date'); $ranges[$index] = [$monthBegin, new Carbon()]; // year to date: $yearBegin = today(config('app.timezone'))->startOfYear(); - $index = (string)trans('firefly.year_to_date'); + $index = (string) trans('firefly.year_to_date'); $ranges[$index] = [$yearBegin, new Carbon()]; // everything - $index = (string)trans('firefly.everything'); + $index = (string) trans('firefly.everything'); $ranges[$index] = [$first, new Carbon()]; return [ 'title' => $title, 'configuration' => [ - 'apply' => (string)trans('firefly.apply'), - 'cancel' => (string)trans('firefly.cancel'), - 'from' => (string)trans('firefly.from'), - 'to' => (string)trans('firefly.to'), - 'customRange' => (string)trans('firefly.customRange'), + 'apply' => (string) trans('firefly.apply'), + 'cancel' => (string) trans('firefly.cancel'), + 'from' => (string) trans('firefly.from'), + 'to' => (string) trans('firefly.to'), + 'customRange' => (string) trans('firefly.customRange'), 'start' => $start->format('Y-m-d'), 'end' => $end->format('Y-m-d'), 'ranges' => $ranges, @@ -192,7 +192,7 @@ trait GetConfigurationData $currentStep = $options; // get the text: - $currentStep['intro'] = (string)trans('intro.'.$route.'_'.$specificPage.'_'.$key); + $currentStep['intro'] = (string) trans('intro.'.$route.'_'.$specificPage.'_'.$key); // save in array: $steps[] = $currentStep; @@ -207,7 +207,7 @@ trait GetConfigurationData protected function verifyRecurringCronJob(): void { $config = app('fireflyconfig')->get('last_rt_job', 0); - $lastTime = (int)$config?->data; + $lastTime = (int) $config?->data; $now = time(); app('log')->debug(sprintf('verifyRecurringCronJob: last time is %d ("%s"), now is %d', $lastTime, $config?->data, $now)); if (0 === $lastTime) { diff --git a/app/Support/Http/Controllers/ModelInformation.php b/app/Support/Http/Controllers/ModelInformation.php index fbd1524b4f..d1a96aa307 100644 --- a/app/Support/Http/Controllers/ModelInformation.php +++ b/app/Support/Http/Controllers/ModelInformation.php @@ -86,9 +86,9 @@ trait ModelInformation /** @var AccountType $mortgage */ $mortgage = $repository->getAccountTypeByType(AccountType::MORTGAGE); $liabilityTypes = [ - $debt->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)), - $loan->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)), - $mortgage->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)), + $debt->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)), + $loan->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)), + $mortgage->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)), ]; asort($liabilityTypes); @@ -99,7 +99,7 @@ trait ModelInformation { $roles = []; foreach (config('firefly.accountRoles') as $role) { - $roles[$role] = (string)trans(sprintf('firefly.account_role_%s', $role)); + $roles[$role] = (string) trans(sprintf('firefly.account_role_%s', $role)); } return $roles; @@ -117,7 +117,7 @@ trait ModelInformation $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { - $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); + $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key)); } } asort($triggers); @@ -168,7 +168,7 @@ trait ModelInformation $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { - $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); + $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key)); } } asort($triggers); diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 0563b94750..632f5088cf 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -166,7 +166,7 @@ trait PeriodOverview /** @var array $journal */ foreach ($journals as $journal) { - if ($account->id === (int)$journal['source_account_id']) { + if ($account->id === (int) $journal['source_account_id']) { $return[] = $journal; } } @@ -183,7 +183,7 @@ trait PeriodOverview /** @var array $journal */ foreach ($journals as $journal) { - if ($account->id === (int)$journal['destination_account_id']) { + if ($account->id === (int) $journal['destination_account_id']) { $return[] = $journal; } } @@ -197,37 +197,44 @@ trait PeriodOverview /** @var array $journal */ foreach ($journals as $journal) { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int) $journal['currency_id']; + $currencyCode = $journal['currency_code']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; $foreignCurrencyId = $journal['foreign_currency_id']; - if (!array_key_exists($currencyId, $return)) { - $return[$currencyId] = [ - 'amount' => '0', - 'count' => 0, - 'currency_id' => $currencyId, - 'currency_name' => $journal['currency_name'], - 'currency_code' => $journal['currency_code'], - 'currency_symbol' => $journal['currency_symbol'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - ]; - } - $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $journal['amount'] ?? '0'); - ++$return[$currencyId]['count']; + $amount = $journal['amount']; - if (null !== $foreignCurrencyId && null !== $journal['foreign_amount']) { - if (!array_key_exists($foreignCurrencyId, $return)) { - $return[$foreignCurrencyId] = [ - 'amount' => '0', - 'count' => 0, - 'currency_id' => (int)$foreignCurrencyId, - 'currency_name' => $journal['foreign_currency_name'], - 'currency_code' => $journal['foreign_currency_code'], - 'currency_symbol' => $journal['foreign_currency_symbol'], - 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], - ]; - } - ++$return[$foreignCurrencyId]['count']; - $return[$foreignCurrencyId]['amount'] = bcadd($return[$foreignCurrencyId]['amount'], $journal['foreign_amount']); + + if ($this->convertToNative && $currencyId !== $this->defaultCurrency->id && $foreignCurrencyId !== $this->defaultCurrency->id) { + $amount = $journal['native_amount']; + $currencyId = $this->defaultCurrency->id; + $currencyCode = $this->defaultCurrency->code; + $currencyName = $this->defaultCurrency->name; + $currencySymbol = $this->defaultCurrency->symbol; + $currencyDecimalPlaces = $this->defaultCurrency->decimal_places; } + if ($this->convertToNative && $currencyId !== $this->defaultCurrency->id && $foreignCurrencyId === $this->defaultCurrency->id) { + $currencyId = (int) $foreignCurrencyId; + $currencyCode = $journal['foreign_currency_code']; + $currencyName = $journal['foreign_currency_name']; + $currencySymbol = $journal['foreign_currency_symbol']; + $currencyDecimalPlaces = $journal['foreign_currency_decimal_places']; + $amount = $journal['foreign_amount']; + } + $return[$currencyId] ??= [ + 'amount' => '0', + 'count' => 0, + 'currency_id' => $currencyId, + 'currency_name' => $currencyName, + 'currency_code' => $currencyCode, + 'currency_symbol' => $currencySymbol, + 'currency_decimal_places' => $currencyDecimalPlaces, + ]; + + + $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount); + ++$return[$currencyId]['count']; } return $return; @@ -322,6 +329,7 @@ trait PeriodOverview $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); + $cache->addProperty($this->convertToNative); $cache->addProperty('no-budget-period-entries'); if ($cache->has()) { diff --git a/app/Support/Http/Controllers/RenderPartialViews.php b/app/Support/Http/Controllers/RenderPartialViews.php index b49be23a08..b4764dd024 100644 --- a/app/Support/Http/Controllers/RenderPartialViews.php +++ b/app/Support/Http/Controllers/RenderPartialViews.php @@ -55,10 +55,10 @@ trait RenderPartialViews /** @var BudgetRepositoryInterface $budgetRepository */ $budgetRepository = app(BudgetRepositoryInterface::class); - $budget = $budgetRepository->find((int)$attributes['budgetId']); + $budget = $budgetRepository->find((int) $attributes['budgetId']); $accountRepos = app(AccountRepositoryInterface::class); - $account = $accountRepos->find((int)$attributes['accountId']); + $account = $accountRepos->find((int) $attributes['accountId']); if (null === $budget || null === $account) { throw new FireflyException('Could not render popup.report.balance-amount because budget or account is null.'); @@ -114,7 +114,7 @@ trait RenderPartialViews /** @var PopupReportInterface $popupHelper */ $popupHelper = app(PopupReportInterface::class); - $budget = $budgetRepository->find((int)$attributes['budgetId']); + $budget = $budgetRepository->find((int) $attributes['budgetId']); if (null === $budget) { // transactions without a budget. $budget = new Budget(); @@ -145,7 +145,7 @@ trait RenderPartialViews /** @var CategoryRepositoryInterface $categoryRepository */ $categoryRepository = app(CategoryRepositoryInterface::class); - $category = $categoryRepository->find((int)$attributes['categoryId']); + $category = $categoryRepository->find((int) $attributes['categoryId']); $journals = $popupHelper->byCategory($category, $attributes); try { @@ -238,7 +238,7 @@ trait RenderPartialViews /** @var PopupReportInterface $popupHelper */ $popupHelper = app(PopupReportInterface::class); - $account = $accountRepository->find((int)$attributes['accountId']); + $account = $accountRepository->find((int) $attributes['accountId']); if (null === $account) { return 'This is an unknown account. Apologies.'; @@ -309,7 +309,7 @@ trait RenderPartialViews $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { - $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); + $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key)); } } asort($triggers); @@ -324,7 +324,7 @@ trait RenderPartialViews $count = ($index + 1); try { - $rootOperator = OperatorQuerySearch::getRootOperator((string)$entry->trigger_type); + $rootOperator = OperatorQuerySearch::getRootOperator((string) $entry->trigger_type); if (str_starts_with($rootOperator, '-')) { $rootOperator = substr($rootOperator, 1); } @@ -334,7 +334,7 @@ trait RenderPartialViews 'oldTrigger' => $rootOperator, 'oldValue' => $entry->trigger_value, 'oldChecked' => $entry->stop_processing, - 'oldProhibited' => str_starts_with((string)$entry->trigger_type, '-'), + 'oldProhibited' => str_starts_with((string) $entry->trigger_type, '-'), 'count' => $count, 'triggers' => $triggers, ] @@ -365,7 +365,7 @@ trait RenderPartialViews /** @var PopupReportInterface $popupHelper */ $popupHelper = app(PopupReportInterface::class); - $account = $accountRepository->find((int)$attributes['accountId']); + $account = $accountRepository->find((int) $attributes['accountId']); if (null === $account) { return 'This is an unknown category. Apologies.'; diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php index 5466ed5242..7307496f2e 100644 --- a/app/Support/Http/Controllers/RequestInformation.php +++ b/app/Support/Http/Controllers/RequestInformation.php @@ -64,7 +64,7 @@ trait RequestInformation 'type' => $triggerInfo['type'] ?? '', 'value' => $triggerInfo['value'] ?? '', 'prohibited' => $triggerInfo['prohibited'] ?? false, - 'stop_processing' => 1 === (int)($triggerInfo['stop_processing'] ?? '0'), + 'stop_processing' => 1 === (int) ($triggerInfo['stop_processing'] ?? '0'), ]; $current = RuleFormRequest::replaceAmountTrigger($current); $triggers[] = $current; @@ -170,11 +170,11 @@ trait RequestInformation final protected function validatePassword(User $user, string $current, string $new): bool // get request info { if (!\Hash::check($current, $user->password)) { - throw new ValidationException((string)trans('firefly.invalid_current_password')); + throw new ValidationException((string) trans('firefly.invalid_current_password')); } if ($current === $new) { - throw new ValidationException((string)trans('firefly.should_change')); + throw new ValidationException((string) trans('firefly.should_change')); } return true; diff --git a/app/Support/Http/Controllers/RuleManagement.php b/app/Support/Http/Controllers/RuleManagement.php index 91331bfe49..a334161ee0 100644 --- a/app/Support/Http/Controllers/RuleManagement.php +++ b/app/Support/Http/Controllers/RuleManagement.php @@ -50,7 +50,7 @@ trait RuleManagement [ 'oldAction' => $oldAction['type'], 'oldValue' => $oldAction['value'] ?? '', - 'oldChecked' => 1 === (int)($oldAction['stop_processing'] ?? '0'), + 'oldChecked' => 1 === (int) ($oldAction['stop_processing'] ?? '0'), 'count' => $index + 1, ] )->render(); @@ -77,7 +77,7 @@ trait RuleManagement $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { - $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); + $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key)); } } asort($triggers); @@ -93,8 +93,8 @@ trait RuleManagement [ 'oldTrigger' => OperatorQuerySearch::getRootOperator($oldTrigger['type']), 'oldValue' => $oldTrigger['value'] ?? '', - 'oldChecked' => 1 === (int)($oldTrigger['stop_processing'] ?? '0'), - 'oldProhibited' => 1 === (int)($oldTrigger['prohibited'] ?? '0'), + 'oldChecked' => 1 === (int) ($oldTrigger['stop_processing'] ?? '0'), + 'oldProhibited' => 1 === (int) ($oldTrigger['prohibited'] ?? '0'), 'count' => $index + 1, 'triggers' => $triggers, ] @@ -123,7 +123,7 @@ trait RuleManagement $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { - $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); + $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key)); } } asort($triggers); @@ -131,7 +131,7 @@ trait RuleManagement $index = 0; foreach ($submittedOperators as $operator) { $rootOperator = OperatorQuerySearch::getRootOperator($operator['type']); - $needsContext = (bool)config(sprintf('search.operators.%s.needs_context', $rootOperator)); + $needsContext = (bool) config(sprintf('search.operators.%s.needs_context', $rootOperator)); try { $renderedEntries[] = view( @@ -163,8 +163,8 @@ trait RuleManagement $repository = app(RuleGroupRepositoryInterface::class); if (0 === $repository->count()) { $data = [ - 'title' => (string)trans('firefly.default_rule_group_name'), - 'description' => (string)trans('firefly.default_rule_group_description'), + 'title' => (string) trans('firefly.default_rule_group_name'), + 'description' => (string) trans('firefly.default_rule_group_description'), 'active' => true, ]; diff --git a/app/Support/Http/Controllers/UserNavigation.php b/app/Support/Http/Controllers/UserNavigation.php index 9a62ee092d..f9f2bc4091 100644 --- a/app/Support/Http/Controllers/UserNavigation.php +++ b/app/Support/Http/Controllers/UserNavigation.php @@ -49,7 +49,7 @@ trait UserNavigation final protected function getPreviousUrl(string $identifier): string { app('log')->debug(sprintf('Trying to retrieve URL stored under "%s"', $identifier)); - $url = (string)session($identifier); + $url = (string) session($identifier); app('log')->debug(sprintf('The URL is %s', $url)); return app('steam')->getSafeUrl($url, route('index')); diff --git a/app/Support/JsonApi/Concerns/UserGroupDetectable.php b/app/Support/JsonApi/Concerns/UserGroupDetectable.php index 3648053d77..e3c081bab2 100644 --- a/app/Support/JsonApi/Concerns/UserGroupDetectable.php +++ b/app/Support/JsonApi/Concerns/UserGroupDetectable.php @@ -44,10 +44,10 @@ trait UserGroupDetectable $userGroup = request()->route()?->parameter('userGroup'); if (null === $userGroup) { app('log')->debug('Request class has no userGroup parameter, but perhaps there is a parameter.'); - $userGroupId = (int)request()->get('user_group_id'); + $userGroupId = (int) request()->get('user_group_id'); if (0 === $userGroupId) { app('log')->debug(sprintf('Request class has no user_group_id parameter, grab default from user (group #%d).', $user->user_group_id)); - $userGroupId = (int)$user->user_group_id; + $userGroupId = (int) $user->user_group_id; } $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php index 940e528806..4fc240b83a 100644 --- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php @@ -44,17 +44,16 @@ use Illuminate\Support\Facades\Log; */ class AccountEnrichment implements EnrichmentInterface { - private Collection $collection; - private array $currencies; - private array $objectGroups; - private array $grouped; - private array $balances; - private TransactionCurrency $default; - private ?Carbon $start; - private ?Carbon $end; - - private AccountRepositoryInterface $repository; + private array $balances; + private Collection $collection; + private array $currencies; private CurrencyRepositoryInterface $currencyRepository; + private TransactionCurrency $default; + private ?Carbon $end; + private array $grouped; + private array $objectGroups; + private AccountRepositoryInterface $repository; + private ?Carbon $start; public function __construct() { @@ -64,6 +63,16 @@ class AccountEnrichment implements EnrichmentInterface $this->end = null; } + #[\Override] + public function enrichSingle(Model $model): Account + { + Log::debug(__METHOD__); + $collection = new Collection([$model]); + $collection = $this->enrich($collection); + + return $collection->first(); + } + #[\Override] /** * Do the actual enrichment. @@ -111,6 +120,53 @@ class AccountEnrichment implements EnrichmentInterface } } + /** + * TODO this method refers to a single-use method inside Steam that could be moved here. + */ + private function collectAccountTypes(): void + { + $accountTypes = $this->repository->getAccountTypes($this->collection); + $types = []; + + /** @var AccountType $row */ + foreach ($accountTypes as $row) { + $types[$row->id] = $row->type; + } + $this->collection->transform(function (Account $account) use ($types) { + $account->account_type_string = $types[$account->id]; + + return $account; + }); + } + + private function collectMetaData(): void + { + $metaFields = $this->repository->getMetaValues($this->collection, ['is_multi_currency', 'currency_id', 'account_role', 'account_number', 'liability_direction', 'interest', 'interest_period', 'current_debt']); + $currencyIds = $metaFields->where('name', 'currency_id')->pluck('data')->toArray(); + + $currencies = []; + foreach ($this->currencyRepository->getByIds($currencyIds) as $currency) { + $id = $currency->id; + $currencies[$id] = $currency; + } + + $this->collection->transform(function (Account $account) use ($metaFields, $currencies) { + $set = $metaFields->where('account_id', $account->id); + foreach ($set as $entry) { + $account->{$entry->name} = $entry->data; + if ('currency_id' === $entry->name) { + $id = (int) $entry->data; + $account->currency_name = $currencies[$id]?->name; + $account->currency_code = $currencies[$id]?->code; + $account->currency_symbol = $currencies[$id]?->symbol; + $account->currency_decimal_places = $currencies[$id]?->decimal_places; + } + } + + return $account; + }); + } + private function getMetaBalances(): void { $this->balances = Balance::getAccountBalances($this->collection, today()); @@ -181,73 +237,6 @@ class AccountEnrichment implements EnrichmentInterface }); } - /** - * TODO this method refers to a single-use method inside Steam that could be moved here. - */ - private function collectAccountTypes(): void - { - $accountTypes = $this->repository->getAccountTypes($this->collection); - $types = []; - - /** @var AccountType $row */ - foreach ($accountTypes as $row) { - $types[$row->id] = $row->type; - } - $this->collection->transform(function (Account $account) use ($types) { - $account->account_type_string = $types[$account->id]; - - return $account; - }); - } - - private function collectMetaData(): void - { - $metaFields = $this->repository->getMetaValues($this->collection, ['is_multi_currency', 'currency_id', 'account_role', 'account_number', 'liability_direction', 'interest', 'interest_period', 'current_debt']); - $currencyIds = $metaFields->where('name', 'currency_id')->pluck('data')->toArray(); - - $currencies = []; - foreach ($this->currencyRepository->getByIds($currencyIds) as $currency) { - $id = $currency->id; - $currencies[$id] = $currency; - } - - $this->collection->transform(function (Account $account) use ($metaFields, $currencies) { - $set = $metaFields->where('account_id', $account->id); - foreach ($set as $entry) { - $account->{$entry->name} = $entry->data; - if ('currency_id' === $entry->name) { - $id = (int) $entry->data; - $account->currency_name = $currencies[$id]?->name; - $account->currency_code = $currencies[$id]?->code; - $account->currency_symbol = $currencies[$id]?->symbol; - $account->currency_decimal_places = $currencies[$id]?->decimal_places; - } - } - - return $account; - }); - } - - #[\Override] - public function enrichSingle(Model $model): Account - { - Log::debug(__METHOD__); - $collection = new Collection([$model]); - $collection = $this->enrich($collection); - - return $collection->first(); - } - - public function setStart(?Carbon $start): void - { - $this->start = $start; - } - - public function setEnd(?Carbon $end): void - { - $this->end = $end; - } - private function getObjectGroups(): void { $set = \DB::table('object_groupables') @@ -279,4 +268,14 @@ class AccountEnrichment implements EnrichmentInterface return $account; }); } + + public function setEnd(?Carbon $end): void + { + $this->end = $end; + } + + public function setStart(?Carbon $start): void + { + $this->start = $start; + } } diff --git a/app/Support/JsonApi/ExpandsQuery.php b/app/Support/JsonApi/ExpandsQuery.php index a45c2c6ed3..2062eb9e7d 100644 --- a/app/Support/JsonApi/ExpandsQuery.php +++ b/app/Support/JsonApi/ExpandsQuery.php @@ -28,79 +28,11 @@ use FireflyIII\Models\Account; use FireflyIII\Support\Http\Api\AccountFilter; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Support\Facades\Log; -use LaravelJsonApi\Core\Query\FilterParameters; -use LaravelJsonApi\Core\Query\SortFields; trait ExpandsQuery { use AccountFilter; - final protected function addPagination(Builder $query, array $pagination): Builder - { - $skip = ($pagination['number'] - 1) * $pagination['size']; - - return $query->skip($skip)->take($pagination['size']); - } - - final protected function addSortParams(string $class, Builder $query, ?SortFields $sort): Builder - { - $config = config('api.valid_query_sort')[$class] ?? []; - if (null === $sort) { - return $query; - } - foreach ($sort->all() as $sortField) { - if (in_array($sortField->name(), $config, true)) { - $query->orderBy($sortField->name(), $sortField->isAscending() ? 'ASC' : 'DESC'); - } - } - - return $query; - } - - private function parseAccountTypeFilter(array $value): array - { - $return = []; - foreach ($value as $entry) { - $return = array_merge($return, $this->mapAccountTypes($entry)); - } - - return array_unique($return); - } - - private function parseAllFilters(string $class, FilterParameters $filters): array - { - $config = config('api.valid_api_filters')[$class]; - $parsed = []; - foreach ($filters->all() as $filter) { - $key = $filter->key(); - if (!in_array($key, $config, true)) { - continue; - } - // make array if not array: - $value = $filter->value(); - if (null === $value) { - continue; - } - if (!is_array($value)) { - $value = [$value]; - } - - switch ($filter->key()) { - case 'name': - $parsed['name'] = $value; - - break; - - case 'type': - $parsed['type'] = $this->parseAccountTypeFilter($value); - - break; - } - } - - return $parsed; - } - final protected function addFilterParams(string $class, Builder $query, ?FilterParameters $filters): Builder { Log::debug(__METHOD__); @@ -137,4 +69,70 @@ trait ExpandsQuery return $query; } + + private function parseAllFilters(string $class, FilterParameters $filters): array + { + $config = config('api.valid_api_filters')[$class]; + $parsed = []; + foreach ($filters->all() as $filter) { + $key = $filter->key(); + if (!in_array($key, $config, true)) { + continue; + } + // make array if not array: + $value = $filter->value(); + if (null === $value) { + continue; + } + if (!is_array($value)) { + $value = [$value]; + } + + switch ($filter->key()) { + case 'name': + $parsed['name'] = $value; + + break; + + case 'type': + $parsed['type'] = $this->parseAccountTypeFilter($value); + + break; + } + } + + return $parsed; + } + + private function parseAccountTypeFilter(array $value): array + { + $return = []; + foreach ($value as $entry) { + $return = array_merge($return, $this->mapAccountTypes($entry)); + } + + return array_unique($return); + } + + final protected function addPagination(Builder $query, array $pagination): Builder + { + $skip = ($pagination['number'] - 1) * $pagination['size']; + + return $query->skip($skip)->take($pagination['size']); + } + + final protected function addSortParams(string $class, Builder $query, ?SortFields $sort): Builder + { + $config = config('api.valid_query_sort')[$class] ?? []; + if (null === $sort) { + return $query; + } + foreach ($sort->all() as $sortField) { + if (in_array($sortField->name(), $config, true)) { + $query->orderBy($sortField->name(), $sortField->isAscending() ? 'ASC' : 'DESC'); + } + } + + return $query; + } } diff --git a/app/Support/JsonApi/SortsCollection.php b/app/Support/JsonApi/SortsCollection.php index 1a58c1a621..30f0d0315b 100644 --- a/app/Support/JsonApi/SortsCollection.php +++ b/app/Support/JsonApi/SortsCollection.php @@ -26,7 +26,6 @@ namespace FireflyIII\Support\JsonApi; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -use LaravelJsonApi\Core\Query\SortFields; trait SortsCollection { diff --git a/app/Support/JsonApi/SortsQueryResults.php b/app/Support/JsonApi/SortsQueryResults.php index d9bf99b666..17d8370405 100644 --- a/app/Support/JsonApi/SortsQueryResults.php +++ b/app/Support/JsonApi/SortsQueryResults.php @@ -27,8 +27,6 @@ namespace FireflyIII\Support\JsonApi; use FireflyIII\Models\Account; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -use LaravelJsonApi\Core\Query\SortField; -use LaravelJsonApi\Core\Query\SortFields; trait SortsQueryResults { @@ -82,8 +80,8 @@ trait SortsQueryResults if (Account::class === $class && 'last_activity' === $field->name()) { $ascending = $field->isAscending(); $collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int { - $leftNr = (int)$left->last_activity?->format('U'); - $rightNr = (int)$right->last_activity?->format('U'); + $leftNr = (int) $left->last_activity?->format('U'); + $rightNr = (int) $right->last_activity?->format('U'); if ($ascending) { return $leftNr <=> $rightNr; } diff --git a/app/Support/JsonApi/ValidateSortParameters.php b/app/Support/JsonApi/ValidateSortParameters.php index c2fda7ecf2..79ae9c461e 100644 --- a/app/Support/JsonApi/ValidateSortParameters.php +++ b/app/Support/JsonApi/ValidateSortParameters.php @@ -25,7 +25,6 @@ declare(strict_types=1); namespace FireflyIII\Support\JsonApi; use Illuminate\Support\Facades\Log; -use LaravelJsonApi\Core\Query\SortFields; trait ValidateSortParameters { diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php index f807a0a0f8..dab18b9699 100644 --- a/app/Support/Models/AccountBalanceCalculator.php +++ b/app/Support/Models/AccountBalanceCalculator.php @@ -61,68 +61,6 @@ class AccountBalanceCalculator $object->optimizedCalculation(new Collection()); } - public static function recalculateForJournal(TransactionJournal $transactionJournal): void - { - Log::debug(__METHOD__); - $object = new self(); - - // recalculate the involved accounts: - $accounts = new Collection(); - foreach ($transactionJournal->transactions as $transaction) { - $accounts->push($transaction->account); - } - $object->optimizedCalculation($accounts, $transactionJournal->date); - } - - private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string - { - if (null === $notBefore) { - return '0'; - } - Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d'))); - $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNull('transactions.deleted_at') - ->where('transaction_journals.transaction_currency_id', $currencyId) - ->whereNull('transaction_journals.deleted_at') - // this order is the same as GroupCollector - ->orderBy('transaction_journals.date', 'DESC') - ->orderBy('transaction_journals.order', 'ASC') - ->orderBy('transaction_journals.id', 'DESC') - ->orderBy('transaction_journals.description', 'DESC') - ->orderBy('transactions.amount', 'DESC') - ->where('transactions.account_id', $accountId) - ; - $notBefore->startOfDay(); - $query->where('transaction_journals.date', '<', $notBefore); - - $first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']); - $balance = (string) ($first->balance_after ?? '0'); - Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0)); - - return $balance; - } - - private function getAccountBalanceByAccount(int $account, int $currency): AccountBalance - { - $query = AccountBalance::where('title', 'balance')->where('account_id', $account)->where('transaction_currency_id', $currency); - - $entry = $query->first(); - if (null !== $entry) { - // Log::debug(sprintf('Found account balance "balance" for account #%d and currency #%d: %s', $account, $currency, $entry->balance)); - - return $entry; - } - $entry = new AccountBalance(); - $entry->title = 'balance'; - $entry->account_id = $account; - $entry->transaction_currency_id = $currency; - $entry->balance = '0'; - $entry->save(); - // Log::debug(sprintf('Created new account balance for account #%d and currency #%d: %s', $account, $currency, $entry->balance)); - - return $entry; - } - private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void { Log::debug('start of optimizedCalculation'); @@ -183,23 +121,32 @@ class AccountBalanceCalculator $this->storeAccountBalances($balances); } - private function getAccountBalanceByJournal(string $title, int $account, int $journal, int $currency): AccountBalance + private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string { - $query = AccountBalance::where('title', $title)->where('account_id', $account)->where('transaction_journal_id', $journal)->where('transaction_currency_id', $currency); - - $entry = $query->first(); - if (null !== $entry) { - return $entry; + if (null === $notBefore) { + return '0'; } - $entry = new AccountBalance(); - $entry->title = $title; - $entry->account_id = $account; - $entry->transaction_journal_id = $journal; - $entry->transaction_currency_id = $currency; - $entry->balance = '0'; - $entry->save(); + Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d'))); + $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->whereNull('transactions.deleted_at') + ->where('transaction_journals.transaction_currency_id', $currencyId) + ->whereNull('transaction_journals.deleted_at') + // this order is the same as GroupCollector + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transaction_journals.order', 'ASC') + ->orderBy('transaction_journals.id', 'DESC') + ->orderBy('transaction_journals.description', 'DESC') + ->orderBy('transactions.amount', 'DESC') + ->where('transactions.account_id', $accountId) + ; + $notBefore->startOfDay(); + $query->where('transaction_journals.date', '<', $notBefore); - return $entry; + $first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']); + $balance = (string) ($first->balance_after ?? '0'); + Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0)); + + return $balance; } private function storeAccountBalances(array $balances): void @@ -243,8 +190,61 @@ class AccountBalanceCalculator $object->balance = $balance[0]; $object->date = $balance[1]; $object->date_tz = $balance[1]?->format('e'); - $object->save(); + $object->saveQuietly(); } } } + + public static function recalculateForJournal(TransactionJournal $transactionJournal): void + { + Log::debug(__METHOD__); + $object = new self(); + + // recalculate the involved accounts: + $accounts = new Collection(); + foreach ($transactionJournal->transactions as $transaction) { + $accounts->push($transaction->account); + } + $object->optimizedCalculation($accounts, $transactionJournal->date); + } + + private function getAccountBalanceByAccount(int $account, int $currency): AccountBalance + { + $query = AccountBalance::where('title', 'balance')->where('account_id', $account)->where('transaction_currency_id', $currency); + + $entry = $query->first(); + if (null !== $entry) { + // Log::debug(sprintf('Found account balance "balance" for account #%d and currency #%d: %s', $account, $currency, $entry->balance)); + + return $entry; + } + $entry = new AccountBalance(); + $entry->title = 'balance'; + $entry->account_id = $account; + $entry->transaction_currency_id = $currency; + $entry->balance = '0'; + $entry->save(); + // Log::debug(sprintf('Created new account balance for account #%d and currency #%d: %s', $account, $currency, $entry->balance)); + + return $entry; + } + + private function getAccountBalanceByJournal(string $title, int $account, int $journal, int $currency): AccountBalance + { + $query = AccountBalance::where('title', $title)->where('account_id', $account)->where('transaction_journal_id', $journal)->where('transaction_currency_id', $currency); + + $entry = $query->first(); + if (null !== $entry) { + return $entry; + } + $entry = new AccountBalance(); + $entry->title = $title; + $entry->account_id = $account; + $entry->transaction_journal_id = $journal; + $entry->transaction_currency_id = $currency; + $entry->balance = '0'; + $entry->save(); + + return $entry; + } } diff --git a/app/Support/Models/ReturnsIntegerIdTrait.php b/app/Support/Models/ReturnsIntegerIdTrait.php index 3432772855..4b1fe3affc 100644 --- a/app/Support/Models/ReturnsIntegerIdTrait.php +++ b/app/Support/Models/ReturnsIntegerIdTrait.php @@ -39,7 +39,7 @@ trait ReturnsIntegerIdTrait protected function id(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Support/Models/ReturnsIntegerUserIdTrait.php b/app/Support/Models/ReturnsIntegerUserIdTrait.php index 3f1808923d..a0d2bc79e9 100644 --- a/app/Support/Models/ReturnsIntegerUserIdTrait.php +++ b/app/Support/Models/ReturnsIntegerUserIdTrait.php @@ -37,14 +37,14 @@ trait ReturnsIntegerUserIdTrait protected function userGroupId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } protected function userId(): Attribute { return Attribute::make( - get: static fn ($value) => (int)$value, + get: static fn ($value) => (int) $value, ); } } diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index f048ae0f68..9aa6f13b0f 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -161,7 +161,7 @@ class Navigation public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon { $date = clone $theDate; - Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); + // Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); $functionMap = [ '1D' => 'startOfDay', 'daily' => 'startOfDay', @@ -186,25 +186,25 @@ class Navigation if (array_key_exists($repeatFreq, $functionMap)) { $function = $functionMap[$repeatFreq]; - Log::debug(sprintf('Function is ->%s()', $function)); + // Log::debug(sprintf('Function is ->%s()', $function)); if (array_key_exists($function, $parameterMap)) { - Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); + // Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); $date->{$function}($parameterMap[$function][0]); // @phpstan-ignore-line - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $date; } $date->{$function}(); // @phpstan-ignore-line - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $date; } if ('half-year' === $repeatFreq || '6M' === $repeatFreq) { $skipTo = $date->month > 7 ? 6 : 0; $date->startOfYear()->addMonths($skipTo); - Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + // Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $date; } @@ -220,13 +220,13 @@ class Navigation default => null, }; if (null !== $result) { - Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); return $result; } if ('custom' === $repeatFreq) { - Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); + // Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); return $date; // the date is already at the start. } @@ -238,7 +238,7 @@ class Navigation public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon { $currentEnd = clone $end; - Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq)); + // Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq)); $functionMap = [ '1D' => 'endOfDay', @@ -327,7 +327,7 @@ class Navigation if (in_array($repeatFreq, $subDay, true)) { $currentEnd->subDay(); } - Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String())); + // Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String())); return $currentEnd; } @@ -507,12 +507,12 @@ class Navigation $diff = $start->diffInMonths($end, true); Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff)); if ($diff >= 1.001) { - Log::debug(sprintf('Return Y-m because %s', $diff)); + // Log::debug(sprintf('Return Y-m because %s', $diff)); $format = 'Y-m'; } if ($diff >= 12.001) { - Log::debug(sprintf('Return Y because %s', $diff)); + // Log::debug(sprintf('Return Y because %s', $diff)); $format = 'Y'; } diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php index e747f81175..9919f9fb0e 100644 --- a/app/Support/ParseDateString.php +++ b/app/Support/ParseDateString.php @@ -112,7 +112,7 @@ class ParseDateString return new Carbon('1984-09-17'); } // maybe a year, nothing else? - if (4 === strlen($date) && is_numeric($date) && (int)$date > 1000 && (int)$date <= 3000) { + if (4 === strlen($date) && is_numeric($date) && (int) $date > 1000 && (int) $date <= 3000) { return new Carbon(sprintf('%d-01-01', $date)); } @@ -190,7 +190,7 @@ class ParseDateString } $direction = str_starts_with($part, '+') ? 1 : 0; $period = $part[strlen($part) - 1]; - $number = (int)substr($part, 1, -1); + $number = (int) substr($part, 1, -1); if (!array_key_exists($period, $functions[$direction])) { app('log')->error(sprintf('No method for direction %d and period "%s".', $direction, $period)); diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index 86fb043e51..3fbde6d2de 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -26,9 +26,12 @@ namespace FireflyIII\Support; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Preference; use FireflyIII\User; +use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Contracts\Encryption\EncryptException; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Session; /** @@ -95,7 +98,7 @@ class Preferences $groupId = null; $items = config('firefly.admin_specific_prefs') ?? []; if (in_array($preferenceName, $items, true)) { - $groupId = (int)$user->user_group_id; + $groupId = (int) $user->user_group_id; } return $groupId; @@ -123,6 +126,8 @@ class Preferences { $fullName = sprintf('preference%s%s', $user->id, $name); $groupId = $this->getUserGroupId($user, $name); + $groupId = 0 === (int) $groupId ? null : (int) $groupId; + Cache::forget($fullName); /** @var null|Preference $pref */ @@ -138,9 +143,10 @@ class Preferences } if (null === $pref) { $pref = new Preference(); - $pref->user_id = (int)$user->id; + $pref->user_id = (int) $user->id; $pref->user_group_id = $groupId; $pref->name = $name; + } $pref->data = $value; $pref->save(); @@ -189,6 +195,50 @@ class Preferences return $result; } + public function getEncrypted(string $name, $default = null): ?Preference + { + $result = $this->get($name, $default); + if (null === $result) { + return null; + } + if ('' === $result->data) { + Log::warning(sprintf('Empty encrypted preference found: "%s"', $name)); + + return $result; + } + + try { + $result->data = decrypt($result->data); + } catch (DecryptException $e) { + Log::error(sprintf('Could not decrypt preference "%s": %s', $name, $e->getMessage())); + + return $result; + } + + return $result; + } + + public function getEncryptedForUser(User $user, string $name, null|array|bool|int|string $default = null): ?Preference + { + $result = $this->getForUser($user, $name, $default); + if ('' === $result->data) { + Log::warning(sprintf('Empty encrypted preference found: "%s"', $name)); + + return $result; + } + + try { + $result->data = decrypt($result->data); + } catch (DecryptException $e) { + Log::error(sprintf('Could not decrypt preference "%s": %s', $name, $e->getMessage())); + + return $result; + } + + + return $result; + } + public function getFresh(string $name, null|array|bool|int|string $default = null): ?Preference { /** @var null|User $user */ @@ -218,7 +268,7 @@ class Preferences $lastActivity = implode(',', $lastActivity); } - return hash('sha256', (string)$lastActivity); + return hash('sha256', (string) $lastActivity); } public function mark(): void @@ -242,4 +292,17 @@ class Preferences return $this->setForUser($user, $name, $value); } + + public function setEncrypted(string $name, mixed $value): Preference + { + try { + $encrypted = encrypt($value); + } catch (EncryptException $e) { + Log::error(sprintf('Could not encrypt preference "%s": %s', $name, $e->getMessage())); + + throw new FireflyException(sprintf('Could not encrypt preference "%s". Cowardly refuse to continue.', $name)); + } + + return $this->set($name, $encrypted); + } } diff --git a/app/Support/Report/Budget/BudgetReportGenerator.php b/app/Support/Report/Budget/BudgetReportGenerator.php index 7059886943..2de00d88c0 100644 --- a/app/Support/Report/Budget/BudgetReportGenerator.php +++ b/app/Support/Report/Budget/BudgetReportGenerator.php @@ -106,8 +106,8 @@ class BudgetReportGenerator */ private function processBudgetExpenses(array $expenses, array $budget): void { - $budgetId = (int)$budget['id']; - $currencyId = (int)$expenses['currency_id']; + $budgetId = (int) $budget['id']; + $currencyId = (int) $expenses['currency_id']; foreach ($budget['transaction_journals'] as $journal) { $sourceAccountId = $journal['source_account_id']; @@ -245,7 +245,7 @@ class BudgetReportGenerator $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts); foreach ($noBudget as $noBudgetEntry) { // currency information: - $nbCurrencyId = (int)($noBudgetEntry['currency_id'] ?? $this->currency->id); + $nbCurrencyId = (int) ($noBudgetEntry['currency_id'] ?? $this->currency->id); $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code; $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name; $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol; @@ -290,9 +290,9 @@ class BudgetReportGenerator // make percentages based on total amount. foreach ($this->report['budgets'] as $budgetId => $data) { foreach ($data['budget_limits'] as $limitId => $entry) { - $budgetId = (int)$budgetId; - $limitId = (int)$limitId; - $currencyId = (int)$entry['currency_id']; + $budgetId = (int) $budgetId; + $limitId = (int) $limitId; + $currencyId = (int) $entry['currency_id']; $spent = $entry['spent']; $totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0'; $spentPct = '0'; @@ -301,10 +301,10 @@ class BudgetReportGenerator $budgetedPct = '0'; if (0 !== bccomp($spent, '0') && 0 !== bccomp($totalSpent, '0')) { - $spentPct = round((float)bcmul(bcdiv($spent, $totalSpent), '100')); + $spentPct = round((float) bcmul(bcdiv($spent, $totalSpent), '100')); } if (0 !== bccomp($budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) { - $budgetedPct = round((float)bcmul(bcdiv($budgeted, $totalBudgeted), '100')); + $budgetedPct = round((float) bcmul(bcdiv($budgeted, $totalBudgeted), '100')); } $this->report['sums'][$currencyId]['budgeted'] ??= '0'; $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct; diff --git a/app/Support/Report/Summarizer/TransactionSummarizer.php b/app/Support/Report/Summarizer/TransactionSummarizer.php new file mode 100644 index 0000000000..7e74cd09f0 --- /dev/null +++ b/app/Support/Report/Summarizer/TransactionSummarizer.php @@ -0,0 +1,180 @@ +setUser($user); + } + } + + public function setUser(User $user): void + { + $this->user = $user; + $this->default = Amount::getDefaultCurrencyByUserGroup($user->userGroup); + $this->convertToNative = Amount::convertToNative($user); + } + + public function groupByCurrencyId(array $journals, string $method = 'negative'): array + { + $array = []; + foreach ($journals as $journal) { + $field = 'amount'; + + // grab default currency information. + $currencyId = (int) $journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + if ($this->convertToNative) { + // if convert to native, use the native amount yes or no? + $useNative = $this->default->id !== (int) $journal['currency_id']; + $useForeign = $this->default->id === (int) $journal['foreign_currency_id']; + if ($useNative) { + Log::debug(sprintf('Journal #%d switches to native amount (original is %s)', $journal['transaction_journal_id'], $journal['currency_code'])); + $field = 'native_amount'; + $currencyId = $this->default->id; + $currencyName = $this->default->name; + $currencySymbol = $this->default->symbol; + $currencyCode = $this->default->code; + $currencyDecimalPlaces = $this->default->decimal_places; + } + if ($useForeign) { + Log::debug(sprintf('Journal #%d switches to foreign amount (foreign is %s)', $journal['transaction_journal_id'], $journal['foreign_currency_code'])); + $field = 'foreign_amount'; + $currencyId = (int) $journal['foreign_currency_id']; + $currencyName = $journal['foreign_currency_name']; + $currencySymbol = $journal['foreign_currency_symbol']; + $currencyCode = $journal['foreign_currency_code']; + $currencyDecimalPlaces = $journal['foreign_currency_decimal_places']; + } + } + if (!$this->convertToNative) { + // default to the normal amount, but also + } + $amount = (string) ($journal[$field] ?? '0'); + $array[$currencyId] ??= [ + 'sum' => '0', + 'currency_id' => $currencyId, + 'currency_name' => $currencyName, + 'currency_symbol' => $currencySymbol, + 'currency_code' => $currencyCode, + 'currency_decimal_places' => $currencyDecimalPlaces, + ]; + $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->{$method}($amount)); + // Log::debug(sprintf('Journal #%d adds amount %s %s', $journal['transaction_journal_id'], $currencyCode, $amount)); + } + Log::debug('End of sumExpenses.', $array); + + return $array; + } + + public function groupByDirection(array $journals, string $method, string $direction): array + { + + $array = []; + $idKey = sprintf('%s_account_id', $direction); + $nameKey = sprintf('%s_account_name', $direction); + $convertToNative = Amount::convertToNative($this->user); + $default = Amount::getDefaultCurrencyByUserGroup($this->user->userGroup); + + + + + + Log::debug(sprintf('groupByDirection(array, %s, %s).', $direction, $method)); + foreach ($journals as $journal) { + // currency + $currencyId = $journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; + $field = $convertToNative && $currencyId !== $default->id ? 'native_amount' : 'amount'; + + // perhaps use default currency instead? + if ($convertToNative && $journal['currency_id'] !== $default->id) { + $currencyId = $default->id; + $currencyName = $default->name; + $currencySymbol = $default->symbol; + $currencyCode = $default->code; + $currencyDecimalPlaces = $default->decimal_places; + } + // use foreign amount when the foreign currency IS the default currency. + if ($convertToNative && $journal['currency_id'] !== $default->id && $default->id === $journal['foreign_currency_id']) { + $field = 'foreign_amount'; + } + $key = sprintf('%s-%s', $journal[$idKey], $currencyId); + // sum it all up or create a new array. + $array[$key] ??= [ + 'id' => $journal[$idKey], + 'name' => $journal[$nameKey], + 'sum' => '0', + 'currency_id' => $currencyId, + 'currency_name' => $currencyName, + 'currency_symbol' => $currencySymbol, + 'currency_code' => $currencyCode, + 'currency_decimal_places' => $currencyDecimalPlaces, + ]; + + // add the data from the $field to the array. + $array[$key]['sum'] = bcadd($array[$key]['sum'], app('steam')->{$method}((string) ($journal[$field] ?? '0'))); // @phpstan-ignore-line + Log::debug(sprintf('Field for transaction #%d is "%s" (%s). Sum: %s', $journal['transaction_group_id'], $currencyCode, $field, $array[$key]['sum'])); + + // also do foreign amount, but only when convertToNative is false (otherwise we have it already) + // or when convertToNative is true and the foreign currency is ALSO not the default currency. + if ((!$convertToNative || $journal['foreign_currency_id'] !== $default->id) && 0 !== (int) $journal['foreign_currency_id']) { + Log::debug(sprintf('Use foreign amount from transaction #%d: %s %s. Sum: %s', $journal['transaction_group_id'], $currencyCode, $journal['foreign_amount'], $array[$key]['sum'])); + $key = sprintf('%s-%s', $journal[$idKey], $journal['foreign_currency_id']); + $array[$key] ??= [ + 'id' => $journal[$idKey], + 'name' => $journal[$nameKey], + 'sum' => '0', + 'currency_id' => $journal['foreign_currency_id'], + 'currency_name' => $journal['foreign_currency_name'], + 'currency_symbol' => $journal['foreign_currency_symbol'], + 'currency_code' => $journal['foreign_currency_code'], + 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], + ]; + $array[$key]['sum'] = bcadd($array[$key]['sum'], app('steam')->{$method}((string) $journal['foreign_amount'])); // @phpstan-ignore-line + } + } + + return $array; + } +} diff --git a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php index df73a72f60..439fd1f50a 100644 --- a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php @@ -58,7 +58,7 @@ trait CalculateRangeOccurrences { $return = []; $attempts = 0; - $dayOfMonth = (int)$moment; + $dayOfMonth = (int) $moment; if ($start->day > $dayOfMonth) { // day has passed already, add a month. $start->addMonth(); @@ -113,7 +113,7 @@ trait CalculateRangeOccurrences app('log')->debug('Rep is weekly.'); // monday = 1 // sunday = 7 - $dayOfWeek = (int)$moment; + $dayOfWeek = (int) $moment; app('log')->debug(sprintf('DoW in repetition is %d, in mutator is %d', $dayOfWeek, $start->dayOfWeekIso)); if ($start->dayOfWeekIso > $dayOfWeek) { // day has already passed this week, add one week: diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrences.php b/app/Support/Repositories/Recurring/CalculateXOccurrences.php index f31171810f..04f07046d4 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrences.php @@ -63,7 +63,7 @@ trait CalculateXOccurrences $mutator = clone $date; $total = 0; $attempts = 0; - $dayOfMonth = (int)$moment; + $dayOfMonth = (int) $moment; if ($mutator->day > $dayOfMonth) { // day has passed already, add a month. $mutator->addMonth(); @@ -127,7 +127,7 @@ trait CalculateXOccurrences // monday = 1 // sunday = 7 $mutator->addDay(); // always assume today has passed. - $dayOfWeek = (int)$moment; + $dayOfWeek = (int) $moment; if ($mutator->dayOfWeekIso > $dayOfWeek) { // day has already passed this week, add one week: $mutator->addWeek(); diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php index 4b766a0f47..45a246f9cc 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php @@ -67,7 +67,7 @@ trait CalculateXOccurrencesSince $mutator = clone $date; $total = 0; $attempts = 0; - $dayOfMonth = (int)$moment; + $dayOfMonth = (int) $moment; $dayOfMonth = 0 === $dayOfMonth ? 1 : $dayOfMonth; if ($mutator->day > $dayOfMonth) { app('log')->debug(sprintf('%d is after %d, add a month. Mutator is now', $mutator->day, $dayOfMonth)); @@ -143,7 +143,7 @@ trait CalculateXOccurrencesSince // sunday = 7 // Removed assumption today has passed, see issue https://github.com/firefly-iii/firefly-iii/issues/4798 // $mutator->addDay(); // always assume today has passed. - $dayOfWeek = (int)$moment; + $dayOfWeek = (int) $moment; if ($mutator->dayOfWeekIso > $dayOfWeek) { // day has already passed this week, add one week: $mutator->addWeek(); diff --git a/app/Support/Request/AppendsLocationData.php b/app/Support/Request/AppendsLocationData.php index c1cffa5e5d..4329f865bb 100644 --- a/app/Support/Request/AppendsLocationData.php +++ b/app/Support/Request/AppendsLocationData.php @@ -48,14 +48,14 @@ trait AppendsLocationData private function validLongitude(string $longitude): bool { - $number = (float)$longitude; + $number = (float) $longitude; return $number >= -180 && $number <= 180; } private function validLatitude(string $latitude): bool { - $number = (float)$latitude; + $number = (float) $latitude; return $number >= -90 && $number <= 90; } diff --git a/app/Support/Request/ChecksLogin.php b/app/Support/Request/ChecksLogin.php index 0f9753ee62..9fd2e11883 100644 --- a/app/Support/Request/ChecksLogin.php +++ b/app/Support/Request/ChecksLogin.php @@ -86,10 +86,10 @@ trait ChecksLogin $userGroup = $this->route()?->parameter('userGroup'); if (null === $userGroup) { app('log')->debug('Request class has no userGroup parameter, but perhaps there is a parameter.'); - $userGroupId = (int)$this->get('user_group_id'); + $userGroupId = (int) $this->get('user_group_id'); if (0 === $userGroupId) { app('log')->debug(sprintf('Request class has no user_group_id parameter, grab default from user (group #%d).', $user->user_group_id)); - $userGroupId = (int)$user->user_group_id; + $userGroupId = (int) $user->user_group_id; } $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index ecca196381..62bd8beef1 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -89,36 +89,11 @@ trait ConvertsDataTypes "\r", // carriage return ]; - /** - * Return integer value. - */ - public function convertInteger(string $field): int + public function clearIban(?string $string): ?string { - return (int)$this->get($field); - } + $string = $this->clearString($string); - /** - * Abstract method that always exists in the Request classes that use this - * trait, OR a stub needs to be added by any other class that uses this train. - */ - abstract public function get(string $key, mixed $default = null): mixed; - - /** - * Return string value. - */ - public function convertString(string $field, string $default = ''): string - { - $entry = $this->get($field); - if (!is_scalar($entry)) { - return $default; - } - - return (string)$this->clearString((string)$entry); - } - - public function convertIban(string $field): string - { - return Steam::filterSpaces($this->convertString($field)); + return Steam::filterSpaces($string); } public function clearString(?string $string): ?string @@ -138,13 +113,6 @@ trait ConvertsDataTypes return trim($string); } - public function clearIban(?string $string): ?string - { - $string = $this->clearString($string); - - return Steam::filterSpaces($string); - } - public function clearStringKeepNewlines(?string $string): ?string { if (null === $string) { @@ -158,7 +126,39 @@ trait ConvertsDataTypes // clear zalgo text (TODO also in API v2) $string = preg_replace('/(\pM{2})\pM+/u', '\1', $string); - return trim((string)$string); + return trim((string) $string); + } + + public function convertIban(string $field): string + { + return Steam::filterSpaces($this->convertString($field)); + } + + /** + * Return string value. + */ + public function convertString(string $field, string $default = ''): string + { + $entry = $this->get($field); + if (!is_scalar($entry)) { + return $default; + } + + return (string) $this->clearString((string) $entry); + } + + /** + * Abstract method that always exists in the Request classes that use this + * trait, OR a stub needs to be added by any other class that uses this train. + */ + abstract public function get(string $key, mixed $default = null): mixed; + + /** + * Return integer value. + */ + public function convertInteger(string $field): int + { + return (int) $this->get($field); } /** @@ -183,7 +183,7 @@ trait ConvertsDataTypes $collection = new Collection(); if (is_array($set)) { foreach ($set as $accountId) { - $account = $repository->find((int)$accountId); + $account = $repository->find((int) $accountId); if (null !== $account) { $collection->push($account); } @@ -198,7 +198,7 @@ trait ConvertsDataTypes */ public function stringWithNewlines(string $field): string { - return (string)$this->clearStringKeepNewlines((string)($this->get($field) ?? '')); + return (string) $this->clearStringKeepNewlines((string) ($this->get($field) ?? '')); } /** @@ -242,7 +242,7 @@ trait ConvertsDataTypes protected function convertDateTime(?string $string): ?Carbon { - $value = $this->get((string)$string); + $value = $this->get((string) $string); if (null === $value) { return null; } @@ -297,7 +297,7 @@ trait ConvertsDataTypes return null; } - return (float)$res; + return (float) $res; } protected function dateFromValue(?string $string): ?Carbon @@ -360,7 +360,7 @@ trait ConvertsDataTypes $result = null; try { - $result = '' !== (string)$this->get($field) ? new Carbon((string)$this->get($field), config('app.timezone')) : null; + $result = '' !== (string) $this->get($field) ? new Carbon((string) $this->get($field), config('app.timezone')) : null; } catch (InvalidFormatException $e) { // @ignoreException } @@ -383,7 +383,7 @@ trait ConvertsDataTypes return null; } - return (int)$string; + return (int) $string; } /** @@ -395,11 +395,11 @@ trait ConvertsDataTypes return null; } - $value = (string)$this->get($field); + $value = (string) $this->get($field); if ('' === $value) { return null; } - return (int)$value; + return (int) $value; } } diff --git a/app/Support/Request/GetRecurrenceData.php b/app/Support/Request/GetRecurrenceData.php index 40f23738dd..50dc0cd8f2 100644 --- a/app/Support/Request/GetRecurrenceData.php +++ b/app/Support/Request/GetRecurrenceData.php @@ -38,12 +38,12 @@ trait GetRecurrenceData foreach ($stringKeys as $key) { if (array_key_exists($key, $transaction)) { - $return[$key] = (string)$transaction[$key]; + $return[$key] = (string) $transaction[$key]; } } foreach ($intKeys as $key) { if (array_key_exists($key, $transaction)) { - $return[$key] = (int)$transaction[$key]; + $return[$key] = (int) $transaction[$key]; } } foreach ($keys as $key) { diff --git a/app/Support/Search/AccountSearch.php b/app/Support/Search/AccountSearch.php index 1cdc88e430..6ff1a42d0c 100644 --- a/app/Support/Search/AccountSearch.php +++ b/app/Support/Search/AccountSearch.php @@ -90,7 +90,7 @@ class AccountSearch implements GenericSearchInterface break; case self::SEARCH_ID: - $searchQuery->where('accounts.id', '=', (int)$originalQuery); + $searchQuery->where('accounts.id', '=', (int) $originalQuery); break; diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index aa69994a8e..8683d4e289 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -201,7 +201,7 @@ class OperatorQuerySearch implements SearchInterface case Emoticon::class: case Emoji::class: case Mention::class: - $allWords = (string)$searchNode->getValue(); + $allWords = (string) $searchNode->getValue(); app('log')->debug(sprintf('Add words "%s" to search string, because Node class is "%s"', $allWords, $class)); $this->words[] = $allWords; @@ -231,11 +231,11 @@ class OperatorQuerySearch implements SearchInterface // must be valid operator: if ( in_array($operator, $this->validOperators, true) - && $this->updateCollector($operator, (string)$value, $prohibited)) { + && $this->updateCollector($operator, (string) $value, $prohibited)) { $this->operators->push( [ 'type' => self::getRootOperator($operator), - 'value' => (string)$value, + 'value' => (string) $value, 'prohibited' => $prohibited, ] ); @@ -245,7 +245,7 @@ class OperatorQuerySearch implements SearchInterface app('log')->debug(sprintf('Added INVALID operator type "%s"', $operator)); $this->invalidOperators[] = [ 'type' => $operator, - 'value' => (string)$value, + 'value' => (string) $value, ]; } } @@ -445,7 +445,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'source_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int) $value); if (null !== $account) { $this->collector->setSourceAccounts(new Collection([$account])); } @@ -457,7 +457,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-source_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int) $value); if (null !== $account) { $this->collector->excludeSourceAccounts(new Collection([$account])); } @@ -573,7 +573,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'destination_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int) $value); if (null !== $account) { $this->collector->setDestinationAccounts(new Collection([$account])); } @@ -584,7 +584,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-destination_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int) $value); if (null !== $account) { $this->collector->excludeDestinationAccounts(new Collection([$account])); } @@ -598,7 +598,7 @@ class OperatorQuerySearch implements SearchInterface $parts = explode(',', $value); $collection = new Collection(); foreach ($parts as $accountId) { - $account = $this->accountRepository->find((int)$accountId); + $account = $this->accountRepository->find((int) $accountId); if (null !== $account) { $collection->push($account); } @@ -616,7 +616,7 @@ class OperatorQuerySearch implements SearchInterface $parts = explode(',', $value); $collection = new Collection(); foreach ($parts as $accountId) { - $account = $this->accountRepository->find((int)$accountId); + $account = $this->accountRepository->find((int) $accountId); if (null !== $account) { $collection->push($account); } @@ -1822,6 +1822,74 @@ class OperatorQuerySearch implements SearchInterface case 'sepa_ct_is': $this->collector->setSepaCT($value); + break; + + case 'source_balance_gte': + case '-source_balance_lt': + $this->collector->accountBalanceIs('source', '>=', $value); + + break; + + case '-source_balance_gte': + case 'source_balance_lt': + $this->collector->accountBalanceIs('source', '<', $value); + + break; + + case 'source_balance_gt': + case '-source_balance_lte': + $this->collector->accountBalanceIs('source', '>', $value); + + break; + + case '-source_balance_gt': + case 'source_balance_lte': + $this->collector->accountBalanceIs('source', '<=', $value); + + break; + + case 'source_balance_is': + $this->collector->accountBalanceIs('source', '==', $value); + + break; + + case '-source_balance_is': + $this->collector->accountBalanceIs('source', '!=', $value); + + break; + + case 'destination_balance_gte': + case '-destination_balance_lt': + $this->collector->accountBalanceIs('destination', '>=', $value); + + break; + + case '-destination_balance_gte': + case 'destination_balance_lt': + $this->collector->accountBalanceIs('destination', '<', $value); + + break; + + case 'destination_balance_gt': + case '-destination_balance_lte': + $this->collector->accountBalanceIs('destination', '>', $value); + + break; + + case '-destination_balance_gt': + case 'destination_balance_lte': + $this->collector->accountBalanceIs('destination', '<=', $value); + + break; + + case 'destination_balance_is': + $this->collector->accountBalanceIs('destination', '==', $value); + + break; + + case '-destination_balance_is': + $this->collector->accountBalanceIs('destination', '!=', $value); + break; } @@ -2003,7 +2071,7 @@ class OperatorQuerySearch implements SearchInterface $filtered = $accounts->filter( static function (Account $account) use ($value, $stringMethod) { // either IBAN or account number - $ibanMatch = $stringMethod(strtolower((string)$account->iban), strtolower($value)); + $ibanMatch = $stringMethod(strtolower((string) $account->iban), strtolower($value)); $accountNrMatch = false; /** @var AccountMeta $meta */ @@ -2777,7 +2845,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->setUser($user); $this->collector->withAccountInformation()->withCategoryInformation()->withBudgetInformation(); - $this->setLimit((int)app('preferences')->getForUser($user, 'listPageSize', 50)->data); + $this->setLimit((int) app('preferences')->getForUser($user, 'listPageSize', 50)->data); } public function setLimit(int $limit): void diff --git a/app/Support/Steam.php b/app/Support/Steam.php index a08d243652..d6e9e96b9a 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -24,55 +24,38 @@ declare(strict_types=1); namespace FireflyIII\Support; use Carbon\Carbon; -use Carbon\Exceptions\InvalidFormatException; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; -use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Support\Http\Api\ExchangeRateConverter; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use FireflyIII\Support\Facades\Amount; /** * Class Steam. */ class Steam { - /** - * @deprecated - */ - public function balanceIgnoreVirtual(Account $account, Carbon $date): string + public function getAccountCurrency(Account $account): ?TransactionCurrency { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($account->user); + $type = $account->accountType->type; + $list = config('firefly.valid_currency_account_types'); - $currencyId = (int) $repository->getMetaValue($account, 'currency_id'); - $transactions = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', $currencyId) - ->get(['transactions.amount'])->toArray() - ; - $nativeBalance = $this->sumTransactions($transactions, 'amount'); + // return null if not in this list. + if (!in_array($type, $list, true)) { + return null; + } + $result = $account->accountMeta->where('name', 'currency_id')->first(); + if (null === $result) { + return null; + } - // get all balances in foreign currency: - $transactions = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.foreign_currency_id', $currencyId) - ->where('transactions.transaction_currency_id', '!=', $currencyId) - ->get(['transactions.foreign_amount'])->toArray() - ; - - $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); - - return bcadd($nativeBalance, $foreignBalance); + return TransactionCurrency::find((int) $result->data); } - public function sumTransactions(array $transactions, string $key): string + private function sumTransactions(array $transactions, string $key): string { $sum = '0'; @@ -81,52 +64,58 @@ class Steam $value = (string) ($transaction[$key] ?? '0'); $value = '' === $value ? '0' : $value; $sum = bcadd($sum, $value); + // Log::debug(sprintf('Add value from "%s": %s', $key, $value)); } + Log::debug(sprintf('Sum of "%s"-fields is %s', $key, $sum)); return $sum; } - /** - * Gets the balance for the given account during the whole range, using this format:. - * - * [yyyy-mm-dd] => 123,2 - * - * @throws FireflyException - */ - public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array + public function finalAccountBalanceInRange(Account $account, Carbon $start, Carbon $end, bool $convertToNative): array { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); + // expand period. + $start->subDay()->startOfDay(); + $end->addDay()->endOfDay(); + Log::debug(sprintf('finalAccountBalanceInRange(#%d, %s, %s)', $account->id, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); + + // set up cache $cache = new CacheProperties(); $cache->addProperty($account->id); - $cache->addProperty('balance-in-range'); - $cache->addProperty(null !== $currency ? $currency->id : 0); + $cache->addProperty('final-balance-in-range'); $cache->addProperty($start); $cache->addProperty($end); if ($cache->has()) { return $cache->get(); } - $start->subDay(); - $end->addDay(); $balances = []; $formatted = $start->format('Y-m-d'); - $startBalance = $this->balance($account, $start, $currency); - - $balances[$formatted] = $startBalance; - if (null === $currency) { - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($account->user); - $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); + $startBalance = $this->finalAccountBalance($account, $start); + $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); + $accountCurrency = $this->getAccountCurrency($account); + $hasCurrency = null !== $accountCurrency; + $currency = $accountCurrency ?? $defaultCurrency; + Log::debug(sprintf('Currency is %s', $currency->code)); + if (!$hasCurrency) { + Log::debug(sprintf('Also set start balance in %s', $defaultCurrency->code)); + $startBalance[$defaultCurrency->code] ??= '0'; } - $currencyId = $currency->id; + $currencies = [ + $currency->id => $currency, + $defaultCurrency->id => $defaultCurrency, + ]; - $start->addDay(); - // query! + $startBalance[$currency->code] ??= '0'; + $balances[$formatted] = $startBalance; + Log::debug('Final start balance: ', $startBalance); + + + // sums up the balance changes per day, for foreign, native and normal amounts. $set = $account->transactions() ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')) + ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) ->groupBy('transaction_journals.date') ->groupBy('transactions.transaction_currency_id') ->groupBy('transactions.foreign_currency_id') @@ -136,9 +125,10 @@ class Steam [ // @phpstan-ignore-line 'transaction_journals.date', 'transactions.transaction_currency_id', - \DB::raw('SUM(transactions.amount) AS modified'), + DB::raw('SUM(transactions.amount) AS modified'), 'transactions.foreign_currency_id', - \DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), + DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), + DB::raw('SUM(transactions.native_amount) AS modified_native'), ] ) ; @@ -147,517 +137,69 @@ class Steam /** @var Transaction $entry */ foreach ($set as $entry) { - // normal amount and foreign amount - $modified = (string) (null === $entry->modified ? '0' : $entry->modified); - $foreignModified = (string) (null === $entry->modified_foreign ? '0' : $entry->modified_foreign); - $amount = '0'; - if ($currencyId === (int) $entry->transaction_currency_id || 0 === $currencyId) { - // use normal amount: - $amount = $modified; - } - if ($currencyId === (int) $entry->foreign_currency_id) { - // use foreign amount: - $amount = $foreignModified; - } - // Log::debug(sprintf('Trying to add %s and %s.', var_export($currentBalance, true), var_export($amount, true))); - $currentBalance = bcadd($currentBalance, $amount); - $carbon = new Carbon($entry->date, config('app.timezone')); - $date = $carbon->format('Y-m-d'); - $balances[$date] = $currentBalance; - } + // normal, native and foreign amount + $carbon = new Carbon($entry->date, $entry->date_tz); + $modified = (string) (null === $entry->modified ? '0' : $entry->modified); + $foreignModified = (string) (null === $entry->modified_foreign ? '0' : $entry->modified_foreign); + $nativeModified = (string) (null === $entry->modified_native ? '0' : $entry->modified_native); + // find currency of this entry. + $currencies[$entry->transaction_currency_id] ??= TransactionCurrency::find($entry->transaction_currency_id); + $entryCurrency = $currencies[$entry->transaction_currency_id]; + + Log::debug(sprintf('Processing transaction(s) on date %s', $carbon->format('Y-m-d H:i:s'))); + + // if convert to native, if NOT convert to native. + if ($convertToNative) { + Log::debug(sprintf('Amount is %s %s, foreign amount is %s, native amount is %s', $entryCurrency->code, $this->bcround($modified, 2), $this->bcround($foreignModified, 2), $this->bcround($nativeModified, 2))); + // if the currency is the default currency add to native balance + currency balance + if ($entry->transaction_currency_id === $defaultCurrency->id) { + Log::debug('Add amount to native.'); + $currentBalance['native_balance'] = bcadd($currentBalance['native_balance'], $modified); + } + + // add to native balance. + if ($entry->foreign_currency_id !== $defaultCurrency->id) { + // this check is not necessary, because if the foreign currency is the same as the default currency, the native amount is zero. + // so adding this would mean nothing. + $currentBalance['native_balance'] = bcadd($currentBalance['native_balance'], $nativeModified); + } + if ($entry->foreign_currency_id === $defaultCurrency->id) { + $currentBalance['native_balance'] = bcadd($currentBalance['native_balance'], $foreignModified); + } + // add to balance if is the same. + if ($entry->transaction_currency_id === $accountCurrency?->id) { + $currentBalance['balance'] = bcadd($currentBalance['balance'], $modified); + } + // add currency balance + $currentBalance[$entryCurrency->code] = bcadd($currentBalance[$entryCurrency->code] ?? '0', $modified); + } + if (!$convertToNative) { + Log::debug(sprintf('Amount is %s %s, foreign amount is %s, native amount is %s', $entryCurrency->code, $modified, $foreignModified, $nativeModified)); + // add to balance, as expected. + $currentBalance['balance'] = bcadd($currentBalance['balance'] ?? '0', $modified); + // add to GBP, as expected. + $currentBalance[$entryCurrency->code] = bcadd($currentBalance[$entryCurrency->code] ?? '0', $modified); + } + $balances[$carbon->format('Y-m-d')] = $currentBalance; + Log::debug('Updated entry', $currentBalance); + } $cache->store($balances); + Log::debug('End of method'); return $balances; } - public function balanceByTransactions(Account $account, Carbon $date, ?TransactionCurrency $currency): array + public function finalAccountsBalance(Collection $accounts, Carbon $date): array { - $cache = new CacheProperties(); - $cache->addProperty($account->id); - $cache->addProperty('balance-by-transactions'); - $cache->addProperty($date); - $cache->addProperty(null !== $currency ? $currency->id : 0); - if ($cache->has()) { - return $cache->get(); + $balances = []; + foreach ($accounts as $account) { + $balances[$account->id] = $this->finalAccountBalance($account, $date); } - $query = $account->transactions() - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->orderBy('transaction_journals.date', 'desc') - ->orderBy('transaction_journals.order', 'asc') - ->orderBy('transaction_journals.description', 'desc') - ->orderBy('transactions.amount', 'desc') - ; - if (null !== $currency) { - $query->where('transactions.transaction_currency_id', $currency->id); - $query->limit(1); - $result = $query->get(['transactions.transaction_currency_id', 'transactions.balance_after'])->first(); - $key = (int) $result->transaction_currency_id; - $return = [$key => $result->balance_after]; - $cache->store($return); - - return $return; - } - - $return = []; - $result = $query->get(['transactions.transaction_currency_id', 'transactions.balance_after']); - foreach ($result as $entry) { - $key = (int) $entry->transaction_currency_id; - if (array_key_exists($key, $return)) { - continue; - } - $return[$key] = $entry->balance_after; - } - - return $return; - } - - /** - * Gets balance at the end of current month by default - * - * @throws FireflyException - */ - public function balance(Account $account, Carbon $date, ?TransactionCurrency $currency = null): string - { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - // abuse chart properties: - $cache = new CacheProperties(); - $cache->addProperty($account->id); - $cache->addProperty('balance'); - $cache->addProperty($date); - $cache->addProperty(null !== $currency ? $currency->id : 0); - if ($cache->has()) { - return $cache->get(); - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - if (null === $currency) { - $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); - } - // first part: get all balances in own currency: - $transactions = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', $currency->id) - ->get(['transactions.amount'])->toArray() - ; - $nativeBalance = $this->sumTransactions($transactions, 'amount'); - // get all balances in foreign currency: - $transactions = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.foreign_currency_id', $currency->id) - ->where('transactions.transaction_currency_id', '!=', $currency->id) - ->get(['transactions.foreign_amount'])->toArray() - ; - $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); - $balance = bcadd($nativeBalance, $foreignBalance); - $virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance; - $balance = bcadd($balance, $virtual); - - $cache->store($balance); - - return $balance; - } - - /** - * @throws FireflyException - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function balanceInRangeConverted(Account $account, Carbon $start, Carbon $end, TransactionCurrency $native): array - { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $cache = new CacheProperties(); - $cache->addProperty($account->id); - $cache->addProperty('balance-in-range-converted'); - $cache->addProperty($native->id); - $cache->addProperty($start); - $cache->addProperty($end); - if ($cache->has()) { - return $cache->get(); - } - Log::debug(sprintf('balanceInRangeConverted for account #%d to %s', $account->id, $native->code)); - $start->subDay(); - $end->addDay(); - $balances = []; - $formatted = $start->format('Y-m-d'); - $currencies = []; - $startBalance = $this->balanceConverted($account, $start, $native); // already converted to native amount - $balances[$formatted] = $startBalance; - - Log::debug(sprintf('Start balance on %s is %s', $formatted, $startBalance)); - Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); - $converter = new ExchangeRateConverter(); - - // not sure why this is happening: - $start->addDay(); - - // grab all transactions between start and end: - $set = $account->transactions() - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')) - ->orderBy('transaction_journals.date', 'ASC') - ->whereNull('transaction_journals.deleted_at') - ->get( - [ - 'transaction_journals.date', - 'transactions.transaction_currency_id', - 'transactions.amount', - 'transactions.foreign_currency_id', - 'transactions.foreign_amount', - ] - )->toArray() - ; - - // loop the set and convert if necessary: - $currentBalance = $startBalance; - - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $day = false; - - try { - $day = Carbon::parse($transaction['date'], config('app.timezone')); - } catch (InvalidFormatException $e) { - Log::error(sprintf('Could not parse date "%s" in %s: %s', $transaction['date'], __METHOD__, $e->getMessage())); - } - if (false === $day) { - $day = today(config('app.timezone')); - } - $format = $day->format('Y-m-d'); - // if the transaction is in the expected currency, change nothing. - if ((int) $transaction['transaction_currency_id'] === $native->id) { - // change the current balance, set it to today, continue the loop. - $currentBalance = bcadd($currentBalance, $transaction['amount']); - $balances[$format] = $currentBalance; - Log::debug(sprintf('%s: transaction in %s, new balance is %s.', $format, $native->code, $currentBalance)); - - continue; - } - // if foreign currency is in the expected currency, do nothing: - if ((int) $transaction['foreign_currency_id'] === $native->id) { - $currentBalance = bcadd($currentBalance, $transaction['foreign_amount']); - $balances[$format] = $currentBalance; - Log::debug(sprintf('%s: transaction in %s (foreign), new balance is %s.', $format, $native->code, $currentBalance)); - - continue; - } - // otherwise, convert 'amount' to the necessary currency: - $currencyId = (int) $transaction['transaction_currency_id']; - $currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId); - $currencies[$currencyId] = $currency; - - $rate = $converter->getCurrencyRate($currency, $native, $day); - $convertedAmount = bcmul($transaction['amount'], $rate); - $currentBalance = bcadd($currentBalance, $convertedAmount); - $balances[$format] = $currentBalance; - - Log::debug(sprintf( - '%s: transaction in %s(!). Conversion rate is %s. %s %s = %s %s', - $format, - $currency->code, - $rate, - $currency->code, - $transaction['amount'], - $native->code, - $convertedAmount - )); - } - - $cache->store($balances); - $converter->summarize(); - return $balances; } - /** - * selection of transactions - * 1: all normal transactions. No foreign currency info. In $currency. Need conversion. - * 2: all normal transactions. No foreign currency info. In $native. Need NO conversion. - * 3: all normal transactions. No foreign currency info. In neither currency. Need conversion. - * Then, select everything with foreign currency info: - * 4. All transactions with foreign currency info in $native. Normal currency value is ignored. Do not need - * conversion. - * 5. All transactions with foreign currency info NOT in $native, but currency info in $currency. Need conversion. - * 6. All transactions with foreign currency info NOT in $native, and currency info NOT in $currency. Need - * conversion. - * - * Gets balance at the end of current month by default. Returns the balance converted - * to the indicated currency ($native). - * - * @throws FireflyException - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function balanceConverted(Account $account, Carbon $date, TransactionCurrency $native): string - { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - Log::debug(sprintf('Now in balanceConverted (%s) for account #%d, converting to %s', $date->format('Y-m-d'), $account->id, $native->code)); - $cache = new CacheProperties(); - $cache->addProperty($account->id); - $cache->addProperty('balance'); - $cache->addProperty($date); - $cache->addProperty($native->id); - if ($cache->has()) { - Log::debug('Cached!'); - - return $cache->get(); - } - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $currency = $repository->getAccountCurrency($account); - $currency = null === $currency ? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup) : $currency; - if ($native->id === $currency->id) { - Log::debug('No conversion necessary!'); - - return $this->balance($account, $date); - } - - $new = []; - $existing = []; - $new[] = $account->transactions() // 1 - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', $currency->id) - ->whereNull('transactions.foreign_currency_id') - ->get(['transaction_journals.date', 'transactions.amount'])->toArray() - ; - Log::debug(sprintf('%d transaction(s) in set #1', count($new[0]))); - $existing[] = $account->transactions() // 2 - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', $native->id) - ->whereNull('transactions.foreign_currency_id') - ->get(['transactions.amount'])->toArray() - ; - Log::debug(sprintf('%d transaction(s) in set #2', count($existing[0]))); - $new[] = $account->transactions() // 3 - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', '!=', $currency->id) - ->where('transactions.transaction_currency_id', '!=', $native->id) - ->whereNull('transactions.foreign_currency_id') - ->get(['transaction_journals.date', 'transactions.amount'])->toArray() - ; - Log::debug(sprintf('%d transactions in set #3', count($new[1]))); - $existing[] = $account->transactions() // 4 - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.foreign_currency_id', $native->id) - ->whereNotNull('transactions.foreign_amount') - ->get(['transactions.foreign_amount'])->toArray() - ; - Log::debug(sprintf('%d transactions in set #4', count($existing[1]))); - $new[] = $account->transactions()// 5 - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', $currency->id) - ->where('transactions.foreign_currency_id', '!=', $native->id) - ->whereNotNull('transactions.foreign_amount') - ->get(['transaction_journals.date', 'transactions.amount'])->toArray() - ; - Log::debug(sprintf('%d transactions in set #5', count($new[2]))); - $new[] = $account->transactions()// 6 - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', '!=', $currency->id) - ->where('transactions.foreign_currency_id', '!=', $native->id) - ->whereNotNull('transactions.foreign_amount') - ->get(['transaction_journals.date', 'transactions.amount'])->toArray() - ; - Log::debug(sprintf('%d transactions in set #6', count($new[3]))); - - // process both sets of transactions. Of course, no need to convert set "existing". - $balance = $this->sumTransactions($existing[0], 'amount'); - $balance = bcadd($balance, $this->sumTransactions($existing[1], 'foreign_amount')); - Log::debug(sprintf('Balance from set #2 and #4 is %f', $balance)); - - // need to convert the others. All sets use the "amount" value as their base (that's easy) - // but we need to convert each transaction separately because the date difference may - // incur huge currency changes. - Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); - $start = clone $date; - $end = clone $date; - $converter = new ExchangeRateConverter(); - foreach ($new as $set) { - foreach ($set as $transaction) { - $currentDate = false; - - try { - $currentDate = Carbon::parse($transaction['date'], config('app.timezone')); - } catch (InvalidFormatException $e) { - Log::error(sprintf('Could not parse date "%s" in %s', $transaction['date'], __METHOD__)); - } - if (false === $currentDate) { - $currentDate = today(config('app.timezone')); - } - if ($currentDate->lte($start)) { - $start = clone $currentDate; - } - } - } - unset($currentDate); - $converter->prepare($currency, $native, $start, $end); - - foreach ($new as $set) { - foreach ($set as $transaction) { - $currentDate = false; - - try { - $currentDate = Carbon::parse($transaction['date'], config('app.timezone')); - } catch (InvalidFormatException $e) { - Log::error(sprintf('Could not parse date "%s" in %s', $transaction['date'], __METHOD__)); - } - if (false === $currentDate) { - $currentDate = today(config('app.timezone')); - } - $rate = $converter->getCurrencyRate($currency, $native, $currentDate); - $convertedAmount = bcmul($transaction['amount'], $rate); - $balance = bcadd($balance, $convertedAmount); - } - } - - // add virtual balance (also needs conversion) - $virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance; - $virtual = $converter->convert($currency, $native, $account->created_at, $virtual); - $balance = bcadd($balance, $virtual); - $converter->summarize(); - - $cache->store($balance); - $converter->summarize(); - - return $balance; - } - - /** - * This method always ignores the virtual balance. - * - * @throws FireflyException - */ - public function balancesByAccounts(Collection $accounts, Carbon $date): array - { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $ids = $accounts->pluck('id')->toArray(); - // cache this property. - $cache = new CacheProperties(); - $cache->addProperty($ids); - $cache->addProperty('balances'); - $cache->addProperty($date); - if ($cache->has()) { - return $cache->get(); - } - - // need to do this per account. - $result = []; - - /** @var Account $account */ - foreach ($accounts as $account) { - $result[$account->id] = $this->balance($account, $date); - } - - $cache->store($result); - - return $result; - } - - /** - * This method always ignores the virtual balance. - * - * @throws FireflyException - */ - public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array - { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $ids = $accounts->pluck('id')->toArray(); - // cache this property. - $cache = new CacheProperties(); - $cache->addProperty($ids); - $cache->addProperty('balances-converted'); - $cache->addProperty($date); - if ($cache->has()) { - return $cache->get(); - } - - // need to do this per account. - $result = []; - - /** @var Account $account */ - foreach ($accounts as $account) { - $default = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); - $result[$account->id] - = [ - 'balance' => $this->balance($account, $date), - 'native_balance' => $this->balanceConverted($account, $date, $default), - ]; - } - - $cache->store($result); - - return $result; - } - - /** - * Same as above, but also groups per currency. - */ - public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array - { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $ids = $accounts->pluck('id')->toArray(); - // cache this property. - $cache = new CacheProperties(); - $cache->addProperty($ids); - $cache->addProperty('balances-per-currency'); - $cache->addProperty($date); - if ($cache->has()) { - return $cache->get(); - } - - // need to do this per account. - $result = []; - - /** @var Account $account */ - foreach ($accounts as $account) { - $result[$account->id] = $this->balancePerCurrency($account, $date); - } - - $cache->store($result); - - return $result; - } - - public function balancePerCurrency(Account $account, Carbon $date): array - { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - // abuse chart properties: - $cache = new CacheProperties(); - $cache->addProperty($account->id); - $cache->addProperty('balance-per-currency'); - $cache->addProperty($date); - if ($cache->has()) { - return $cache->get(); - } - $query = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->groupBy('transactions.transaction_currency_id') - ; - $balances = $query->get(['transactions.transaction_currency_id', \DB::raw('SUM(transactions.amount) as sum_for_currency')]); // @phpstan-ignore-line - $return = []; - - /** @var \stdClass $entry */ - foreach ($balances as $entry) { - $return[(int) $entry->transaction_currency_id] = (string) $entry->sum_for_currency; - } - $cache->store($return); - - return $return; - } - /** * https://stackoverflow.com/questions/1642614/how-to-ceil-floor-and-round-bcmath-numbers */ @@ -745,18 +287,204 @@ class Steam return str_replace($search, '', $string); } + /** + * Returns the balance of an account at exact moment given. Array with at least one value. + * + * "balance" the balance in whatever currency the account has, so the sum of all transaction that happen to have + * THAT currency. + * "native_balance" the balance according to the "native_amount" + "native_foreign_amount" fields. + * "ABC" the balance in this particular currency code (may repeat for each found currency). + * + * Het maakt niet uit of de native currency wel of niet gelijk is aan de account currency. + * Optelsom zou hetzelfde moeten zijn. Als het EUR is en de rekening ook is native_amount 0. + * Zo niet is amount 0 en native_amount het bedrag. + * + * Eerst een som van alle transacties in de native currency. Alle EUR bij elkaar opgeteld. + * Om te weten wat er nog meer op de rekening gebeurt, pak alles waar currency niet EUR is, en de foreign ook niet, + * en tel native_amount erbij op. + * Daarna pak je alle transacties waar currency niet EUR is, en de foreign wel, en tel foreign_amount erbij op. + * + * Wil je niks weten van native currencies, pak je: + * + * Eerst een som van alle transacties gegroepeerd op currency. Einde. + */ + public function finalAccountBalance(Account $account, Carbon $date): array + { + Log::debug(sprintf('Now in finalAccountBalance(#%d, "%s", "%s")', $account->id, $account->name, $date->format('Y-m-d H:i:s'))); + $native = Amount::getDefaultCurrencyByUserGroup($account->user->userGroup); + $convertToNative = Amount::convertToNative($account->user); + $accountCurrency = $this->getAccountCurrency($account); + $hasCurrency = null !== $accountCurrency; + $currency = $hasCurrency ? $accountCurrency : $native; + $return = []; + + // first, the "balance", as described earlier. + if ($convertToNative) { + // normal balance + $return['balance'] = (string) $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->where('transactions.transaction_currency_id', $native->id) + ->sum('transactions.amount') + ; + // plus virtual balance, if the account has a virtual_balance in the native currency + if ($native->id === $accountCurrency?->id) { + $return['balance'] = bcadd('' === (string) $account->virtual_balance ? '0' : $account->virtual_balance, $return['balance']); + } + Log::debug(sprintf('balance is (%s only) %s (with virtual balance)', $native->code, $this->bcround($return['balance'], 2))); + + // native balance + $return['native_balance'] = (string) $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->whereNot('transactions.transaction_currency_id', $native->id) + ->sum('transactions.native_amount') + ; + // plus native virtual balance. + $return['native_balance'] = bcadd('' === (string) $account->native_virtual_balance ? '0' : $account->native_virtual_balance, $return['native_balance']); + Log::debug(sprintf('native_balance is (all transactions to %s) %s (with virtual balance)', $native->code, $this->bcround($return['native_balance']))); + + // plus foreign transactions in THIS currency. + $sum = (string) $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->whereNot('transactions.transaction_currency_id', $native->id) + ->where('transactions.foreign_currency_id', $native->id) + ->sum('transactions.foreign_amount') + ; + $return['native_balance'] = bcadd($return['native_balance'], $sum); + + Log::debug(sprintf('Foreign amount transactions add (%s only) %s, total native_balance is now %s', $native->code, $this->bcround($sum), $this->bcround($return['native_balance']))); + } + + // balance(s) in other (all) currencies. + $array = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')) + ->get(['transaction_currencies.code', 'transactions.amount'])->toArray() + ; + $others = $this->groupAndSumTransactions($array, 'code', 'amount'); + Log::debug('All balances are (joined)', $others); + // if the account has no own currency preference, drop balance in favor of native balance + if ($hasCurrency && !$convertToNative) { + $return['balance'] = $others[$currency->code] ?? '0'; + $return['native_balance'] = $others[$currency->code] ?? '0'; + Log::debug(sprintf('Set balance + native_balance to %s', $return['balance'])); + } + + // if the currency is the same as the native currency, set the native_balance to the balance for consistency. + // if($currency->id === $native->id) { + // $return['native_balance'] = $return['balance']; + // } + + if (!$hasCurrency && array_key_exists('balance', $return) && array_key_exists('native_balance', $return)) { + Log::debug('Account has no currency preference, dropping balance in favor of native balance.'); + $sum = bcadd($return['balance'], $return['native_balance']); + Log::debug(sprintf('%s + %s = %s', $return['balance'], $return['native_balance'], $sum)); + $return['native_balance'] = $sum; + unset($return['balance']); + } + $final = array_merge($return, $others); + Log::debug('Return is', $final); + + return $final; + } + + public function filterAccountBalances(array $total, Account $account, bool $convertToNative, ?TransactionCurrency $currency = null): array + { + Log::debug(sprintf('filterAccountBalances(#%d)', $account->id)); + $return = []; + foreach ($total as $key => $value) { + $return[$key] = $this->filterAccountBalance($value, $account, $convertToNative, $currency); + } + Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id)); + + return $return; + } + + public function filterAccountBalance(array $set, Account $account, bool $convertToNative, ?TransactionCurrency $currency = null): array + { + Log::debug(sprintf('filterAccountBalance(#%d)', $account->id), $set); + if (0 === count($set)) { + Log::debug(sprintf('Return empty array for account #%d', $account->id)); + + return []; + } + $defaultCurrency = app('amount')->getDefaultCurrency(); + if ($convertToNative) { + if ($defaultCurrency->id === $currency?->id) { + Log::debug(sprintf('Unset "native_balance" and "%s" for account #%d', $defaultCurrency->code, $account->id)); + unset($set['native_balance'], $set[$defaultCurrency->code]); + } + if (null !== $currency && $defaultCurrency->id !== $currency->id) { + Log::debug(sprintf('Unset balance for account #%d', $account->id)); + unset($set['balance']); + } + + if (null === $currency) { + Log::debug(sprintf('TEMP DO NOT Drop defaultCurrency balance for account #%d', $account->id)); + // unset($set[$this->defaultCurrency->code]); + } + } + + if (!$convertToNative) { + if (null === $currency) { + Log::debug(sprintf('Unset native_balance and make defaultCurrency balance the balance for account #%d', $account->id)); + $set['balance'] = $set[$defaultCurrency->code] ?? '0'; + unset($set['native_balance'], $set[$defaultCurrency->code]); + } + + if (null !== $currency) { + Log::debug(sprintf('Unset native_balance + defaultCurrency + currencyCode balance for account #%d', $account->id)); + unset($set['native_balance'], $set[$defaultCurrency->code], $set[$currency->code]); + } + } + + + // put specific value first in array. + if (array_key_exists('native_balance', $set)) { + $set = ['native_balance' => $set['native_balance']] + $set; + } + if (array_key_exists('balance', $set)) { + $set = ['balance' => $set['balance']] + $set; + } + Log::debug(sprintf('Return #%d', $account->id), $set); + + return $set; + } + + private function groupAndSumTransactions(array $array, string $group, string $field): array + { + $return = []; + + foreach ($array as $item) { + $groupKey = $item[$group] ?? 'unknown'; + $return[$groupKey] = bcadd($return[$groupKey] ?? '0', $item[$field]); + } + + return $return; + } + /** * @throws FireflyException */ public function getHostName(string $ipAddress): string { + $host = ''; + try { $hostName = gethostbyaddr($ipAddress); - } catch (\Exception $e) { // intentional generic exception - throw new FireflyException($e->getMessage(), 0, $e); + } catch (\Exception $e) { + app('log')->error($e->getMessage()); + $hostName = $ipAddress; } - return (string) $hostName; + if ('' !== (string) $hostName && $hostName !== $ipAddress) { + $host = $hostName; + } + + return (string) $host; } public function getLastActivities(array $accounts): array @@ -771,9 +499,9 @@ class Steam /** @var Transaction $entry */ foreach ($set as $entry) { - $date = new Carbon($entry->max_date, config('app.timezone')); + $date = new Carbon($entry->max_date, config('app.timezone')); $date->setTimezone(config('app.timezone')); - $list[$entry->account_id] = $date; + $list[(int) $entry->account_id] = $date; } return $list; diff --git a/app/Support/System/GeneratesInstallationId.php b/app/Support/System/GeneratesInstallationId.php index bd017f4f5a..e3dde0e793 100644 --- a/app/Support/System/GeneratesInstallationId.php +++ b/app/Support/System/GeneratesInstallationId.php @@ -49,7 +49,7 @@ trait GeneratesInstallationId if (null === $config) { $uuid4 = Uuid::uuid4(); - $uniqueId = (string)$uuid4; + $uniqueId = (string) $uuid4; app('log')->info(sprintf('Created Firefly III installation ID %s', $uniqueId)); app('fireflyconfig')->set('installation_id', $uniqueId); } diff --git a/app/Support/System/OAuthKeys.php b/app/Support/System/OAuthKeys.php index 5a7501dd03..641a561f7a 100644 --- a/app/Support/System/OAuthKeys.php +++ b/app/Support/System/OAuthKeys.php @@ -63,8 +63,8 @@ class OAuthKeys // better check if keys are in the database: if (app('fireflyconfig')->has(self::PRIVATE_KEY) && app('fireflyconfig')->has(self::PUBLIC_KEY)) { try { - $privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; - $publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; + $privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; + $publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; } catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) { app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage())); app('log')->error($e->getTraceAsString()); @@ -104,8 +104,8 @@ class OAuthKeys */ public static function restoreKeysFromDB(): bool { - $privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; - $publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; + $privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; + $publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; try { $privateContent = \Crypt::decrypt($privateKey); diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index 403aac5884..015378ab29 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -75,6 +75,7 @@ class AmountFormat extends AbstractExtension $this->formatAmountByAccount(), $this->formatAmountBySymbol(), $this->formatAmountByCurrency(), + $this->formatAmountByCode(), ]; } @@ -100,6 +101,26 @@ class AmountFormat extends AbstractExtension ); } + /** + * Use the code to format a currency. + */ + protected function formatAmountByCode(): TwigFunction + { + // formatAmountByCode + return new TwigFunction( + 'formatAmountByCode', + static function (string $amount, string $code, ?bool $coloured = null): string { + $coloured ??= true; + + /** @var TransactionCurrency $currency */ + $currency = TransactionCurrency::whereCode($code)->first(); + + return app('amount')->formatAnything($currency, $amount, $coloured); + }, + ['is_safe' => ['html']] + ); + } + /** * Will format the amount by the currency related to the given account. */ diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 4568cf7fbc..18b3d5bac7 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -25,10 +25,14 @@ namespace FireflyIII\Support\Twig; use Carbon\Carbon; use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface; +use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Search\OperatorQuerySearch; use League\CommonMark\GithubFlavoredMarkdownConverter; +use Route; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -62,28 +66,33 @@ class General extends AbstractExtension } /** @var Carbon $date */ - $date = session('end', today(config('app.timezone'))->endOfMonth()); - $runningBalance = config('firefly.feature_flags.running_balance_column'); - $info = []; - if (true === $runningBalance) { - $info = app('steam')->balanceByTransactions($account, $date, null); - } - if (false === $runningBalance) { - $info[] = app('steam')->balance($account, $date); - } + $date = session('end', today(config('app.timezone'))->endOfMonth()); + $info = Steam::finalAccountBalance($account, $date); + $currency = Steam::getAccountCurrency($account); + $default = Amount::getDefaultCurrency(); + $convertToNative = Amount::convertToNative(); + $useNative = $convertToNative && $default->id !== $currency->id; + $strings = []; + foreach ($info as $key => $balance) { + if ('balance' === $key) { + // balance in account currency. + if (!$useNative) { + $strings[] = app('amount')->formatAnything($currency, $balance, false); + } - $strings = []; - foreach ($info as $currencyId => $balance) { - $balance = (string) $balance; - if (0 === $currencyId) { - // not good code but OK - /** @var AccountRepositoryInterface $accountRepos */ - $accountRepos = app(AccountRepositoryInterface::class); - $currency = $accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency(); - $strings[] = app('amount')->formatAnything($currency, $balance, false); + continue; } - if (0 !== $currencyId) { - $strings[] = app('amount')->formatByCurrencyId($currencyId, $balance, false); + if ('native_balance' === $key) { + // balance in native currency. + if ($useNative) { + $strings[] = app('amount')->formatAnything($default, $balance, false); + } + + continue; + } + // for multi currency accounts. + if ($useNative && $key !== $default->code) { + $strings[] = app('amount')->formatAnything(TransactionCurrency::where('code', $key)->first(), $balance, false); } } diff --git a/app/Support/Twig/Rule.php b/app/Support/Twig/Rule.php index c79031d807..42ef0f6ff9 100644 --- a/app/Support/Twig/Rule.php +++ b/app/Support/Twig/Rule.php @@ -46,8 +46,9 @@ class Rule extends AbstractExtension 'allJournalTriggers', static function () { return [ - 'store-journal' => (string)trans('firefly.rule_trigger_store_journal'), - 'update-journal' => (string)trans('firefly.rule_trigger_update_journal'), + 'store-journal' => (string) trans('firefly.rule_trigger_store_journal'), + 'update-journal' => (string) trans('firefly.rule_trigger_update_journal'), + 'manual-activation' => (string) trans('firefly.rule_trigger_manual'), ]; } ); @@ -62,7 +63,7 @@ class Rule extends AbstractExtension $possibleTriggers = []; foreach ($ruleTriggers as $key) { if ('user_action' !== $key) { - $possibleTriggers[$key] = (string)trans('firefly.rule_trigger_'.$key.'_choice'); + $possibleTriggers[$key] = (string) trans('firefly.rule_trigger_'.$key.'_choice'); } } unset($ruleTriggers); @@ -82,7 +83,7 @@ class Rule extends AbstractExtension $ruleActions = array_keys(\Config::get('firefly.rule-actions')); $possibleActions = []; foreach ($ruleActions as $key) { - $possibleActions[$key] = (string)trans('firefly.rule_action_'.$key.'_choice'); + $possibleActions[$key] = (string) trans('firefly.rule_action_'.$key.'_choice'); } unset($ruleActions); asort($possibleActions); diff --git a/app/Support/Twig/TransactionGroupTwig.php b/app/Support/Twig/TransactionGroupTwig.php index dc4cd7812a..4f13658810 100644 --- a/app/Support/Twig/TransactionGroupTwig.php +++ b/app/Support/Twig/TransactionGroupTwig.php @@ -86,7 +86,7 @@ class TransactionGroupTwig extends AbstractExtension $colored = false; } - $result = app('amount')->formatFlat($array['currency_symbol'], (int)$array['currency_decimal_places'], $amount, $colored); + $result = app('amount')->formatFlat($array['currency_symbol'], (int) $array['currency_decimal_places'], $amount, $colored); if (TransactionType::TRANSFER === $type) { $result = sprintf('%s', $result); } @@ -129,7 +129,7 @@ class TransactionGroupTwig extends AbstractExtension if (TransactionType::TRANSFER === $type) { $colored = false; } - $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int)$array['foreign_currency_decimal_places'], $amount, $colored); + $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int) $array['foreign_currency_decimal_places'], $amount, $colored); if (TransactionType::TRANSFER === $type) { $result = sprintf('%s', $result); } diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php index 4167f64c31..706dd74221 100644 --- a/app/Support/Twig/Translation.php +++ b/app/Support/Twig/Translation.php @@ -38,7 +38,7 @@ class Translation extends AbstractExtension new TwigFilter( '_', static function ($name) { - return (string)trans(sprintf('firefly.%s', $name)); + return (string) trans(sprintf('firefly.%s', $name)); }, ['is_safe' => ['html']] ), diff --git a/app/TransactionRules/Actions/AppendDescriptionToNotes.php b/app/TransactionRules/Actions/AppendDescriptionToNotes.php index ae4c3866fd..5ca9f9214f 100644 --- a/app/TransactionRules/Actions/AppendDescriptionToNotes.php +++ b/app/TransactionRules/Actions/AppendDescriptionToNotes.php @@ -57,7 +57,7 @@ class AppendDescriptionToNotes implements ActionInterface $object = TransactionJournal::where('user_id', $journal['user_id'])->find($journal['transaction_journal_id']); if (null === $object) { app('log')->error(sprintf('No journal #%d belongs to user #%d.', $journal['transaction_journal_id'], $journal['user_id'])); - event(new RuleActionFailedOnArray($this->action, $journal, (string)trans('rules.journal_other_user'))); + event(new RuleActionFailedOnArray($this->action, $journal, (string) trans('rules.journal_other_user'))); return false; } @@ -72,7 +72,7 @@ class AppendDescriptionToNotes implements ActionInterface $note->text = trim(sprintf("%s \n%s", $note->text, $object->description)); } if ('' === $note->text) { - $note->text = (string)$object->description; + $note->text = (string) $object->description; } $after = $note->text; diff --git a/app/TransactionRules/Actions/AppendNotes.php b/app/TransactionRules/Actions/AppendNotes.php index a097811ea0..c3f06b8ada 100644 --- a/app/TransactionRules/Actions/AppendNotes.php +++ b/app/TransactionRules/Actions/AppendNotes.php @@ -50,13 +50,13 @@ class AppendNotes implements ActionInterface public function actOnArray(array $journal): bool { $this->refreshNotes($journal); - $dbNote = Note::where('noteable_id', (int)$journal['transaction_journal_id']) + $dbNote = Note::where('noteable_id', (int) $journal['transaction_journal_id']) ->where('noteable_type', TransactionJournal::class) ->first(['notes.*']) ; if (null === $dbNote) { $dbNote = new Note(); - $dbNote->noteable_id = (int)$journal['transaction_journal_id']; + $dbNote->noteable_id = (int) $journal['transaction_journal_id']; $dbNote->noteable_type = TransactionJournal::class; $dbNote->text = ''; } diff --git a/app/TransactionRules/Actions/AppendNotesToDescription.php b/app/TransactionRules/Actions/AppendNotesToDescription.php index 235808f1ce..3402a6e8a8 100644 --- a/app/TransactionRules/Actions/AppendNotesToDescription.php +++ b/app/TransactionRules/Actions/AppendNotesToDescription.php @@ -74,7 +74,7 @@ class AppendNotesToDescription implements ActionInterface // only append if there is something to append if ('' !== $note->text) { $before = $object->description; - $object->description = trim(sprintf('%s %s', $object->description, (string)$this->clearString($note->text))); + $object->description = trim(sprintf('%s %s', $object->description, (string) $this->clearString($note->text))); $object->save(); app('log')->debug(sprintf('Journal description is updated to "%s".', $object->description)); diff --git a/app/TransactionRules/Actions/ConvertToTransfer.php b/app/TransactionRules/Actions/ConvertToTransfer.php index 7793d60188..b743736468 100644 --- a/app/TransactionRules/Actions/ConvertToTransfer.php +++ b/app/TransactionRules/Actions/ConvertToTransfer.php @@ -169,7 +169,7 @@ class ConvertToTransfer implements ActionInterface return ''; } - return (string)$journal->transactions()->where('amount', '<', 0)->first()?->account?->accountType?->type; + return (string) $journal->transactions()->where('amount', '<', 0)->first()?->account?->accountType?->type; } private function getDestinationType(int $journalId): string @@ -182,7 +182,7 @@ class ConvertToTransfer implements ActionInterface return ''; } - return (string)$journal->transactions()->where('amount', '>', 0)->first()?->account?->accountType?->type; + return (string) $journal->transactions()->where('amount', '>', 0)->first()?->account?->accountType?->type; } /** diff --git a/app/TransactionRules/Actions/MoveDescriptionToNotes.php b/app/TransactionRules/Actions/MoveDescriptionToNotes.php index be168448f5..6dae68c34d 100644 --- a/app/TransactionRules/Actions/MoveDescriptionToNotes.php +++ b/app/TransactionRules/Actions/MoveDescriptionToNotes.php @@ -71,7 +71,7 @@ class MoveDescriptionToNotes implements ActionInterface $object->description = '(no description)'; } if ('' === $note->text) { - $note->text = (string)$object->description; + $note->text = (string) $object->description; $object->description = '(no description)'; } $after = $note->text; diff --git a/app/TransactionRules/Actions/MoveNotesToDescription.php b/app/TransactionRules/Actions/MoveNotesToDescription.php index 8687d9b614..d073cce16b 100644 --- a/app/TransactionRules/Actions/MoveNotesToDescription.php +++ b/app/TransactionRules/Actions/MoveNotesToDescription.php @@ -78,7 +78,7 @@ class MoveNotesToDescription implements ActionInterface } $before = $object->description; $beforeNote = $note->text; - $object->description = (string)$this->clearString($note->text); + $object->description = (string) $this->clearString($note->text); $object->save(); $note->delete(); diff --git a/app/TransactionRules/Actions/PrependNotes.php b/app/TransactionRules/Actions/PrependNotes.php index f04fe0ffd4..12cdacf466 100644 --- a/app/TransactionRules/Actions/PrependNotes.php +++ b/app/TransactionRules/Actions/PrependNotes.php @@ -46,13 +46,13 @@ class PrependNotes implements ActionInterface public function actOnArray(array $journal): bool { - $dbNote = Note::where('noteable_id', (int)$journal['transaction_journal_id']) + $dbNote = Note::where('noteable_id', (int) $journal['transaction_journal_id']) ->where('noteable_type', TransactionJournal::class) ->first(['notes.*']) ; if (null === $dbNote) { $dbNote = new Note(); - $dbNote->noteable_id = (int)$journal['transaction_journal_id']; + $dbNote->noteable_id = (int) $journal['transaction_journal_id']; $dbNote->noteable_type = TransactionJournal::class; $dbNote->text = ''; } diff --git a/app/TransactionRules/Actions/SetBudget.php b/app/TransactionRules/Actions/SetBudget.php index 067dc93a49..3063efc414 100644 --- a/app/TransactionRules/Actions/SetBudget.php +++ b/app/TransactionRules/Actions/SetBudget.php @@ -84,7 +84,7 @@ class SetBudget implements ActionInterface $object = $user->transactionJournals()->find($journal['transaction_journal_id']); $oldBudget = $object->budgets()->first(); $oldBudgetName = $oldBudget?->name; - if ((int)$oldBudget?->id === $budget->id) { + if ((int) $oldBudget?->id === $budget->id) { event(new RuleActionFailedOnArray($this->action, $journal, trans('rules.already_linked_to_budget', ['name' => $budget->name]))); return false; diff --git a/app/TransactionRules/Actions/SetCategory.php b/app/TransactionRules/Actions/SetCategory.php index 760f0e01d5..edf0176cd0 100644 --- a/app/TransactionRules/Actions/SetCategory.php +++ b/app/TransactionRules/Actions/SetCategory.php @@ -88,7 +88,7 @@ class SetCategory implements ActionInterface $object = $user->transactionJournals()->find($journal['transaction_journal_id']); $oldCategory = $object->categories()->first(); $oldCategoryName = $oldCategory?->name; - if ((int)$oldCategory?->id === $category->id) { + if ((int) $oldCategory?->id === $category->id) { // event(new RuleActionFailedOnArray($this->action, $journal, trans('rules.already_linked_to_category', ['name' => $category->name]))); return false; diff --git a/app/TransactionRules/Actions/SetDestinationAccount.php b/app/TransactionRules/Actions/SetDestinationAccount.php index 4b8498de8d..8dd35245bc 100644 --- a/app/TransactionRules/Actions/SetDestinationAccount.php +++ b/app/TransactionRules/Actions/SetDestinationAccount.php @@ -57,7 +57,7 @@ class SetDestinationAccount implements ActionInterface $user = User::find($journal['user_id']); /** @var null|TransactionJournal $object */ - $object = $user->transactionJournals()->find((int)$journal['transaction_journal_id']); + $object = $user->transactionJournals()->find((int) $journal['transaction_journal_id']); $this->repository = app(AccountRepositoryInterface::class); if (null === $object) { diff --git a/app/TransactionRules/Actions/SetDestinationToCashAccount.php b/app/TransactionRules/Actions/SetDestinationToCashAccount.php index ac985eb0f1..9ebcef779f 100644 --- a/app/TransactionRules/Actions/SetDestinationToCashAccount.php +++ b/app/TransactionRules/Actions/SetDestinationToCashAccount.php @@ -54,7 +54,7 @@ class SetDestinationToCashAccount implements ActionInterface $user = User::find($journal['user_id']); /** @var null|TransactionJournal $object */ - $object = $user->transactionJournals()->find((int)$journal['transaction_journal_id']); + $object = $user->transactionJournals()->find((int) $journal['transaction_journal_id']); $repository = app(AccountRepositoryInterface::class); if (null === $object) { diff --git a/app/TransactionRules/Actions/SetSourceAccount.php b/app/TransactionRules/Actions/SetSourceAccount.php index 9da778d2a6..dd0d5a671f 100644 --- a/app/TransactionRules/Actions/SetSourceAccount.php +++ b/app/TransactionRules/Actions/SetSourceAccount.php @@ -57,7 +57,7 @@ class SetSourceAccount implements ActionInterface $user = User::find($journal['user_id']); /** @var null|TransactionJournal $object */ - $object = $user->transactionJournals()->find((int)$journal['transaction_journal_id']); + $object = $user->transactionJournals()->find((int) $journal['transaction_journal_id']); $this->repository = app(AccountRepositoryInterface::class); if (null === $object) { app('log')->error('Could not find journal.'); diff --git a/app/TransactionRules/Actions/SetSourceToCashAccount.php b/app/TransactionRules/Actions/SetSourceToCashAccount.php index f011fc3f76..f8e8175320 100644 --- a/app/TransactionRules/Actions/SetSourceToCashAccount.php +++ b/app/TransactionRules/Actions/SetSourceToCashAccount.php @@ -54,7 +54,7 @@ class SetSourceToCashAccount implements ActionInterface $user = User::find($journal['user_id']); /** @var null|TransactionJournal $object */ - $object = $user->transactionJournals()->find((int)$journal['transaction_journal_id']); + $object = $user->transactionJournals()->find((int) $journal['transaction_journal_id']); $repository = app(AccountRepositoryInterface::class); if (null === $object) { diff --git a/app/TransactionRules/Actions/UpdatePiggybank.php b/app/TransactionRules/Actions/UpdatePiggybank.php index 3e90a79ad9..37f4db13a4 100644 --- a/app/TransactionRules/Actions/UpdatePiggybank.php +++ b/app/TransactionRules/Actions/UpdatePiggybank.php @@ -26,6 +26,7 @@ namespace FireflyIII\TransactionRules\Actions; use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray; use FireflyIII\Events\TriggeredAuditLog; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\RuleAction; use FireflyIII\Models\Transaction; @@ -81,6 +82,8 @@ class UpdatePiggybank implements ActionInterface if ($source->account_id === $piggyBank->account_id) { app('log')->debug('Piggy bank account is linked to source, so remove amount from piggy bank.'); + + throw new FireflyException('Reference the correct account here.'); $this->removeAmount($piggyBank, $journal, $journalObj, $destination->amount); event( @@ -111,11 +114,11 @@ class UpdatePiggybank implements ActionInterface 'add_to_piggy', null, [ - 'currency_symbol' => $journalObj->transactionCurrency->symbol, - 'decimal_places' => $journalObj->transactionCurrency->decimal_places, - 'amount' => $destination->amount, - 'piggy' => $piggyBank->name, - 'piggy_id' => $piggyBank->id, + 'currency_symbol' => $journalObj->transactionCurrency->symbol, + 'decimal_places' => $journalObj->transactionCurrency->decimal_places, + 'amount' => $destination->amount, + 'piggy' => $piggyBank->name, + 'piggy_id' => $piggyBank->id, ] ) ); @@ -161,6 +164,7 @@ class UpdatePiggybank implements ActionInterface } // make sure we can remove amount: + throw new FireflyException('Reference the correct account here.'); if (false === $repository->canRemoveAmount($piggyBank, $amount)) { app('log')->warning(sprintf('Cannot remove %s from piggy bank.', $amount)); event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_remove_from_piggy', ['amount' => $amount, 'name' => $piggyBank->name]))); @@ -169,6 +173,7 @@ class UpdatePiggybank implements ActionInterface } app('log')->debug(sprintf('Will now remove %s from piggy bank.', $amount)); + throw new FireflyException('Reference the correct account here.'); $repository->removeAmount($piggyBank, $amount, $journal); } @@ -178,15 +183,15 @@ class UpdatePiggybank implements ActionInterface $repository->setUser($journal->user); // how much can we add to the piggy bank? - if (0 !== bccomp($piggyBank->targetamount, '0')) { - $toAdd = bcsub($piggyBank->targetamount, $repository->getCurrentAmount($piggyBank)); + if (0 !== bccomp($piggyBank->target_amount, '0')) { + $toAdd = bcsub($piggyBank->target_amount, $repository->getCurrentAmount($piggyBank)); app('log')->debug(sprintf('Max amount to add to piggy bank is %s, amount is %s', $toAdd, $amount)); // update amount to fit: $amount = -1 === bccomp($amount, $toAdd) ? $amount : $toAdd; app('log')->debug(sprintf('Amount is now %s', $amount)); } - if (0 === bccomp($piggyBank->targetamount, '0')) { + if (0 === bccomp($piggyBank->target_amount, '0')) { app('log')->debug('Target amount is zero, can add anything.'); } @@ -199,6 +204,7 @@ class UpdatePiggybank implements ActionInterface } // make sure we can add amount: + throw new FireflyException('Reference the correct account here.'); if (false === $repository->canAddAmount($piggyBank, $amount)) { app('log')->warning(sprintf('Cannot add %s to piggy bank.', $amount)); event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_add_to_piggy', ['amount' => $amount, 'name' => $piggyBank->name]))); diff --git a/app/TransactionRules/Engine/SearchRuleEngine.php b/app/TransactionRules/Engine/SearchRuleEngine.php index d0c7025ebd..d06a20de72 100644 --- a/app/TransactionRules/Engine/SearchRuleEngine.php +++ b/app/TransactionRules/Engine/SearchRuleEngine.php @@ -114,7 +114,7 @@ class SearchRuleEngine implements RuleEngineInterface } // if the trigger needs no context, value is different: - $needsContext = (bool)(config(sprintf('search.operators.%s.needs_context', $contextSearch)) ?? true); + $needsContext = (bool) (config(sprintf('search.operators.%s.needs_context', $contextSearch)) ?? true); if (false === $needsContext) { app('log')->debug(sprintf('SearchRuleEngine:: add a rule trigger (no context): %s:true', $ruleTrigger->trigger_type)); $searchArray[$ruleTrigger->trigger_type][] = 'true'; @@ -187,7 +187,7 @@ class SearchRuleEngine implements RuleEngineInterface $journalId = 0; foreach ($array as $triggerName => $values) { if ('journal_id' === $triggerName && is_array($values) && 1 === count($values)) { - $journalId = (int)trim($values[0] ?? '"0"', '"'); // follows format "123". + $journalId = (int) trim($values[0] ?? '"0"', '"'); // follows format "123". app('log')->debug(sprintf('Found journal ID #%d', $journalId)); } } @@ -476,7 +476,7 @@ class SearchRuleEngine implements RuleEngineInterface private function addNotes(array $transaction): array { $transaction['notes'] = ''; - $dbNote = Note::where('noteable_id', (int)$transaction['transaction_journal_id'])->where('noteable_type', TransactionJournal::class)->first(['notes.*']); + $dbNote = Note::where('noteable_id', (int) $transaction['transaction_journal_id'])->where('noteable_type', TransactionJournal::class)->first(['notes.*']); if (null !== $dbNote) { $transaction['notes'] = $dbNote->text; } diff --git a/app/TransactionRules/Expressions/ActionExpression.php b/app/TransactionRules/Expressions/ActionExpression.php index ec73675d1d..fbabc1a3eb 100644 --- a/app/TransactionRules/Expressions/ActionExpression.php +++ b/app/TransactionRules/Expressions/ActionExpression.php @@ -144,7 +144,7 @@ class ActionExpression { $result = $this->expressionLanguage->evaluate($expr, $journal); - return (string)$result; + return (string) $result; } public function evaluate(array $journal): string diff --git a/app/TransactionRules/Expressions/ActionExpressionLanguageProvider.php b/app/TransactionRules/Expressions/ActionExpressionLanguageProvider.php index 678b54ee2e..f871b3390a 100644 --- a/app/TransactionRules/Expressions/ActionExpressionLanguageProvider.php +++ b/app/TransactionRules/Expressions/ActionExpressionLanguageProvider.php @@ -36,7 +36,7 @@ class ActionExpressionLanguageProvider implements ExpressionFunctionProviderInte { $function = function ($arguments, $str): string { if (!is_string($str)) { - return (string)$str; + return (string) $str; } return strtolower($str.'!'); diff --git a/app/TransactionRules/Traits/RefreshNotesTrait.php b/app/TransactionRules/Traits/RefreshNotesTrait.php index 37355b9602..a9ab866a8c 100644 --- a/app/TransactionRules/Traits/RefreshNotesTrait.php +++ b/app/TransactionRules/Traits/RefreshNotesTrait.php @@ -33,7 +33,7 @@ trait RefreshNotesTrait final protected function refreshNotes(array $transaction): array { $transaction['notes'] = ''; - $dbNote = Note::where('noteable_id', (int)$transaction['transaction_journal_id'])->where('noteable_type', TransactionJournal::class)->first(['notes.*']); + $dbNote = Note::where('noteable_id', (int) $transaction['transaction_journal_id'])->where('noteable_type', TransactionJournal::class)->first(['notes.*']); if (null !== $dbNote) { $transaction['notes'] = $dbNote->text; } diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index 609e26cefc..70212444b4 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -28,6 +28,7 @@ use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use Symfony\Component\HttpFoundation\ParameterBag; /** @@ -57,8 +58,8 @@ class AccountTransformer extends AbstractTransformer // get account type: $fullType = $account->accountType->type; - $accountType = (string)config(sprintf('firefly.shortNamesByFullName.%s', $fullType)); - $liabilityType = (string)config(sprintf('firefly.shortLiabilityNameByFullName.%s', $fullType)); + $accountType = (string) config(sprintf('firefly.shortNamesByFullName.%s', $fullType)); + $liabilityType = (string) config(sprintf('firefly.shortLiabilityNameByFullName.%s', $fullType)); $liabilityType = '' === $liabilityType ? null : strtolower($liabilityType); $liabilityDirection = $this->repository->getMetaValue($account, 'liability_direction'); @@ -81,7 +82,7 @@ class AccountTransformer extends AbstractTransformer if (null !== $location) { $longitude = $location->longitude; $latitude = $location->latitude; - $zoomLevel = (int)$location->zoom_level; + $zoomLevel = (int) $location->zoom_level; } // no order for some accounts: @@ -91,7 +92,7 @@ class AccountTransformer extends AbstractTransformer } return [ - 'id' => (string)$account->id, + 'id' => (string) $account->id, 'created_at' => $account->created_at->toAtomString(), 'updated_at' => $account->updated_at->toAtomString(), 'active' => $account->active, @@ -103,7 +104,7 @@ class AccountTransformer extends AbstractTransformer 'currency_code' => $currencyCode, 'currency_symbol' => $currencySymbol, 'currency_decimal_places' => $decimalPlaces, - 'current_balance' => app('steam')->bcround(app('steam')->balance($account, $date), $decimalPlaces), + 'current_balance' => app('steam')->bcround(Steam::finalAccountBalance($account, $date)['balance'] ?? '0', $decimalPlaces), 'current_balance_date' => $date->toAtomString(), 'notes' => $this->repository->getNoteText($account), 'monthly_payment_date' => $monthlyPaymentDate, @@ -135,7 +136,7 @@ class AccountTransformer extends AbstractTransformer private function getAccountRole(Account $account, string $accountType): ?string { $accountRole = $this->repository->getMetaValue($account, 'account_role'); - if ('asset' !== $accountType || '' === (string)$accountRole) { + if ('asset' !== $accountType || '' === (string) $accountRole) { $accountRole = null; } @@ -166,7 +167,7 @@ class AccountTransformer extends AbstractTransformer if (null === $currency) { $currency = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); } - $currencyId = (string)$currency->id; + $currencyId = (string) $currency->id; $currencyCode = $currency->code; $decimalPlaces = $currency->decimal_places; $currencySymbol = $currency->symbol; diff --git a/app/Transformers/AttachmentTransformer.php b/app/Transformers/AttachmentTransformer.php index 0125c08afc..208487459b 100644 --- a/app/Transformers/AttachmentTransformer.php +++ b/app/Transformers/AttachmentTransformer.php @@ -50,10 +50,10 @@ class AttachmentTransformer extends AbstractTransformer $this->repository->setUser($attachment->user); return [ - 'id' => (string)$attachment->id, + 'id' => (string) $attachment->id, 'created_at' => $attachment->created_at->toAtomString(), 'updated_at' => $attachment->updated_at->toAtomString(), - 'attachable_id' => (string)$attachment->attachable_id, + 'attachable_id' => (string) $attachment->attachable_id, 'attachable_type' => str_replace('FireflyIII\Models\\', '', $attachment->attachable_type), 'md5' => $attachment->md5, 'filename' => $attachment->filename, @@ -62,7 +62,7 @@ class AttachmentTransformer extends AbstractTransformer 'title' => $attachment->title, 'notes' => $this->repository->getNoteText($attachment), 'mime' => $attachment->mime, - 'size' => (int)$attachment->size, + 'size' => (int) $attachment->size, 'links' => [ [ 'rel' => 'self', diff --git a/app/Transformers/AvailableBudgetTransformer.php b/app/Transformers/AvailableBudgetTransformer.php index dbbc91b8ee..d58a6bebd6 100644 --- a/app/Transformers/AvailableBudgetTransformer.php +++ b/app/Transformers/AvailableBudgetTransformer.php @@ -57,10 +57,10 @@ class AvailableBudgetTransformer extends AbstractTransformer $currency = $availableBudget->transactionCurrency; $data = [ - 'id' => (string)$availableBudget->id, + 'id' => (string) $availableBudget->id, 'created_at' => $availableBudget->created_at->toAtomString(), 'updated_at' => $availableBudget->updated_at->toAtomString(), - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, diff --git a/app/Transformers/BillTransformer.php b/app/Transformers/BillTransformer.php index 375a1e6bec..a9de92351d 100644 --- a/app/Transformers/BillTransformer.php +++ b/app/Transformers/BillTransformer.php @@ -30,6 +30,7 @@ use FireflyIII\Models\Bill; use FireflyIII\Models\ObjectGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Models\BillDateCalculator; use Illuminate\Support\Collection; @@ -58,6 +59,8 @@ class BillTransformer extends AbstractTransformer */ public function transform(Bill $bill): array { + $defaultCurrency = $this->parameters->get('defaultCurrency') ?? Amount::getDefaultCurrency(); + $paidData = $this->paidData($bill); $lastPaidDate = $this->getLastPaidDate($paidData); $start = $this->parameters->get('start') ?? today()->subYears(10); @@ -142,13 +145,15 @@ class BillTransformer extends AbstractTransformer 'id' => $bill->id, 'created_at' => $bill->created_at->toAtomString(), 'updated_at' => $bill->updated_at->toAtomString(), - 'currency_id' => (string)$bill->transaction_currency_id, + 'currency_id' => (string) $bill->transaction_currency_id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, 'name' => $bill->name, 'amount_min' => app('steam')->bcround($bill->amount_min, $currency->decimal_places), 'amount_max' => app('steam')->bcround($bill->amount_max, $currency->decimal_places), + 'native_amount_min' => app('steam')->bcround($bill->native_amount_min, $defaultCurrency->decimal_places), + 'native_amount_max' => app('steam')->bcround($bill->native_amount_max, $defaultCurrency->decimal_places), 'date' => $bill->date->toAtomString(), 'end_date' => $bill->end_date?->toAtomString(), 'extension_date' => $bill->extension_date?->toAtomString(), @@ -157,7 +162,7 @@ class BillTransformer extends AbstractTransformer 'active' => $bill->active, 'order' => $bill->order, 'notes' => $notes, - 'object_group_id' => null !== $objectGroupId ? (string)$objectGroupId : null, + 'object_group_id' => null !== $objectGroupId ? (string) $objectGroupId : null, 'object_group_order' => $objectGroupOrder, 'object_group_title' => $objectGroupTitle, @@ -219,8 +224,8 @@ class BillTransformer extends AbstractTransformer $result = []; foreach ($set as $entry) { $result[] = [ - 'transaction_group_id' => (string)$entry->transaction_group_id, - 'transaction_journal_id' => (string)$entry->id, + 'transaction_group_id' => (string) $entry->transaction_group_id, + 'transaction_journal_id' => (string) $entry->id, 'date' => $entry->date->format('Y-m-d'), 'date_object' => $entry->date, ]; diff --git a/app/Transformers/BudgetLimitTransformer.php b/app/Transformers/BudgetLimitTransformer.php index ad8225ceec..afac7c5684 100644 --- a/app/Transformers/BudgetLimitTransformer.php +++ b/app/Transformers/BudgetLimitTransformer.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; use FireflyIII\Models\BudgetLimit; +use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepository; use Illuminate\Support\Collection; use League\Fractal\Resource\Item; @@ -55,7 +56,9 @@ class BudgetLimitTransformer extends AbstractTransformer public function transform(BudgetLimit $budgetLimit): array { $repository = app(OperationsRepository::class); + $limitRepos = app(BudgetLimitRepositoryInterface::class); $repository->setUser($budgetLimit->budget->user); + $limitRepos->setUser($budgetLimit->budget->user); $expenses = $repository->sumExpenses( $budgetLimit->start_date, $budgetLimit->end_date, @@ -65,6 +68,7 @@ class BudgetLimitTransformer extends AbstractTransformer ); $currency = $budgetLimit->transactionCurrency; $amount = $budgetLimit->amount; + $notes = $limitRepos->getNoteText($budgetLimit); $currencyDecimalPlaces = 2; $currencyId = null; $currencyName = null; @@ -81,13 +85,13 @@ class BudgetLimitTransformer extends AbstractTransformer $amount = app('steam')->bcround($amount, $currencyDecimalPlaces); return [ - 'id' => (string)$budgetLimit->id, + 'id' => (string) $budgetLimit->id, 'created_at' => $budgetLimit->created_at->toAtomString(), 'updated_at' => $budgetLimit->updated_at->toAtomString(), 'start' => $budgetLimit->start_date->toAtomString(), 'end' => $budgetLimit->end_date->endOfDay()->toAtomString(), - 'budget_id' => (string)$budgetLimit->budget_id, - 'currency_id' => (string)$currencyId, + 'budget_id' => (string) $budgetLimit->budget_id, + 'currency_id' => (string) $currencyId, 'currency_code' => $currencyCode, 'currency_name' => $currencyName, 'currency_decimal_places' => $currencyDecimalPlaces, @@ -95,6 +99,7 @@ class BudgetLimitTransformer extends AbstractTransformer 'amount' => $amount, 'period' => $budgetLimit->period, 'spent' => $expenses[$currencyId]['sum'] ?? '0', + 'notes' => '' === $notes ? null : $notes, 'links' => [ [ 'rel' => 'self', diff --git a/app/Transformers/BudgetTransformer.php b/app/Transformers/BudgetTransformer.php index a88a54cc2d..da818c671a 100644 --- a/app/Transformers/BudgetTransformer.php +++ b/app/Transformers/BudgetTransformer.php @@ -77,7 +77,7 @@ class BudgetTransformer extends AbstractTransformer ]; if (null !== $autoBudget) { - $abCurrencyId = (string)$autoBudget->transactionCurrency->id; + $abCurrencyId = (string) $autoBudget->transactionCurrency->id; $abCurrencyCode = $autoBudget->transactionCurrency->code; $abType = $types[$autoBudget->auto_budget_type]; $abAmount = app('steam')->bcround($autoBudget->amount, $autoBudget->transactionCurrency->decimal_places); @@ -85,7 +85,7 @@ class BudgetTransformer extends AbstractTransformer } return [ - 'id' => (string)$budget->id, + 'id' => (string) $budget->id, 'created_at' => $budget->created_at->toAtomString(), 'updated_at' => $budget->updated_at->toAtomString(), 'active' => $budget->active, @@ -111,7 +111,7 @@ class BudgetTransformer extends AbstractTransformer { $return = []; foreach ($array as $data) { - $data['sum'] = app('steam')->bcround($data['sum'], (int)$data['currency_decimal_places']); + $data['sum'] = app('steam')->bcround($data['sum'], (int) $data['currency_decimal_places']); $return[] = $data; } diff --git a/app/Transformers/CategoryTransformer.php b/app/Transformers/CategoryTransformer.php index 64337993e2..bc6fa8f64e 100644 --- a/app/Transformers/CategoryTransformer.php +++ b/app/Transformers/CategoryTransformer.php @@ -85,7 +85,7 @@ class CategoryTransformer extends AbstractTransformer { $return = []; foreach ($array as $data) { - $data['sum'] = app('steam')->bcround($data['sum'], (int)$data['currency_decimal_places']); + $data['sum'] = app('steam')->bcround($data['sum'], (int) $data['currency_decimal_places']); $return[] = $data; } diff --git a/app/Transformers/ObjectGroupTransformer.php b/app/Transformers/ObjectGroupTransformer.php index 7fc833fdb0..fa38e1d819 100644 --- a/app/Transformers/ObjectGroupTransformer.php +++ b/app/Transformers/ObjectGroupTransformer.php @@ -50,7 +50,7 @@ class ObjectGroupTransformer extends AbstractTransformer $this->repository->setUser($objectGroup->user); return [ - 'id' => (string)$objectGroup->id, + 'id' => (string) $objectGroup->id, 'created_at' => $objectGroup->created_at?->toAtomString(), 'updated_at' => $objectGroup->updated_at?->toAtomString(), 'title' => $objectGroup->title, diff --git a/app/Transformers/PiggyBankEventTransformer.php b/app/Transformers/PiggyBankEventTransformer.php index db39d8cb3b..657fb7b560 100644 --- a/app/Transformers/PiggyBankEventTransformer.php +++ b/app/Transformers/PiggyBankEventTransformer.php @@ -66,22 +66,22 @@ class PiggyBankEventTransformer extends AbstractTransformer // get associated journal and transaction, if any: $journalId = $event->transaction_journal_id; $groupId = null; - if (0 !== (int)$journalId) { - $groupId = (int)$event->transactionJournal->transaction_group_id; - $journalId = (int)$journalId; + if (0 !== (int) $journalId) { + $groupId = (int) $event->transactionJournal->transaction_group_id; + $journalId = (int) $journalId; } return [ - 'id' => (string)$event->id, + 'id' => (string) $event->id, 'created_at' => $event->created_at->toAtomString(), 'updated_at' => $event->updated_at->toAtomString(), 'amount' => app('steam')->bcround($event->amount, $currency->decimal_places), - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, - 'transaction_journal_id' => null !== $journalId ? (string)$journalId : null, - 'transaction_group_id' => null !== $groupId ? (string)$groupId : null, + 'transaction_journal_id' => null !== $journalId ? (string) $journalId : null, + 'transaction_group_id' => null !== $groupId ? (string) $groupId : null, 'links' => [ [ 'rel' => 'self', diff --git a/app/Transformers/PiggyBankTransformer.php b/app/Transformers/PiggyBankTransformer.php index aaccf20c63..c1cf567fa3 100644 --- a/app/Transformers/PiggyBankTransformer.php +++ b/app/Transformers/PiggyBankTransformer.php @@ -54,14 +54,11 @@ class PiggyBankTransformer extends AbstractTransformer */ public function transform(PiggyBank $piggyBank): array { - $account = $piggyBank->account; + $user = $piggyBank->accounts()->first()->user; // set up repositories - $this->accountRepos->setUser($account->user); - $this->piggyRepos->setUser($account->user); - - // get currency from account, or use default. - $currency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); + $this->accountRepos->setUser($user); + $this->piggyRepos->setUser($user); // note $notes = $this->piggyRepos->getNoteText($piggyBank); @@ -80,31 +77,33 @@ class PiggyBankTransformer extends AbstractTransformer } // get currently saved amount: - $currentAmount = app('steam')->bcround($this->piggyRepos->getCurrentAmount($piggyBank), $currency->decimal_places); + $currency = $piggyBank->transactionCurrency; + $currentAmount = $this->piggyRepos->getCurrentAmount($piggyBank); // Amounts, depending on 0.0 state of target amount $percentage = null; - $targetAmount = $piggyBank->targetamount; + $targetAmount = $piggyBank->target_amount; $leftToSave = null; $savePerMonth = null; if (0 !== bccomp($targetAmount, '0')) { // target amount is not 0.00 - $leftToSave = bcsub($piggyBank->targetamount, $currentAmount); - $percentage = (int)bcmul(bcdiv($currentAmount, $targetAmount), '100'); + $leftToSave = bcsub($piggyBank->target_amount, $currentAmount); + $percentage = (int) bcmul(bcdiv($currentAmount, $targetAmount), '100'); $targetAmount = app('steam')->bcround($targetAmount, $currency->decimal_places); $leftToSave = app('steam')->bcround($leftToSave, $currency->decimal_places); $savePerMonth = app('steam')->bcround($this->piggyRepos->getSuggestedMonthlyAmount($piggyBank), $currency->decimal_places); } - $startDate = $piggyBank->startdate?->format('Y-m-d'); - $targetDate = $piggyBank->targetdate?->format('Y-m-d'); + $startDate = $piggyBank->start_date?->format('Y-m-d'); + $targetDate = $piggyBank->target_date?->format('Y-m-d'); return [ - 'id' => (string)$piggyBank->id, + 'id' => (string) $piggyBank->id, 'created_at' => $piggyBank->created_at->toAtomString(), 'updated_at' => $piggyBank->updated_at->toAtomString(), - 'account_id' => (string)$piggyBank->account_id, - 'account_name' => $piggyBank->account->name, + 'accounts' => $this->renderAccounts($piggyBank), + // 'account_id' => (string)$piggyBank->account_id, + // 'account_name' => $piggyBank->account->name, 'name' => $piggyBank->name, - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, @@ -118,7 +117,7 @@ class PiggyBankTransformer extends AbstractTransformer 'order' => $piggyBank->order, 'active' => true, 'notes' => $notes, - 'object_group_id' => null !== $objectGroupId ? (string)$objectGroupId : null, + 'object_group_id' => null !== $objectGroupId ? (string) $objectGroupId : null, 'object_group_order' => $objectGroupOrder, 'object_group_title' => $objectGroupTitle, 'links' => [ @@ -129,4 +128,19 @@ class PiggyBankTransformer extends AbstractTransformer ], ]; } + + private function renderAccounts(PiggyBank $piggyBank): array + { + $return = []; + foreach ($piggyBank->accounts()->get() as $account) { + $return[] = [ + 'id' => $account->id, + 'name' => $account->name, + 'current_amount' => (string) $account->pivot->current_amount, + // TODO add balance, add left to save. + ]; + } + + return $return; + } } diff --git a/app/Transformers/RecurrenceTransformer.php b/app/Transformers/RecurrenceTransformer.php index 5739406b8d..8eecfbcf3e 100644 --- a/app/Transformers/RecurrenceTransformer.php +++ b/app/Transformers/RecurrenceTransformer.php @@ -74,14 +74,14 @@ class RecurrenceTransformer extends AbstractTransformer $this->budgetRepos->setUser($recurrence->user); app('log')->debug('Set user.'); - $shortType = (string)config(sprintf('firefly.transactionTypesToShort.%s', $recurrence->transactionType->type)); + $shortType = (string) config(sprintf('firefly.transactionTypesToShort.%s', $recurrence->transactionType->type)); $notes = $this->repository->getNoteText($recurrence); - $reps = 0 === (int)$recurrence->repetitions ? null : (int)$recurrence->repetitions; + $reps = 0 === (int) $recurrence->repetitions ? null : (int) $recurrence->repetitions; app('log')->debug('Get basic data.'); // basic data. return [ - 'id' => (string)$recurrence->id, + 'id' => (string) $recurrence->id, 'created_at' => $recurrence->created_at->toAtomString(), 'updated_at' => $recurrence->updated_at->toAtomString(), 'type' => $shortType, @@ -117,7 +117,7 @@ class RecurrenceTransformer extends AbstractTransformer /** @var RecurrenceRepetition $repetition */ foreach ($recurrence->recurrenceRepetitions as $repetition) { $repetitionArray = [ - 'id' => (string)$repetition->id, + 'id' => (string) $repetition->id, 'created_at' => $repetition->created_at->toAtomString(), 'updated_at' => $repetition->updated_at->toAtomString(), 'type' => $repetition->repetition_type, @@ -164,7 +164,7 @@ class RecurrenceTransformer extends AbstractTransformer $foreignCurrencyDp = null; $foreignCurrencyId = null; if (null !== $transaction->foreign_currency_id) { - $foreignCurrencyId = (int)$transaction->foreign_currency_id; + $foreignCurrencyId = (int) $transaction->foreign_currency_id; $foreignCurrencyCode = $transaction->foreignCurrency->code; $foreignCurrencySymbol = $transaction->foreignCurrency->symbol; $foreignCurrencyDp = $transaction->foreignCurrency->decimal_places; @@ -197,20 +197,20 @@ class RecurrenceTransformer extends AbstractTransformer $foreignAmount = app('steam')->bcround($transaction->foreign_amount, $foreignCurrencyDp); } $transactionArray = [ - 'id' => (string)$transaction->id, - 'currency_id' => (string)$transaction->transaction_currency_id, + 'id' => (string) $transaction->id, + 'currency_id' => (string) $transaction->transaction_currency_id, 'currency_code' => $transaction->transactionCurrency->code, 'currency_symbol' => $transaction->transactionCurrency->symbol, 'currency_decimal_places' => $transaction->transactionCurrency->decimal_places, - 'foreign_currency_id' => null === $foreignCurrencyId ? null : (string)$foreignCurrencyId, + 'foreign_currency_id' => null === $foreignCurrencyId ? null : (string) $foreignCurrencyId, 'foreign_currency_code' => $foreignCurrencyCode, 'foreign_currency_symbol' => $foreignCurrencySymbol, 'foreign_currency_decimal_places' => $foreignCurrencyDp, - 'source_id' => (string)$sourceId, + 'source_id' => (string) $sourceId, 'source_name' => $sourceName, 'source_iban' => $sourceIban, 'source_type' => $sourceType, - 'destination_id' => (string)$destinationId, + 'destination_id' => (string) $destinationId, 'destination_name' => $destinationName, 'destination_iban' => $destinationIban, 'destination_type' => $destinationType, @@ -255,9 +255,9 @@ class RecurrenceTransformer extends AbstractTransformer throw new FireflyException(sprintf('Recurrence transformer cant handle field "%s"', $transactionMeta->name)); case 'bill_id': - $bill = $this->billRepos->find((int)$transactionMeta->value); + $bill = $this->billRepos->find((int) $transactionMeta->value); if (null !== $bill) { - $array['bill_id'] = (string)$bill->id; + $array['bill_id'] = (string) $bill->id; $array['bill_name'] = $bill->name; } @@ -269,18 +269,18 @@ class RecurrenceTransformer extends AbstractTransformer break; case 'piggy_bank_id': - $piggy = $this->piggyRepos->find((int)$transactionMeta->value); + $piggy = $this->piggyRepos->find((int) $transactionMeta->value); if (null !== $piggy) { - $array['piggy_bank_id'] = (string)$piggy->id; + $array['piggy_bank_id'] = (string) $piggy->id; $array['piggy_bank_name'] = $piggy->name; } break; case 'category_id': - $category = $this->factory->findOrCreate((int)$transactionMeta->value, null); + $category = $this->factory->findOrCreate((int) $transactionMeta->value, null); if (null !== $category) { - $array['category_id'] = (string)$category->id; + $array['category_id'] = (string) $category->id; $array['category_name'] = $category->name; } @@ -289,16 +289,16 @@ class RecurrenceTransformer extends AbstractTransformer case 'category_name': $category = $this->factory->findOrCreate(null, $transactionMeta->value); if (null !== $category) { - $array['category_id'] = (string)$category->id; + $array['category_id'] = (string) $category->id; $array['category_name'] = $category->name; } break; case 'budget_id': - $budget = $this->budgetRepos->find((int)$transactionMeta->value); + $budget = $this->budgetRepos->find((int) $transactionMeta->value); if (null !== $budget) { - $array['budget_id'] = (string)$budget->id; + $array['budget_id'] = (string) $budget->id; $array['budget_name'] = $budget->name; } diff --git a/app/Transformers/RuleTransformer.php b/app/Transformers/RuleTransformer.php index b98d2e727f..313521ce50 100644 --- a/app/Transformers/RuleTransformer.php +++ b/app/Transformers/RuleTransformer.php @@ -55,11 +55,11 @@ class RuleTransformer extends AbstractTransformer $this->ruleRepository->setUser($rule->user); return [ - 'id' => (string)$rule->id, + 'id' => (string) $rule->id, 'created_at' => $rule->created_at->toAtomString(), 'updated_at' => $rule->updated_at->toAtomString(), - 'rule_group_id' => (string)$rule->rule_group_id, - 'rule_group_title' => (string)$rule->ruleGroup->title, + 'rule_group_id' => (string) $rule->rule_group_id, + 'rule_group_title' => (string) $rule->ruleGroup->title, 'title' => $rule->title, 'description' => $rule->description, 'order' => $rule->order, @@ -110,7 +110,7 @@ class RuleTransformer extends AbstractTransformer continue; } $triggerType = (string) $ruleTrigger->trigger_type; - $triggerValue = (string)$ruleTrigger->trigger_value; + $triggerValue = (string) $ruleTrigger->trigger_value; $prohibited = false; if (str_starts_with($triggerType, '-')) { @@ -124,7 +124,7 @@ class RuleTransformer extends AbstractTransformer } $result[] = [ - 'id' => (string)$ruleTrigger->id, + 'id' => (string) $ruleTrigger->id, 'created_at' => $ruleTrigger->created_at->toAtomString(), 'updated_at' => $ruleTrigger->updated_at->toAtomString(), 'type' => $triggerType, @@ -147,7 +147,7 @@ class RuleTransformer extends AbstractTransformer /** @var RuleAction $ruleAction */ foreach ($actions as $ruleAction) { $result[] = [ - 'id' => (string)$ruleAction->id, + 'id' => (string) $ruleAction->id, 'created_at' => $ruleAction->created_at->toAtomString(), 'updated_at' => $ruleAction->updated_at->toAtomString(), 'type' => $ruleAction->action_type, diff --git a/app/Transformers/TagTransformer.php b/app/Transformers/TagTransformer.php index 4b4acf81e8..d9222069e9 100644 --- a/app/Transformers/TagTransformer.php +++ b/app/Transformers/TagTransformer.php @@ -49,7 +49,7 @@ class TagTransformer extends AbstractTransformer if (null !== $location) { $latitude = $location->latitude; $longitude = $location->longitude; - $zoomLevel = (int)$location->zoom_level; + $zoomLevel = (int) $location->zoom_level; } return [ diff --git a/app/Transformers/TransactionGroupTransformer.php b/app/Transformers/TransactionGroupTransformer.php index 94c8ea2280..b5a66b4458 100644 --- a/app/Transformers/TransactionGroupTransformer.php +++ b/app/Transformers/TransactionGroupTransformer.php @@ -81,10 +81,10 @@ class TransactionGroupTransformer extends AbstractTransformer $first = new NullArrayObject(reset($group['transactions'])); return [ - 'id' => (int)$first['transaction_group_id'], + 'id' => (int) $first['transaction_group_id'], 'created_at' => $first['created_at']->toAtomString(), 'updated_at' => $first['updated_at']->toAtomString(), - 'user' => (string)$data['user_id'], + 'user' => (string) $data['user_id'], 'group_title' => $data['title'], 'transactions' => $this->transformTransactions($data), 'links' => [ @@ -115,20 +115,20 @@ class TransactionGroupTransformer extends AbstractTransformer $row = new NullArrayObject($transaction); // amount: - $amount = app('steam')->positive((string)($row['amount'] ?? '0')); + $amount = app('steam')->positive((string) ($row['amount'] ?? '0')); $foreignAmount = null; if (null !== $row['foreign_amount'] && '' !== $row['foreign_amount'] && 0 !== bccomp('0', $row['foreign_amount'])) { $foreignAmount = app('steam')->positive($row['foreign_amount']); } - $metaFieldData = $this->groupRepos->getMetaFields((int)$row['transaction_journal_id'], $this->metaFields); - $metaDateData = $this->groupRepos->getMetaDateFields((int)$row['transaction_journal_id'], $this->metaDateFields); + $metaFieldData = $this->groupRepos->getMetaFields((int) $row['transaction_journal_id'], $this->metaFields); + $metaDateData = $this->groupRepos->getMetaDateFields((int) $row['transaction_journal_id'], $this->metaDateFields); $type = $this->stringFromArray($transaction, 'transaction_type_type', TransactionType::WITHDRAWAL); $longitude = null; $latitude = null; $zoomLevel = null; - $location = $this->getLocationById((int)$row['transaction_journal_id']); + $location = $this->getLocationById((int) $row['transaction_journal_id']); if (null !== $location) { $longitude = $location->longitude; $latitude = $location->latitude; @@ -136,17 +136,17 @@ class TransactionGroupTransformer extends AbstractTransformer } return [ - 'user' => (string)$row['user_id'], - 'transaction_journal_id' => (string)$row['transaction_journal_id'], + 'user' => (string) $row['user_id'], + 'transaction_journal_id' => (string) $row['transaction_journal_id'], 'type' => strtolower($type), 'date' => $row['date']->toAtomString(), 'order' => $row['order'], - 'currency_id' => (string)$row['currency_id'], + 'currency_id' => (string) $row['currency_id'], 'currency_code' => $row['currency_code'], 'currency_name' => $row['currency_name'], 'currency_symbol' => $row['currency_symbol'], - 'currency_decimal_places' => (int)$row['currency_decimal_places'], + 'currency_decimal_places' => (int) $row['currency_decimal_places'], 'foreign_currency_id' => $this->stringFromArray($transaction, 'foreign_currency_id', null), 'foreign_currency_code' => $row['foreign_currency_code'], @@ -158,12 +158,12 @@ class TransactionGroupTransformer extends AbstractTransformer 'description' => $row['description'], - 'source_id' => (string)$row['source_account_id'], + 'source_id' => (string) $row['source_account_id'], 'source_name' => $row['source_account_name'], 'source_iban' => $row['source_account_iban'], 'source_type' => $row['source_account_type'], - 'destination_id' => (string)$row['destination_account_id'], + 'destination_id' => (string) $row['destination_account_id'], 'destination_name' => $row['destination_account_name'], 'destination_iban' => $row['destination_account_iban'], 'destination_type' => $row['destination_account_type'], @@ -178,8 +178,8 @@ class TransactionGroupTransformer extends AbstractTransformer 'bill_name' => $row['bill_name'], 'reconciled' => $row['reconciled'], - 'notes' => $this->groupRepos->getNoteText((int)$row['transaction_journal_id']), - 'tags' => $this->groupRepos->getTags((int)$row['transaction_journal_id']), + 'notes' => $this->groupRepos->getNoteText((int) $row['transaction_journal_id']), + 'tags' => $this->groupRepos->getTags((int) $row['transaction_journal_id']), 'internal_reference' => $metaFieldData['internal_reference'], 'external_id' => $metaFieldData['external_id'], @@ -212,7 +212,7 @@ class TransactionGroupTransformer extends AbstractTransformer 'latitude' => $latitude, 'zoom_level' => $zoomLevel, - 'has_attachments' => $this->hasAttachments((int)$row['transaction_journal_id']), + 'has_attachments' => $this->hasAttachments((int) $row['transaction_journal_id']), ]; } @@ -229,7 +229,7 @@ class TransactionGroupTransformer extends AbstractTransformer return $default; } - return (string)$array[$key]; + return (string) $array[$key]; } if (null !== $default) { @@ -252,7 +252,7 @@ class TransactionGroupTransformer extends AbstractTransformer private function integerFromArray(array $array, string $key): ?int { if (array_key_exists($key, $array)) { - return (int)$array[$key]; + return (int) $array[$key]; } return null; @@ -435,7 +435,7 @@ class TransactionGroupTransformer extends AbstractTransformer { $result = $journal->transactions->first( static function (Transaction $transaction) { - return (float)$transaction->amount < 0; // lame but it works. + return (float) $transaction->amount < 0; // lame but it works. } ); if (null === $result) { @@ -452,7 +452,7 @@ class TransactionGroupTransformer extends AbstractTransformer { $result = $journal->transactions->first( static function (Transaction $transaction) { - return (float)$transaction->amount > 0; // lame but it works + return (float) $transaction->amount > 0; // lame but it works } ); if (null === $result) { @@ -556,7 +556,7 @@ class TransactionGroupTransformer extends AbstractTransformer if (null === $bill) { return $array; } - $array['id'] = (string)$bill->id; + $array['id'] = (string) $bill->id; $array['name'] = $bill->name; return $array; diff --git a/app/Transformers/TransactionLinkTransformer.php b/app/Transformers/TransactionLinkTransformer.php index 9b68d11a03..25b83f8597 100644 --- a/app/Transformers/TransactionLinkTransformer.php +++ b/app/Transformers/TransactionLinkTransformer.php @@ -48,12 +48,12 @@ class TransactionLinkTransformer extends AbstractTransformer $notes = $this->repository->getLinkNoteText($link); return [ - 'id' => (string)$link->id, + 'id' => (string) $link->id, 'created_at' => $link->created_at->toAtomString(), 'updated_at' => $link->updated_at->toAtomString(), - 'inward_id' => (string)$link->source_id, - 'outward_id' => (string)$link->destination_id, - 'link_type_id' => (string)$link->link_type_id, + 'inward_id' => (string) $link->source_id, + 'outward_id' => (string) $link->destination_id, + 'link_type_id' => (string) $link->link_type_id, 'notes' => '' === $notes ? null : $notes, 'links' => [ [ diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php index e1662b749e..18dc74d4b6 100644 --- a/app/Transformers/UserTransformer.php +++ b/app/Transformers/UserTransformer.php @@ -45,11 +45,11 @@ class UserTransformer extends AbstractTransformer $this->repository ??= app(UserRepositoryInterface::class); return [ - 'id' => (int)$user->id, + 'id' => (int) $user->id, 'created_at' => $user->created_at->toAtomString(), 'updated_at' => $user->updated_at->toAtomString(), 'email' => $user->email, - 'blocked' => 1 === (int)$user->blocked, + 'blocked' => 1 === (int) $user->blocked, 'blocked_code' => '' === $user->blocked_code ? null : $user->blocked_code, 'role' => $this->repository->getRoleByUser($user), 'links' => [ diff --git a/app/Transformers/V2/AccountTransformer.php b/app/Transformers/V2/AccountTransformer.php index 18eb6eb5fb..0f51e9b213 100644 --- a/app/Transformers/V2/AccountTransformer.php +++ b/app/Transformers/V2/AccountTransformer.php @@ -41,11 +41,11 @@ class AccountTransformer extends AbstractTransformer { private array $accountMeta; private array $accountTypes; - private array $fullTypes; private array $balanceDifferences; private array $convertedBalances; private array $currencies; private TransactionCurrency $default; + private array $fullTypes; private array $lastActivity; private array $objectGroups; @@ -102,6 +102,25 @@ class AccountTransformer extends AbstractTransformer return $objects; } + private function getLastActivity(Collection $accounts): void + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $lastActivity = $accountRepository->getLastActivity($accounts); + foreach ($lastActivity as $row) { + $this->lastActivity[(int) $row['account_id']] = Carbon::parse($row['date_max'], config('app.timezone')); + } + } + + private function getMetaBalances(Collection $accounts): void + { + try { + $this->convertedBalances = app('steam')->finalAccountsBalance($accounts, $this->getDate()); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + } + } + private function getDate(): Carbon { $date = today(config('app.timezone')); @@ -112,6 +131,189 @@ class AccountTransformer extends AbstractTransformer return $date; } + private function getDefaultCurrency(): void + { + $this->default = app('amount')->getDefaultCurrency(); + } + + private function collectAccountMetaData(Collection $accounts): void + { + /** @var CurrencyRepositoryInterface $repository */ + $repository = app(CurrencyRepositoryInterface::class); + + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $metaFields = $accountRepository->getMetaValues($accounts, ['currency_id', 'account_role', 'account_number', 'liability_direction', 'interest', 'interest_period', 'current_debt']); + $currencyIds = $metaFields->where('name', 'currency_id')->pluck('data')->toArray(); + + $currencies = $repository->getByIds($currencyIds); + foreach ($currencies as $currency) { + $id = $currency->id; + $this->currencies[$id] = $currency; + } + foreach ($metaFields as $entry) { + $id = $entry->account_id; + $this->accountMeta[$id][$entry->name] = $entry->data; + } + } + + private function collectAccountTypes(Collection $accounts): void + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accountTypes = $accountRepository->getAccountTypes($accounts); + + /** @var AccountType $row */ + foreach ($accountTypes as $row) { + $this->accountTypes[$row->id] = (string) config(sprintf('firefly.shortNamesByFullName.%s', $row->type)); + $this->fullTypes[$row->id] = $row->type; + } + } + + private function getBalanceDifference(Collection $accounts, Carbon $start, Carbon $end): void + { + throw new FireflyException('Used deprecated method, rethink this.'); + // collect balances, start and end for both native and converted. + // yes the b is usually used for boolean by idiots but here it's for balance. + $bStart = []; + $bEnd = []; + + try { + $bStart = app('steam')->finalAccountsBalance($accounts, $start); + $bEnd = app('steam')->finalAccountsBalance($accounts, $end); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + } + + /** @var Account $account */ + foreach ($accounts as $account) { + $id = $account->id; + if (array_key_exists($id, $bStart) && array_key_exists($id, $bEnd)) { + $this->balanceDifferences[$id] = [ + 'balance' => bcsub($bEnd[$id]['balance'], $bStart[$id]['balance']), + 'native_balance' => bcsub($bEnd[$id]['native_balance'], $bStart[$id]['native_balance']), + ]; + } + } + } + + private function getObjectGroups(Collection $accounts): void + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $this->objectGroups = $accountRepository->getObjectGroups($accounts); + } + + private function sortAccounts(Collection $accounts): Collection + { + /** @var null|array $sort */ + $sort = $this->parameters->get('sort'); + + if (null === $sort || 0 === count($sort)) { + return $accounts; + } + + /** + * @var string $column + * @var string $direction + */ + foreach ($sort as $column => $direction) { + // account_number + iban + if ('iban' === $column) { + $accounts = $this->sortByIban($accounts, $direction); + } + if ('balance' === $column) { + $accounts = $this->sortByBalance($accounts, $direction); + } + if ('last_activity' === $column) { + $accounts = $this->sortByLastActivity($accounts, $direction); + } + if ('balance_difference' === $column) { + $accounts = $this->sortByBalanceDifference($accounts, $direction); + } + if ('current_debt' === $column) { + $accounts = $this->sortByCurrentDebt($accounts, $direction); + } + } + + return $accounts; + } + + private function sortByIban(Collection $accounts, string $direction): Collection + { + $meta = $this->accountMeta; + + return $accounts->sort(function (Account $left, Account $right) use ($meta, $direction) { + $leftIban = trim(sprintf('%s%s', $left->iban, $meta[$left->id]['account_number'] ?? '')); + $rightIban = trim(sprintf('%s%s', $right->iban, $meta[$right->id]['account_number'] ?? '')); + if ('asc' === $direction) { + return strcasecmp($leftIban, $rightIban); + } + + return strcasecmp($rightIban, $leftIban); + }); + } + + private function sortByBalance(Collection $accounts, string $direction): Collection + { + $balances = $this->convertedBalances; + + return $accounts->sort(function (Account $left, Account $right) use ($balances, $direction) { + $leftBalance = (float) ($balances[$left->id]['native_balance'] ?? 0); + $rightBalance = (float) ($balances[$right->id]['native_balance'] ?? 0); + if ('asc' === $direction) { + return $leftBalance <=> $rightBalance; + } + + return $rightBalance <=> $leftBalance; + }); + } + + private function sortByLastActivity(Collection $accounts, string $direction): Collection + { + $dates = $this->lastActivity; + + return $accounts->sort(function (Account $left, Account $right) use ($dates, $direction) { + $leftDate = $dates[$left->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); + $rightDate = $dates[$right->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); + if ('asc' === $direction) { + return $leftDate->gt($rightDate) ? 1 : -1; + } + + return $rightDate->gt($leftDate) ? 1 : -1; + }); + } + + private function sortByBalanceDifference(Collection $accounts, string $direction): Collection + { + $balances = $this->balanceDifferences; + + return $accounts->sort(function (Account $left, Account $right) use ($balances, $direction) { + $leftBalance = (float) ($balances[$left->id]['native_balance'] ?? 0); + $rightBalance = (float) ($balances[$right->id]['native_balance'] ?? 0); + if ('asc' === $direction) { + return $leftBalance <=> $rightBalance; + } + + return $rightBalance <=> $leftBalance; + }); + } + + private function sortByCurrentDebt(Collection $accounts, string $direction): Collection + { + $amounts = $this->accountMeta; + + return $accounts->sort(function (Account $left, Account $right) use ($amounts, $direction) { + $leftCurrent = (float) ($amounts[$left->id]['current_debt'] ?? 0); + $rightCurrent = (float) ($amounts[$right->id]['current_debt'] ?? 0); + if ('asc' === $direction) { + return $leftCurrent <=> $rightCurrent; + } + + return $rightCurrent <=> $leftCurrent; + }); + } + /** * Transform the account. */ @@ -228,205 +430,4 @@ class AccountTransformer extends AbstractTransformer ], ]; } - - private function getMetaBalances(Collection $accounts): void - { - try { - $this->convertedBalances = app('steam')->balancesByAccountsConverted($accounts, $this->getDate()); - } catch (FireflyException $e) { - Log::error($e->getMessage()); - } - } - - private function getDefaultCurrency(): void - { - $this->default = app('amount')->getDefaultCurrency(); - } - - private function collectAccountMetaData(Collection $accounts): void - { - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $metaFields = $accountRepository->getMetaValues($accounts, ['currency_id', 'account_role', 'account_number', 'liability_direction', 'interest', 'interest_period', 'current_debt']); - $currencyIds = $metaFields->where('name', 'currency_id')->pluck('data')->toArray(); - - $currencies = $repository->getByIds($currencyIds); - foreach ($currencies as $currency) { - $id = $currency->id; - $this->currencies[$id] = $currency; - } - foreach ($metaFields as $entry) { - $id = $entry->account_id; - $this->accountMeta[$id][$entry->name] = $entry->data; - } - } - - private function collectAccountTypes(Collection $accounts): void - { - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $accountTypes = $accountRepository->getAccountTypes($accounts); - - /** @var AccountType $row */ - foreach ($accountTypes as $row) { - $this->accountTypes[$row->id] = (string) config(sprintf('firefly.shortNamesByFullName.%s', $row->type)); - $this->fullTypes[$row->id] = $row->type; - } - } - - private function getLastActivity(Collection $accounts): void - { - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $lastActivity = $accountRepository->getLastActivity($accounts); - foreach ($lastActivity as $row) { - $this->lastActivity[(int) $row['account_id']] = Carbon::parse($row['date_max'], config('app.timezone')); - } - } - - private function sortAccounts(Collection $accounts): Collection - { - /** @var null|array $sort */ - $sort = $this->parameters->get('sort'); - - if (null === $sort || 0 === count($sort)) { - return $accounts; - } - - /** - * @var string $column - * @var string $direction - */ - foreach ($sort as $column => $direction) { - // account_number + iban - if ('iban' === $column) { - $accounts = $this->sortByIban($accounts, $direction); - } - if ('balance' === $column) { - $accounts = $this->sortByBalance($accounts, $direction); - } - if ('last_activity' === $column) { - $accounts = $this->sortByLastActivity($accounts, $direction); - } - if ('balance_difference' === $column) { - $accounts = $this->sortByBalanceDifference($accounts, $direction); - } - if ('current_debt' === $column) { - $accounts = $this->sortByCurrentDebt($accounts, $direction); - } - } - - return $accounts; - } - - private function sortByIban(Collection $accounts, string $direction): Collection - { - $meta = $this->accountMeta; - - return $accounts->sort(function (Account $left, Account $right) use ($meta, $direction) { - $leftIban = trim(sprintf('%s%s', $left->iban, $meta[$left->id]['account_number'] ?? '')); - $rightIban = trim(sprintf('%s%s', $right->iban, $meta[$right->id]['account_number'] ?? '')); - if ('asc' === $direction) { - return strcasecmp($leftIban, $rightIban); - } - - return strcasecmp($rightIban, $leftIban); - }); - } - - private function sortByBalance(Collection $accounts, string $direction): Collection - { - $balances = $this->convertedBalances; - - return $accounts->sort(function (Account $left, Account $right) use ($balances, $direction) { - $leftBalance = (float) ($balances[$left->id]['native_balance'] ?? 0); - $rightBalance = (float) ($balances[$right->id]['native_balance'] ?? 0); - if ('asc' === $direction) { - return $leftBalance <=> $rightBalance; - } - - return $rightBalance <=> $leftBalance; - }); - } - - private function sortByLastActivity(Collection $accounts, string $direction): Collection - { - $dates = $this->lastActivity; - - return $accounts->sort(function (Account $left, Account $right) use ($dates, $direction) { - $leftDate = $dates[$left->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); - $rightDate = $dates[$right->id] ?? Carbon::create(1900, 1, 1, 0, 0, 0); - if ('asc' === $direction) { - return $leftDate->gt($rightDate) ? 1 : -1; - } - - return $rightDate->gt($leftDate) ? 1 : -1; - }); - } - - private function getBalanceDifference(Collection $accounts, Carbon $start, Carbon $end): void - { - // collect balances, start and end for both native and converted. - // yes the b is usually used for boolean by idiots but here it's for balance. - $bStart = []; - $bEnd = []; - - try { - $bStart = app('steam')->balancesByAccountsConverted($accounts, $start); - $bEnd = app('steam')->balancesByAccountsConverted($accounts, $end); - } catch (FireflyException $e) { - Log::error($e->getMessage()); - } - - /** @var Account $account */ - foreach ($accounts as $account) { - $id = $account->id; - if (array_key_exists($id, $bStart) && array_key_exists($id, $bEnd)) { - $this->balanceDifferences[$id] = [ - 'balance' => bcsub($bEnd[$id]['balance'], $bStart[$id]['balance']), - 'native_balance' => bcsub($bEnd[$id]['native_balance'], $bStart[$id]['native_balance']), - ]; - } - } - } - - private function sortByBalanceDifference(Collection $accounts, string $direction): Collection - { - $balances = $this->balanceDifferences; - - return $accounts->sort(function (Account $left, Account $right) use ($balances, $direction) { - $leftBalance = (float) ($balances[$left->id]['native_balance'] ?? 0); - $rightBalance = (float) ($balances[$right->id]['native_balance'] ?? 0); - if ('asc' === $direction) { - return $leftBalance <=> $rightBalance; - } - - return $rightBalance <=> $leftBalance; - }); - } - - private function sortByCurrentDebt(Collection $accounts, string $direction): Collection - { - $amounts = $this->accountMeta; - - return $accounts->sort(function (Account $left, Account $right) use ($amounts, $direction) { - $leftCurrent = (float) ($amounts[$left->id]['current_debt'] ?? 0); - $rightCurrent = (float) ($amounts[$right->id]['current_debt'] ?? 0); - if ('asc' === $direction) { - return $leftCurrent <=> $rightCurrent; - } - - return $rightCurrent <=> $leftCurrent; - }); - } - - private function getObjectGroups(Collection $accounts): void - { - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $this->objectGroups = $accountRepository->getObjectGroups($accounts); - } } diff --git a/app/Transformers/V2/BillTransformer.php b/app/Transformers/V2/BillTransformer.php index 6425ec5b16..76f58e2e34 100644 --- a/app/Transformers/V2/BillTransformer.php +++ b/app/Transformers/V2/BillTransformer.php @@ -86,8 +86,8 @@ class BillTransformer extends AbstractTransformer /** @var ObjectGroup $entry */ foreach ($set as $entry) { - $billId = (int)$entry->object_groupable_id; - $id = (int)$entry->object_group_id; + $billId = (int) $entry->object_groupable_id; + $id = (int) $entry->object_group_id; $order = $entry->order; $this->groups[$billId] = [ 'object_group_id' => $id, @@ -125,8 +125,8 @@ class BillTransformer extends AbstractTransformer foreach ($journals as $journal) { app('log')->debug(sprintf('Processing journal #%d', $journal->id)); $transaction = $transactions[$journal->id] ?? []; - $billId = (int)$journal->bill_id; - $currencyId = (int)($transaction['transaction_currency_id'] ?? 0); + $billId = (int) $journal->bill_id; + $currencyId = (int) ($transaction['transaction_currency_id'] ?? 0); $currencies[$currencyId] ??= TransactionCurrency::find($currencyId); // foreign currency @@ -138,7 +138,7 @@ class BillTransformer extends AbstractTransformer app('log')->debug('Foreign currency is NULL'); if (null !== $transaction['foreign_currency_id']) { app('log')->debug(sprintf('Foreign currency is #%d', $transaction['foreign_currency_id'])); - $foreignCurrencyId = (int)$transaction['foreign_currency_id']; + $foreignCurrencyId = (int) $transaction['foreign_currency_id']; $currencies[$foreignCurrencyId] ??= TransactionCurrency::find($foreignCurrencyId); $foreignCurrencyCode = $currencies[$foreignCurrencyId]->code; $foreignCurrencyName = $currencies[$foreignCurrencyId]->name; @@ -147,8 +147,8 @@ class BillTransformer extends AbstractTransformer } $this->paidDates[$billId][] = [ - 'transaction_group_id' => (string)$journal->id, - 'transaction_journal_id' => (string)$journal->transaction_group_id, + 'transaction_group_id' => (string) $journal->id, + 'transaction_journal_id' => (string) $journal->transaction_group_id, 'date' => $journal->date->toAtomString(), 'currency_id' => $currencies[$currencyId]->id, 'currency_code' => $currencies[$currencyId]->code, @@ -167,7 +167,7 @@ class BillTransformer extends AbstractTransformer 'amount' => $transaction['amount'], 'foreign_amount' => $transaction['foreign_amount'], 'native_amount' => $this->converter->convert($currencies[$currencyId], $this->default, $journal->date, $transaction['amount']), - 'foreign_native_amount' => '' === (string)$transaction['foreign_amount'] ? null : $this->converter->convert( + 'foreign_native_amount' => '' === (string) $transaction['foreign_amount'] ? null : $this->converter->convert( $currencies[$foreignCurrencyId], $this->default, $journal->date, @@ -210,7 +210,7 @@ class BillTransformer extends AbstractTransformer 'amount_max' => app('steam')->bcround($bill->amount_max, $currency->decimal_places), 'native_amount_min' => $this->converter->convert($currency, $this->default, $date, $bill->amount_min), 'native_amount_max' => $this->converter->convert($currency, $this->default, $date, $bill->amount_max), - 'currency_id' => (string)$bill->transaction_currency_id, + 'currency_id' => (string) $bill->transaction_currency_id, 'currency_code' => $currency->code, 'currency_name' => $currency->name, 'currency_symbol' => $currency->symbol, diff --git a/app/Transformers/V2/BudgetLimitTransformer.php b/app/Transformers/V2/BudgetLimitTransformer.php index 26d4be0072..192d095c04 100644 --- a/app/Transformers/V2/BudgetLimitTransformer.php +++ b/app/Transformers/V2/BudgetLimitTransformer.php @@ -79,16 +79,16 @@ class BudgetLimitTransformer extends AbstractTransformer $currencySymbol = $currency->symbol; $currencyDecimalPlaces = $currency->decimal_places; } - $amount = number_format((float)$amount, $currencyDecimalPlaces, '.', ''); + $amount = number_format((float) $amount, $currencyDecimalPlaces, '.', ''); return [ - 'id' => (string)$budgetLimit->id, + 'id' => (string) $budgetLimit->id, 'created_at' => $budgetLimit->created_at->toAtomString(), 'updated_at' => $budgetLimit->updated_at->toAtomString(), 'start' => $budgetLimit->start_date->toAtomString(), 'end' => $budgetLimit->end_date->endOfDay()->toAtomString(), - 'budget_id' => (string)$budgetLimit->budget_id, - 'currency_id' => (string)$currencyId, + 'budget_id' => (string) $budgetLimit->budget_id, + 'currency_id' => (string) $currencyId, 'currency_code' => $currencyCode, 'currency_name' => $currencyName, 'currency_decimal_places' => $currencyDecimalPlaces, diff --git a/app/Transformers/V2/BudgetTransformer.php b/app/Transformers/V2/BudgetTransformer.php index 2c17883a31..aca7ed8758 100644 --- a/app/Transformers/V2/BudgetTransformer.php +++ b/app/Transformers/V2/BudgetTransformer.php @@ -87,7 +87,7 @@ class BudgetTransformer extends AbstractTransformer // } return [ - 'id' => (string)$budget->id, + 'id' => (string) $budget->id, 'created_at' => $budget->created_at->toAtomString(), 'updated_at' => $budget->updated_at->toAtomString(), 'name' => $budget->name, diff --git a/app/Transformers/V2/ExchangeRateTransformer.php b/app/Transformers/V2/ExchangeRateTransformer.php new file mode 100644 index 0000000000..045f8f3a28 --- /dev/null +++ b/app/Transformers/V2/ExchangeRateTransformer.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers\V2; + +use FireflyIII\Models\CurrencyExchangeRate; +use Illuminate\Support\Collection; + +/** + * Class AccountTransformer + */ +class ExchangeRateTransformer extends AbstractTransformer +{ + /** + * This method collects meta-data for one or all accounts in the transformer's collection. + */ + public function collectMetaData(Collection $objects): Collection + { + return $objects; + } + + /** + * Transform the account. + */ + public function transform(CurrencyExchangeRate $rate): array + { + return [ + 'id' => (string) $rate->id, + 'created_at' => $rate->created_at->toAtomString(), + 'updated_at' => $rate->updated_at->toAtomString(), + + 'from_currency_id' => (string) $rate->fromCurrency->id, + 'from_currency_code' => $rate->fromCurrency->code, + 'from_currency_symbol' => $rate->fromCurrency->symbol, + 'from_currency_decimal_places' => $rate->fromCurrency->decimal_places, + + 'to_currency_id' => (string) $rate->toCurrency->id, + 'to_currency_code' => $rate->toCurrency->code, + 'to_currency_symbol' => $rate->toCurrency->symbol, + 'to_currency_decimal_places' => $rate->toCurrency->decimal_places, + + 'rate' => $rate->rate, + 'date' => $rate->date->toAtomString(), + 'links' => [ + [ + 'rel' => 'self', + 'uri' => sprintf('/exchange-rates/%s', $rate->id), + ], + ], + ]; + } +} diff --git a/app/Transformers/V2/PiggyBankTransformer.php b/app/Transformers/V2/PiggyBankTransformer.php index b1d3abada7..e48c48be28 100644 --- a/app/Transformers/V2/PiggyBankTransformer.php +++ b/app/Transformers/V2/PiggyBankTransformer.php @@ -88,7 +88,7 @@ class PiggyBankTransformer extends AbstractTransformer /** @var AccountMeta $preference */ foreach ($currencyPreferences as $preference) { - $currencyId = (int)$preference->data; + $currencyId = (int) $preference->data; $accountId = $preference->account_id; $currencies[$currencyId] ??= TransactionJournal::find($currencyId); $this->currencies[$accountId] = $currencies[$currencyId]; @@ -103,11 +103,11 @@ class PiggyBankTransformer extends AbstractTransformer /** @var ObjectGroup $entry */ foreach ($set as $entry) { - $piggyBankId = (int)$entry->object_groupable_id; - $id = (int)$entry->object_group_id; + $piggyBankId = (int) $entry->object_groupable_id; + $id = (int) $entry->object_group_id; $order = $entry->order; $this->groups[$piggyBankId] = [ - 'object_group_id' => (string)$id, + 'object_group_id' => (string) $id, 'object_group_title' => $entry->title, 'object_group_order' => $order, ]; @@ -116,10 +116,12 @@ class PiggyBankTransformer extends AbstractTransformer // grab repetitions (for current amount): $repetitions = PiggyBankRepetition::whereIn('piggy_bank_id', $piggyBanks)->get(); + throw new FireflyException('[d] Piggy bank repetitions are EOL.'); + /** @var PiggyBankRepetition $repetition */ foreach ($repetitions as $repetition) { $this->repetitions[$repetition->piggy_bank_id] = [ - 'amount' => $repetition->currentamount, + 'amount' => $repetition->current_amount, ]; } @@ -178,14 +180,14 @@ class PiggyBankTransformer extends AbstractTransformer $nativeLeftToSave = null; $savePerMonth = null; $nativeSavePerMonth = null; - $startDate = $piggyBank->startdate?->format('Y-m-d'); - $targetDate = $piggyBank->targetdate?->format('Y-m-d'); + $startDate = $piggyBank->start_date?->format('Y-m-d'); + $targetDate = $piggyBank->target_date?->format('Y-m-d'); $accountId = $piggyBank->account_id; $accountName = $this->accounts[$accountId]['name'] ?? null; $currency = $this->currencies[$accountId] ?? $this->default; $currentAmount = app('steam')->bcround($this->repetitions[$piggyBank->id]['amount'] ?? '0', $currency->decimal_places); $nativeCurrentAmount = $this->converter->convert($this->default, $currency, today(), $currentAmount); - $targetAmount = $piggyBank->targetamount; + $targetAmount = $piggyBank->target_amount; $nativeTargetAmount = $this->converter->convert($this->default, $currency, today(), $targetAmount); $note = $this->notes[$piggyBank->id] ?? null; $group = $this->groups[$piggyBank->id] ?? null; @@ -193,24 +195,24 @@ class PiggyBankTransformer extends AbstractTransformer if (0 !== bccomp($targetAmount, '0')) { // target amount is not 0.00 $leftToSave = bcsub($targetAmount, $currentAmount); $nativeLeftToSave = $this->converter->convert($this->default, $currency, today(), $leftToSave); - $percentage = (int)bcmul(bcdiv($currentAmount, $targetAmount), '100'); - $savePerMonth = $this->getSuggestedMonthlyAmount($currentAmount, $targetAmount, $piggyBank->startdate, $piggyBank->targetdate); + $percentage = (int) bcmul(bcdiv($currentAmount, $targetAmount), '100'); + $savePerMonth = $this->getSuggestedMonthlyAmount($currentAmount, $targetAmount, $piggyBank->start_date, $piggyBank->target_date); $nativeSavePerMonth = $this->converter->convert($this->default, $currency, today(), $savePerMonth); } $this->converter->summarize(); return [ - 'id' => (string)$piggyBank->id, + 'id' => (string) $piggyBank->id, 'created_at' => $piggyBank->created_at->toAtomString(), 'updated_at' => $piggyBank->updated_at->toAtomString(), - 'account_id' => (string)$piggyBank->account_id, + 'account_id' => (string) $piggyBank->account_id, 'account_name' => $accountName, 'name' => $piggyBank->name, - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, - 'native_currency_id' => (string)$this->default->id, + 'native_currency_id' => (string) $this->default->id, 'native_currency_code' => $this->default->code, 'native_currency_symbol' => $this->default->symbol, 'native_currency_decimal_places' => $this->default->decimal_places, @@ -249,12 +251,12 @@ class PiggyBankTransformer extends AbstractTransformer if (bccomp($currentAmount, $targetAmount) < 1) { $now = today(config('app.timezone')); $startDate = null !== $startDate && $startDate->gte($now) ? $startDate : $now; - $diffInMonths = (int)$startDate->diffInMonths($targetDate); + $diffInMonths = (int) $startDate->diffInMonths($targetDate); $remainingAmount = bcsub($targetAmount, $currentAmount); // more than 1 month to go and still need money to save: if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) { - $savePerMonth = bcdiv($remainingAmount, (string)$diffInMonths); + $savePerMonth = bcdiv($remainingAmount, (string) $diffInMonths); } // less than 1 month to go but still need money to save: diff --git a/app/Transformers/V2/TransactionGroupTransformer.php b/app/Transformers/V2/TransactionGroupTransformer.php index 806b47f6ea..37b777aac5 100644 --- a/app/Transformers/V2/TransactionGroupTransformer.php +++ b/app/Transformers/V2/TransactionGroupTransformer.php @@ -101,12 +101,12 @@ class TransactionGroupTransformer extends AbstractTransformer private function collectForArray(array $object): void { foreach ($object['sums'] as $sum) { - $this->currencies[(int)$sum['currency_id']] ??= TransactionCurrency::find($sum['currency_id']); + $this->currencies[(int) $sum['currency_id']] ??= TransactionCurrency::find($sum['currency_id']); } /** @var array $transaction */ foreach ($object['transactions'] as $transaction) { - $this->journals[(int)$transaction['transaction_journal_id']] = []; + $this->journals[(int) $transaction['transaction_journal_id']] = []; } } @@ -169,7 +169,7 @@ class TransactionGroupTransformer extends AbstractTransformer /** @var \stdClass $tag */ foreach ($tags as $tag) { - $id = (int)$tag->transaction_journal_id; + $id = (int) $tag->transaction_journal_id; $this->journals[$id]['tags'][] = $tag->tag; } } @@ -196,7 +196,7 @@ class TransactionGroupTransformer extends AbstractTransformer /** @var null|Budget $budget */ $budget = $journal->budgets()->first(); if (null !== $budget) { - $this->journals[$id]['budget_id'] = (string)$budget->id; + $this->journals[$id]['budget_id'] = (string) $budget->id; $this->journals[$id]['budget_name'] = $budget->name; } @@ -204,14 +204,14 @@ class TransactionGroupTransformer extends AbstractTransformer /** @var null|Category $category */ $category = $journal->categories()->first(); if (null !== $category) { - $this->journals[$id]['category_id'] = (string)$category->id; + $this->journals[$id]['category_id'] = (string) $category->id; $this->journals[$id]['category_name'] = $category->name; } // collect bill: if (null !== $journal->bill_id) { $bill = $journal->bill; - $this->journals[$id]['bill_id'] = (string)$bill->id; + $this->journals[$id]['bill_id'] = (string) $bill->id; $this->journals[$id]['bill_name'] = $bill->name; } @@ -262,11 +262,11 @@ class TransactionGroupTransformer extends AbstractTransformer $first = reset($group['transactions']); return [ - 'id' => (string)$group['id'], + 'id' => (string) $group['id'], 'created_at' => $group['created_at']->toAtomString(), 'updated_at' => $group['updated_at']->toAtomString(), - 'user' => (string)$first['user_id'], - 'user_group' => (string)$first['user_group_id'], + 'user' => (string) $first['user_id'], + 'user_group' => (string) $first['user_group_id'], 'group_title' => $group['title'] ?? null, 'transactions' => $this->transformTransactions($group['transactions'] ?? []), 'links' => [ @@ -279,11 +279,11 @@ class TransactionGroupTransformer extends AbstractTransformer } return [ - 'id' => (string)$group->id, + 'id' => (string) $group->id, 'created_at' => $group->created_at->toAtomString(), 'updated_at' => $group->created_at->toAtomString(), - 'user' => (string)$group->user_id, - 'user_group' => (string)$group->user_group_id, + 'user' => (string) $group->user_id, + 'user_group' => (string) $group->user_group_id, 'group_title' => $group->title ?? null, 'transactions' => $this->transformJournals($group), 'links' => [ @@ -316,19 +316,19 @@ class TransactionGroupTransformer extends AbstractTransformer { $transaction = new NullArrayObject($transaction); $type = $this->stringFromArray($transaction, 'transaction_type_type', TransactionType::WITHDRAWAL); - $journalId = (int)$transaction['transaction_journal_id']; + $journalId = (int) $transaction['transaction_journal_id']; $meta = new NullArrayObject($this->meta[$journalId] ?? []); /** * Convert and use amount: */ - $amount = app('steam')->positive((string)($transaction['amount'] ?? '0')); - $currencyId = (int)$transaction['currency_id']; + $amount = app('steam')->positive((string) ($transaction['amount'] ?? '0')); + $currencyId = (int) $transaction['currency_id']; $nativeAmount = $this->converter->convert($this->default, $this->currencies[$currencyId], $transaction['date'], $amount); $foreignAmount = null; $nativeForeignAmount = null; if (null !== $transaction['foreign_amount']) { - $foreignCurrencyId = (int)$transaction['foreign_currency_id']; + $foreignCurrencyId = (int) $transaction['foreign_currency_id']; $foreignAmount = app('steam')->positive($transaction['foreign_amount']); $nativeForeignAmount = $this->converter->convert($this->default, $this->currencies[$foreignCurrencyId], $transaction['date'], $foreignAmount); } @@ -338,15 +338,15 @@ class TransactionGroupTransformer extends AbstractTransformer $latitude = null; $zoomLevel = null; if (array_key_exists('location', $this->journals[$journalId])) { - $latitude = (string)$this->journals[$journalId]['location']['latitude']; - $longitude = (string)$this->journals[$journalId]['location']['longitude']; + $latitude = (string) $this->journals[$journalId]['location']['latitude']; + $longitude = (string) $this->journals[$journalId]['location']['longitude']; $zoomLevel = $this->journals[$journalId]['location']['zoom_level']; } return [ - 'user' => (string)$transaction['user_id'], - 'user_group' => (string)$transaction['user_group_id'], - 'transaction_journal_id' => (string)$transaction['transaction_journal_id'], + 'user' => (string) $transaction['user_id'], + 'user_group' => (string) $transaction['user_group_id'], + 'transaction_journal_id' => (string) $transaction['transaction_journal_id'], 'type' => strtolower($type), 'date' => $transaction['date']->toAtomString(), 'order' => $transaction['order'], @@ -354,14 +354,14 @@ class TransactionGroupTransformer extends AbstractTransformer 'native_amount' => $nativeAmount, 'foreign_amount' => $foreignAmount, 'native_foreign_amount' => $nativeForeignAmount, - 'currency_id' => (string)$transaction['currency_id'], + 'currency_id' => (string) $transaction['currency_id'], 'currency_code' => $transaction['currency_code'], 'currency_name' => $transaction['currency_name'], 'currency_symbol' => $transaction['currency_symbol'], - 'currency_decimal_places' => (int)$transaction['currency_decimal_places'], + 'currency_decimal_places' => (int) $transaction['currency_decimal_places'], // converted to native currency - 'native_currency_id' => (string)$this->default->id, + 'native_currency_id' => (string) $this->default->id, 'native_currency_code' => $this->default->code, 'native_currency_name' => $this->default->name, 'native_currency_symbol' => $this->default->symbol, @@ -376,11 +376,11 @@ class TransactionGroupTransformer extends AbstractTransformer // foreign converted to native: 'description' => $transaction['description'], - 'source_id' => (string)$transaction['source_account_id'], + 'source_id' => (string) $transaction['source_account_id'], 'source_name' => $transaction['source_account_name'], 'source_iban' => $transaction['source_account_iban'], 'source_type' => $transaction['source_account_type'], - 'destination_id' => (string)$transaction['destination_account_id'], + 'destination_id' => (string) $transaction['destination_account_id'], 'destination_name' => $transaction['destination_account_name'], 'destination_iban' => $transaction['destination_account_iban'], 'destination_type' => $transaction['destination_account_type'], @@ -444,7 +444,7 @@ class TransactionGroupTransformer extends AbstractTransformer return $default; } if (null !== $array[$key]) { - return (string)$array[$key]; + return (string) $array[$key]; } if (null !== $default) { @@ -532,15 +532,15 @@ class TransactionGroupTransformer extends AbstractTransformer $latitude = null; $zoomLevel = null; if (array_key_exists('location', $this->journals[$id])) { - $latitude = (string)$this->journals[$id]['location']['latitude']; - $longitude = (string)$this->journals[$id]['location']['longitude']; + $latitude = (string) $this->journals[$id]['location']['latitude']; + $longitude = (string) $this->journals[$id]['location']['longitude']; $zoomLevel = $this->journals[$id]['location']['zoom_level']; } return [ - 'user' => (string)$journal->user_id, - 'user_group' => (string)$journal->user_group_id, - 'transaction_journal_id' => (string)$journal->id, + 'user' => (string) $journal->user_id, + 'user_group' => (string) $journal->user_group_id, + 'transaction_journal_id' => (string) $journal->id, 'type' => $this->journals[$journal->id]['type'], 'date' => $journal->date->toAtomString(), 'order' => $journal->order, @@ -548,14 +548,14 @@ class TransactionGroupTransformer extends AbstractTransformer 'native_amount' => $nativeAmount, 'foreign_amount' => $foreignAmount, 'native_foreign_amount' => $nativeForeignAmount, - 'currency_id' => (string)$currency->id, + 'currency_id' => (string) $currency->id, 'currency_code' => $currency->code, 'currency_name' => $currency->name, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, // converted to native currency - 'native_currency_id' => (string)$this->default->id, + 'native_currency_id' => (string) $this->default->id, 'native_currency_code' => $this->default->code, 'native_currency_name' => $this->default->name, 'native_currency_symbol' => $this->default->symbol, @@ -569,12 +569,12 @@ class TransactionGroupTransformer extends AbstractTransformer 'foreign_currency_decimal_places' => $foreignCurrency?->decimal_places, 'description' => $journal->description, - 'source_id' => (string)$this->journals[$id]['source_account_id'], + 'source_id' => (string) $this->journals[$id]['source_account_id'], 'source_name' => $this->journals[$id]['source_account_name'], 'source_iban' => $this->journals[$id]['source_account_iban'], 'source_type' => $this->journals[$id]['source_account_type'], - 'destination_id' => (string)$this->journals[$id]['destination_account_id'], + 'destination_id' => (string) $this->journals[$id]['destination_account_id'], 'destination_name' => $this->journals[$id]['destination_account_name'], 'destination_iban' => $this->journals[$id]['destination_account_iban'], 'destination_type' => $this->journals[$id]['destination_account_type'], diff --git a/app/Transformers/V2/UserGroupTransformer.php b/app/Transformers/V2/UserGroupTransformer.php index 41ac5ce8b6..2a33edd6b9 100644 --- a/app/Transformers/V2/UserGroupTransformer.php +++ b/app/Transformers/V2/UserGroupTransformer.php @@ -79,23 +79,6 @@ class UserGroupTransformer extends AbstractTransformer return $objects; } - /** - * Transform the user group. - */ - public function transform(UserGroup $userGroup): array - { - return [ - 'id' => $userGroup->id, - 'created_at' => $userGroup->created_at->toAtomString(), - 'updated_at' => $userGroup->updated_at->toAtomString(), - 'in_use' => $this->inUse[$userGroup->id] ?? false, - 'title' => $userGroup->title, - 'can_see_members' => $this->membershipsVisible[$userGroup->id] ?? false, - 'members' => array_values($this->memberships[$userGroup->id] ?? []), - ]; - // if the user has a specific role in this group, then collect the memberships. - } - private function mergeMemberships(): void { $new = []; @@ -115,4 +98,21 @@ class UserGroupTransformer extends AbstractTransformer } $this->memberships = $new; } + + /** + * Transform the user group. + */ + public function transform(UserGroup $userGroup): array + { + return [ + 'id' => $userGroup->id, + 'created_at' => $userGroup->created_at->toAtomString(), + 'updated_at' => $userGroup->updated_at->toAtomString(), + 'in_use' => $this->inUse[$userGroup->id] ?? false, + 'title' => $userGroup->title, + 'can_see_members' => $this->membershipsVisible[$userGroup->id] ?? false, + 'members' => array_values($this->memberships[$userGroup->id] ?? []), + ]; + // if the user has a specific role in this group, then collect the memberships. + } } diff --git a/app/Transformers/WebhookAttemptTransformer.php b/app/Transformers/WebhookAttemptTransformer.php index 95d2c425a3..2ab04f184a 100644 --- a/app/Transformers/WebhookAttemptTransformer.php +++ b/app/Transformers/WebhookAttemptTransformer.php @@ -37,11 +37,11 @@ class WebhookAttemptTransformer extends AbstractTransformer public function transform(WebhookAttempt $attempt): array { return [ - 'id' => (string)$attempt->id, + 'id' => (string) $attempt->id, 'created_at' => $attempt->created_at->toAtomString(), 'updated_at' => $attempt->updated_at->toAtomString(), - 'webhook_message_id' => (string)$attempt->webhook_message_id, - 'status_code' => (int)$attempt->status_code, + 'webhook_message_id' => (string) $attempt->webhook_message_id, + 'status_code' => (int) $attempt->status_code, 'logs' => $attempt->logs, 'response' => $attempt->response, ]; diff --git a/app/Transformers/WebhookMessageTransformer.php b/app/Transformers/WebhookMessageTransformer.php index 18acaf9532..f0fd5c056d 100644 --- a/app/Transformers/WebhookMessageTransformer.php +++ b/app/Transformers/WebhookMessageTransformer.php @@ -45,12 +45,12 @@ class WebhookMessageTransformer extends AbstractTransformer } return [ - 'id' => (string)$message->id, + 'id' => (string) $message->id, 'created_at' => $message->created_at->toAtomString(), 'updated_at' => $message->updated_at->toAtomString(), 'sent' => $message->sent, 'errored' => $message->errored, - 'webhook_id' => (string)$message->webhook_id, + 'webhook_id' => (string) $message->webhook_id, 'uuid' => $message->uuid, 'message' => $json, ]; diff --git a/app/User.php b/app/User.php index deb0b896ab..c4fa2727b1 100644 --- a/app/User.php +++ b/app/User.php @@ -36,7 +36,6 @@ use FireflyIII\Models\Category; use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\ObjectGroup; -use FireflyIII\Models\PiggyBank; use FireflyIII\Models\Preference; use FireflyIII\Models\Recurrence; use FireflyIII\Models\Role; @@ -50,8 +49,6 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserRole; use FireflyIII\Models\Webhook; -use FireflyIII\Notifications\Admin\TestNotification; -use FireflyIII\Notifications\Admin\UserInvitation; use FireflyIII\Notifications\Admin\UserRegistration; use FireflyIII\Notifications\Admin\VersionCheckResult; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -64,7 +61,7 @@ use Illuminate\Notifications\Notification; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Laravel\Passport\HasApiTokens; -use Laravel\Passport\Token; +use NotificationChannels\Pushover\PushoverReceiver; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -91,7 +88,7 @@ class User extends Authenticatable public static function routeBinder(string $value): self { if (auth()->check()) { - $userId = (int)$value; + $userId = (int) $value; $user = self::find($userId); if (null !== $user) { return $user; @@ -184,7 +181,7 @@ class User extends Authenticatable */ public function getAdministrationId(): int { - $groupId = (int)$this->user_group_id; + $groupId = (int) $this->user_group_id; if (0 === $groupId) { throw new FireflyException('User has no administration ID.'); } @@ -332,12 +329,9 @@ class User extends Authenticatable return $this->hasMany(ObjectGroup::class); } - /** - * Link to piggy banks. - */ - public function piggyBanks(): HasManyThrough + public function piggyBanks(): void { - return $this->hasManyThrough(PiggyBank::class, Account::class); + throw new FireflyException('Method no longer supported.'); } /** @@ -382,9 +376,8 @@ class User extends Authenticatable } return match ($driver) { - 'database' => $this->notifications(), - 'mail' => $email, - default => null, + 'mail' => $email, + default => null, }; } @@ -404,35 +397,43 @@ class User extends Authenticatable return $this->belongsToMany(Role::class); } + public function routeNotificationForPushover() + { + $appToken = (string) app('preferences')->getEncrypted('pushover_app_token', '')->data; + $userToken = (string) app('preferences')->getEncrypted('pushover_user_token', '')->data; + + return PushoverReceiver::withUserKey($userToken)->withApplicationToken($appToken); + } + /** * Route notifications for the Slack channel. */ - public function routeNotificationForSlack(Notification $notification): string + public function routeNotificationForSlack(Notification $notification): ?string { // this check does not validate if the user is owner, Should be done by notification itself. - $res = app('fireflyconfig')->get('slack_webhook_url', '')->data; + $res = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; if (is_array($res)) { $res = ''; } - $res = (string)$res; - if ($notification instanceof TestNotification) { - return $res; - } - if ($notification instanceof UserInvitation) { + $res = (string) $res; + + if (property_exists($notification, 'type') && 'owner' === $notification->type) { return $res; } + + // not the best way to do this, but alas. if ($notification instanceof UserRegistration) { return $res; } if ($notification instanceof VersionCheckResult) { return $res; } - $pref = app('preferences')->getForUser($this, 'slack_webhook_url', '')->data; + $pref = app('preferences')->getEncryptedForUser($this, 'slack_webhook_url', '')->data; if (is_array($pref)) { return ''; } - return (string)$pref; + return (string) $pref; } /** diff --git a/app/Validation/Account/DepositValidation.php b/app/Validation/Account/DepositValidation.php index f91c99abd8..961e709503 100644 --- a/app/Validation/Account/DepositValidation.php +++ b/app/Validation/Account/DepositValidation.php @@ -46,7 +46,7 @@ trait DepositValidation if (null === $accountId && null === $accountName && null === $accountIban && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return false, // because the destination of a deposit can't be created. - $this->destError = (string)trans('validation.deposit_dest_need_data'); + $this->destError = (string) trans('validation.deposit_dest_need_data'); app('log')->error('Both values are NULL, cant create deposit destination.'); $result = false; } @@ -61,7 +61,7 @@ trait DepositValidation $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { app('log')->debug('findExistingAccount() returned NULL, so the result is false.'); - $this->destError = (string)trans('validation.deposit_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->destError = (string) trans('validation.deposit_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); $result = false; } if (null !== $search) { @@ -107,7 +107,7 @@ trait DepositValidation // if both values are NULL return false, // because the source of a deposit can't be created. // (this never happens). - $this->sourceError = (string)trans('validation.deposit_source_need_data'); + $this->sourceError = (string) trans('validation.deposit_source_need_data'); $result = false; } @@ -116,7 +116,7 @@ trait DepositValidation app('log')->debug('Check if there is not already another account with this IBAN'); $existing = $this->findExistingAccount($validTypes, ['iban' => $accountIban], true); if (null !== $existing) { - $this->sourceError = (string)trans('validation.deposit_src_iban_exists'); + $this->sourceError = (string) trans('validation.deposit_src_iban_exists'); return false; } diff --git a/app/Validation/Account/OBValidation.php b/app/Validation/Account/OBValidation.php index de2dc2c11d..30c2e65cb2 100644 --- a/app/Validation/Account/OBValidation.php +++ b/app/Validation/Account/OBValidation.php @@ -44,7 +44,7 @@ trait OBValidation if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return false, // because the destination of a deposit can't be created. - $this->destError = (string)trans('validation.ob_dest_need_data'); + $this->destError = (string) trans('validation.ob_dest_need_data'); app('log')->error('Both values are NULL, cant create OB destination.'); $result = false; } @@ -59,7 +59,7 @@ trait OBValidation $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { app('log')->debug('findExistingAccount() returned NULL, so the result is false.', $validTypes); - $this->destError = (string)trans('validation.ob_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->destError = (string) trans('validation.ob_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); $result = false; } if (null !== $search) { @@ -92,7 +92,7 @@ trait OBValidation // if both values are NULL return false, // because the source of a deposit can't be created. // (this never happens). - $this->sourceError = (string)trans('validation.ob_source_need_data'); + $this->sourceError = (string) trans('validation.ob_source_need_data'); $result = false; } diff --git a/app/Validation/Account/ReconciliationValidation.php b/app/Validation/Account/ReconciliationValidation.php index be3695eef3..52b97201d4 100644 --- a/app/Validation/Account/ReconciliationValidation.php +++ b/app/Validation/Account/ReconciliationValidation.php @@ -53,7 +53,7 @@ trait ReconciliationValidation $validTypes = array_keys($this->combinations[$this->transactionType]); $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - $this->sourceError = (string)trans('validation.reconciliation_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->sourceError = (string) trans('validation.reconciliation_source_bad_data', ['id' => $accountId, 'name' => $accountName]); app('log')->warning('Not a valid source. Cant find it.', $validTypes); return false; @@ -88,7 +88,7 @@ trait ReconciliationValidation $validTypes = array_keys($this->combinations[$this->transactionType]); $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - $this->sourceError = (string)trans('validation.reconciliation_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->sourceError = (string) trans('validation.reconciliation_source_bad_data', ['id' => $accountId, 'name' => $accountName]); app('log')->warning('Not a valid source. Cant find it.', $validTypes); return false; diff --git a/app/Validation/Account/TransferValidation.php b/app/Validation/Account/TransferValidation.php index 91a1ae7e39..ff01b7eab6 100644 --- a/app/Validation/Account/TransferValidation.php +++ b/app/Validation/Account/TransferValidation.php @@ -42,7 +42,7 @@ trait TransferValidation if (null === $accountId && null === $accountName && null === $accountIban && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return false, // because the destination of a transfer can't be created. - $this->destError = (string)trans('validation.transfer_dest_need_data'); + $this->destError = (string) trans('validation.transfer_dest_need_data'); app('log')->error('Both values are NULL, cant create transfer destination.'); return false; @@ -51,7 +51,7 @@ trait TransferValidation // or try to find the account: $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - $this->destError = (string)trans('validation.transfer_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->destError = (string) trans('validation.transfer_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); return false; } @@ -86,7 +86,7 @@ trait TransferValidation && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return false, // because the source of a withdrawal can't be created. - $this->sourceError = (string)trans('validation.transfer_source_need_data'); + $this->sourceError = (string) trans('validation.transfer_source_need_data'); app('log')->warning('Not a valid source, need more data.'); return false; @@ -95,7 +95,7 @@ trait TransferValidation // otherwise try to find the account: $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - $this->sourceError = (string)trans('validation.transfer_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->sourceError = (string) trans('validation.transfer_source_bad_data', ['id' => $accountId, 'name' => $accountName]); app('log')->warning('Not a valid source, cant find it.', $validTypes); return false; diff --git a/app/Validation/Account/WithdrawalValidation.php b/app/Validation/Account/WithdrawalValidation.php index aea6884db9..630d8f8b66 100644 --- a/app/Validation/Account/WithdrawalValidation.php +++ b/app/Validation/Account/WithdrawalValidation.php @@ -43,7 +43,7 @@ trait WithdrawalValidation if (null === $accountId && null === $accountName && null === $accountIban && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return TRUE // because we assume the user doesn't want to submit / change anything. - $this->sourceError = (string)trans('validation.withdrawal_source_need_data'); + $this->sourceError = (string) trans('validation.withdrawal_source_need_data'); app('log')->warning('[a] Not a valid source. Need more data.'); return false; @@ -52,7 +52,7 @@ trait WithdrawalValidation // otherwise try to find the account: $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - $this->sourceError = (string)trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); app('log')->warning('Not a valid source. Cant find it.', $validTypes); return false; @@ -80,7 +80,7 @@ trait WithdrawalValidation if (null === $accountId && null === $accountName && null === $accountIban && null === $accountNumber && false === $this->canCreateTypes($validTypes)) { // if both values are NULL return false, // because the destination of a withdrawal can never be created automatically. - $this->destError = (string)trans('validation.withdrawal_dest_need_data'); + $this->destError = (string) trans('validation.withdrawal_dest_need_data'); return false; } @@ -96,7 +96,7 @@ trait WithdrawalValidation return true; } // todo explain error in log message. - $this->destError = (string)trans('validation.withdrawal_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->destError = (string) trans('validation.withdrawal_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); return false; } @@ -108,7 +108,7 @@ trait WithdrawalValidation // the inverse flag reverses the search, searching for everything that is NOT a valid type. $existing = $this->findExistingAccount($validTypes, ['iban' => $accountIban], true); if (null !== $existing) { - $this->destError = (string)trans('validation.withdrawal_dest_iban_exists'); + $this->destError = (string) trans('validation.withdrawal_dest_iban_exists'); return false; } @@ -131,7 +131,7 @@ trait WithdrawalValidation if (null === $accountId && null === $accountName && null === $accountNumber && null === $accountIban && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return false, // because the source of a withdrawal can't be created. - $this->sourceError = (string)trans('validation.withdrawal_source_need_data'); + $this->sourceError = (string) trans('validation.withdrawal_source_need_data'); app('log')->warning('[b] Not a valid source. Need more data.'); return false; @@ -140,7 +140,7 @@ trait WithdrawalValidation // otherwise try to find the account: $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - $this->sourceError = (string)trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + $this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); app('log')->warning('Not a valid source. Cant find it.', $validTypes); return false; diff --git a/app/Validation/AccountValidator.php b/app/Validation/AccountValidator.php index 6e05fa2e53..da47855d40 100644 --- a/app/Validation/AccountValidator.php +++ b/app/Validation/AccountValidator.php @@ -277,7 +277,7 @@ class AccountValidator } // find by iban - if (null !== $accountIban && '' !== (string)$accountIban) { + if (null !== $accountIban && '' !== (string) $accountIban) { $first = $this->getRepository()->findByIbanNull($accountIban, $validTypes); $accountType = null === $first ? 'invalid' : $first->accountType->type; $check = in_array($accountType, $validTypes, true); @@ -290,7 +290,7 @@ class AccountValidator } // find by number - if (null !== $accountNumber && '' !== (string)$accountNumber) { + if (null !== $accountNumber && '' !== (string) $accountNumber) { $first = $this->getRepository()->findByAccountNumber($accountNumber, $validTypes); $accountType = null === $first ? 'invalid' : $first->accountType->type; $check = in_array($accountType, $validTypes, true); @@ -303,7 +303,7 @@ class AccountValidator } // find by name: - if ('' !== (string)$accountName) { + if ('' !== (string) $accountName) { $first = $this->getRepository()->findByName($accountName, $validTypes); if (null !== $first) { app('log')->debug(sprintf('Name: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); diff --git a/app/Validation/Api/Data/Bulk/ValidatesBulkTransactionQuery.php b/app/Validation/Api/Data/Bulk/ValidatesBulkTransactionQuery.php index 6494d4d05a..bef98c6709 100644 --- a/app/Validation/Api/Data/Bulk/ValidatesBulkTransactionQuery.php +++ b/app/Validation/Api/Data/Bulk/ValidatesBulkTransactionQuery.php @@ -41,20 +41,20 @@ trait ValidatesBulkTransactionQuery // find both accounts, must be same type. // already validated: belongs to this user. $repository = app(AccountRepositoryInterface::class); - $source = $repository->find((int)$json['where']['account_id']); - $dest = $repository->find((int)$json['update']['account_id']); + $source = $repository->find((int) $json['where']['account_id']); + $dest = $repository->find((int) $json['update']['account_id']); if (null === $source) { - $validator->errors()->add('query', sprintf((string)trans('validation.invalid_query_data'), 'where', 'account_id')); + $validator->errors()->add('query', sprintf((string) trans('validation.invalid_query_data'), 'where', 'account_id')); return; } if (null === $dest) { - $validator->errors()->add('query', sprintf((string)trans('validation.invalid_query_data'), 'update', 'account_id')); + $validator->errors()->add('query', sprintf((string) trans('validation.invalid_query_data'), 'update', 'account_id')); return; } if ($source->accountType->type !== $dest->accountType->type) { - $validator->errors()->add('query', (string)trans('validation.invalid_query_account_type')); + $validator->errors()->add('query', (string) trans('validation.invalid_query_account_type')); return; } @@ -68,7 +68,7 @@ trait ValidatesBulkTransactionQuery && null !== $destCurrency && $sourceCurrency->id !== $destCurrency->id ) { - $validator->errors()->add('query', (string)trans('validation.invalid_query_currency')); + $validator->errors()->add('query', (string) trans('validation.invalid_query_currency')); } } } diff --git a/app/Validation/AutoBudget/ValidatesAutoBudgetRequest.php b/app/Validation/AutoBudget/ValidatesAutoBudgetRequest.php index d417061c3e..493c0d03ac 100644 --- a/app/Validation/AutoBudget/ValidatesAutoBudgetRequest.php +++ b/app/Validation/AutoBudget/ValidatesAutoBudgetRequest.php @@ -42,38 +42,38 @@ trait ValidatesAutoBudgetRequest /** @var null|float|int|string $amount */ $amount = array_key_exists('auto_budget_amount', $data) ? $data['auto_budget_amount'] : null; $period = array_key_exists('auto_budget_period', $data) ? $data['auto_budget_period'] : null; - $currencyId = array_key_exists('auto_budget_currency_id', $data) ? (int)$data['auto_budget_currency_id'] : null; + $currencyId = array_key_exists('auto_budget_currency_id', $data) ? (int) $data['auto_budget_currency_id'] : null; $currencyCode = array_key_exists('auto_budget_currency_code', $data) ? $data['auto_budget_currency_code'] : null; if (is_numeric($type)) { - $type = (int)$type; + $type = (int) $type; } if ('' === $type || 0 === $type) { return; } // TODO lots of duplicates with number validator. // TODO should be present at more places, stop scientific notification - if (str_contains(strtoupper((string)$amount), 'E')) { + if (str_contains(strtoupper((string) $amount), 'E')) { $amount = ''; } // basic float check: if (!is_numeric($amount)) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); + $validator->errors()->add('auto_budget_amount', (string) trans('validation.amount_required_for_auto_budget')); return; } - if (1 !== bccomp((string)$amount, '0')) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive')); + if (1 !== bccomp((string) $amount, '0')) { + $validator->errors()->add('auto_budget_amount', (string) trans('validation.auto_budget_amount_positive')); } if ('' === $period) { - $validator->errors()->add('auto_budget_period', (string)trans('validation.auto_budget_period_mandatory')); + $validator->errors()->add('auto_budget_period', (string) trans('validation.auto_budget_period_mandatory')); } if (null !== $currencyId && null !== $currencyCode && '' === $currencyCode && 0 === $currencyId) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.require_currency_info')); + $validator->errors()->add('auto_budget_amount', (string) trans('validation.require_currency_info')); } // too big amount - if ((int)$amount > 268435456) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); + if ((int) $amount > 268435456) { + $validator->errors()->add('auto_budget_amount', (string) trans('validation.amount_required_for_auto_budget')); } } } diff --git a/app/Validation/CurrencyValidation.php b/app/Validation/CurrencyValidation.php index c792b0a866..52bad8ff61 100644 --- a/app/Validation/CurrencyValidation.php +++ b/app/Validation/CurrencyValidation.php @@ -57,7 +57,7 @@ trait CurrencyValidation continue; } - $foreignAmount = (string)($transaction['foreign_amount'] ?? ''); + $foreignAmount = (string) ($transaction['foreign_amount'] ?? ''); $foreignId = $transaction['foreign_currency_id'] ?? null; $foreignCode = $transaction['foreign_currency_code'] ?? null; if ('' === $foreignAmount) { @@ -66,9 +66,9 @@ trait CurrencyValidation (array_key_exists('foreign_currency_id', $transaction) || array_key_exists('foreign_currency_code', $transaction)) && (null !== $foreignId || null !== $foreignCode) ) { - $validator->errors()->add('transactions.'.$index.'.foreign_amount', (string)trans('validation.require_currency_amount')); - $validator->errors()->add('transactions.'.$index.'.foreign_currency_id', (string)trans('validation.require_currency_amount')); - $validator->errors()->add('transactions.'.$index.'.foreign_currency_code', (string)trans('validation.require_currency_amount')); + $validator->errors()->add('transactions.'.$index.'.foreign_amount', (string) trans('validation.require_currency_amount')); + $validator->errors()->add('transactions.'.$index.'.foreign_currency_id', (string) trans('validation.require_currency_amount')); + $validator->errors()->add('transactions.'.$index.'.foreign_currency_code', (string) trans('validation.require_currency_amount')); } continue; @@ -79,14 +79,14 @@ trait CurrencyValidation Log::debug('validateForeignCurrencyInformation: array contains foreign amount info.'); if (!array_key_exists('foreign_currency_id', $transaction) && !array_key_exists('foreign_currency_code', $transaction)) { Log::debug('validateForeignCurrencyInformation: array contains NO foreign currency info.'); - $validator->errors()->add('transactions.'.$index.'.foreign_amount', (string)trans('validation.require_currency_info')); + $validator->errors()->add('transactions.'.$index.'.foreign_amount', (string) trans('validation.require_currency_info')); } } - if (0 === $compare && ('' !== (string)$foreignId || '' !== (string)$foreignCode)) { + if (0 === $compare && ('' !== (string) $foreignId || '' !== (string) $foreignCode)) { Log::debug('validateForeignCurrencyInformation: array contains foreign currency info, but zero amount.'); - $validator->errors()->add('transactions.'.$index.'.foreign_currency_id', (string)trans('validation.require_currency_amount')); - $validator->errors()->add('transactions.'.$index.'.foreign_currency_code', (string)trans('validation.require_currency_amount')); - $validator->errors()->add('transactions.'.$index.'.foreign_amount', (string)trans('validation.require_currency_amount')); + $validator->errors()->add('transactions.'.$index.'.foreign_currency_id', (string) trans('validation.require_currency_amount')); + $validator->errors()->add('transactions.'.$index.'.foreign_currency_code', (string) trans('validation.require_currency_amount')); + $validator->errors()->add('transactions.'.$index.'.foreign_amount', (string) trans('validation.require_currency_amount')); } } } diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 95553306ff..5c5a19f9a7 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -27,6 +27,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; +use FireflyIII\Models\PiggyBank; use FireflyIII\Models\TransactionType; use FireflyIII\Models\Webhook; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -78,22 +79,6 @@ class FireflyValidator extends Validator return (bool) \Google2FA::verifyKey((string) $secret, $value); } - public function validateExistingMfaCode($attribute, $value): bool - { - if (!is_string($value) || 6 !== strlen($value)) { - return false; - } - $user = auth()->user(); - if (null === $user) { - app('log')->error('No user during validate2faCode'); - - return false; - } - $secret = (string)$user->mfa_secret; - - return (bool) \Google2FA::verifyKey($secret, $value); - } - /** * @param mixed $attribute * @param mixed $value @@ -130,6 +115,22 @@ class FireflyValidator extends Validator return true; } + public function validateExistingMfaCode($attribute, $value): bool + { + if (!is_string($value) || 6 !== strlen($value)) { + return false; + } + $user = auth()->user(); + if (null === $user) { + app('log')->error('No user during validate2faCode'); + + return false; + } + $secret = (string) $user->mfa_secret; + + return (bool) \Google2FA::verifyKey($secret, $value); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -812,15 +813,16 @@ class FireflyValidator extends Validator public function validateUniquePiggyBankForUser($attribute, $value, $parameters): bool { $exclude = $parameters[0] ?? null; - $query = \DB::table('piggy_banks')->whereNull('piggy_banks.deleted_at') - ->leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id')->where('accounts.user_id', auth()->user()->id) + $query = PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', auth()->user()->id) ; if (null !== $exclude) { $query->where('piggy_banks.id', '!=', (int) $exclude); } $query->where('piggy_banks.name', $value); - return null === $query->first(['piggy_banks.*']); + return 0 === $query->get(['piggy_banks.*'])->count(); } /** diff --git a/app/Validation/GroupValidation.php b/app/Validation/GroupValidation.php index 581c7a239a..84c32cfc90 100644 --- a/app/Validation/GroupValidation.php +++ b/app/Validation/GroupValidation.php @@ -66,7 +66,7 @@ trait GroupValidation $hasAccountInfo = false; $hasJournalId = array_key_exists('transaction_journal_id', $transaction); foreach ($keys as $key) { - if (array_key_exists($key, $transaction) && '' !== (string)$transaction[$key]) { + if (array_key_exists($key, $transaction) && '' !== (string) $transaction[$key]) { $hasAccountInfo = true; } } @@ -74,11 +74,11 @@ trait GroupValidation if (false === $hasAccountInfo && !$hasJournalId) { $validator->errors()->add( sprintf('transactions.%d.source_id', $index), - (string)trans('validation.generic_no_source') + (string) trans('validation.generic_no_source') ); $validator->errors()->add( sprintf('transactions.%d.destination_id', $index), - (string)trans('validation.generic_no_destination') + (string) trans('validation.generic_no_destination') ); } } @@ -117,7 +117,7 @@ trait GroupValidation if (array_key_exists($key, $row)) { $validator->errors()->add( sprintf('transactions.%d.%s', $index, $key), - (string)trans('validation.reconciled_forbidden_field', ['field' => $key]) + (string) trans('validation.reconciled_forbidden_field', ['field' => $key]) ); } } @@ -139,7 +139,7 @@ trait GroupValidation $transactions = $this->getTransactionsArray($validator); $validDescriptions = 0; foreach ($transactions as $transaction) { - if ('' !== (string)($transaction['description'] ?? null)) { + if ('' !== (string) ($transaction['description'] ?? null)) { ++$validDescriptions; } } @@ -148,7 +148,7 @@ trait GroupValidation if (0 === $validDescriptions) { $validator->errors()->add( 'transactions.0.description', - (string)trans('validation.filled', ['attribute' => (string)trans('validation.attributes.description')]) + (string) trans('validation.filled', ['attribute' => (string) trans('validation.attributes.description')]) ); } } @@ -164,7 +164,7 @@ trait GroupValidation $groupTitle = $data['group_title'] ?? ''; if ('' === $groupTitle && count($transactions) > 1) { - $validator->errors()->add('group_title', (string)trans('validation.group_title_mandatory')); + $validator->errors()->add('group_title', (string) trans('validation.group_title_mandatory')); } } @@ -210,12 +210,12 @@ trait GroupValidation return; } - $journalId = (int)$journalId; + $journalId = (int) $journalId; $count = $transactionGroup->transactionJournals()->where('transaction_journals.id', $journalId)->count(); if (0 === $journalId || 0 === $count) { app('log')->warning(sprintf('Transaction group #%d has %d journals with ID %d', $transactionGroup->id, $count, $journalId)); app('log')->warning('Invalid submission: Each split must have transaction_journal_id (either valid ID or 0).'); - $validator->errors()->add(sprintf('transactions.%d.source_name', $index), (string)trans('validation.need_id_in_edit')); + $validator->errors()->add(sprintf('transactions.%d.source_name', $index), (string) trans('validation.need_id_in_edit')); } } } diff --git a/app/Validation/RecurrenceValidation.php b/app/Validation/RecurrenceValidation.php index 166ac36c02..81d74cae6a 100644 --- a/app/Validation/RecurrenceValidation.php +++ b/app/Validation/RecurrenceValidation.php @@ -89,7 +89,7 @@ trait RecurrenceValidation continue; } // validate source account. - $sourceId = array_key_exists('source_id', $transaction) ? (int)$transaction['source_id'] : null; + $sourceId = array_key_exists('source_id', $transaction) ? (int) $transaction['source_id'] : null; $sourceName = $transaction['source_name'] ?? null; $validSource = $accountValidator->validateSource(['id' => $sourceId, 'name' => $sourceName]); @@ -101,7 +101,7 @@ trait RecurrenceValidation return; } // validate destination account - $destinationId = array_key_exists('destination_id', $transaction) ? (int)$transaction['destination_id'] : null; + $destinationId = array_key_exists('destination_id', $transaction) ? (int) $transaction['destination_id'] : null; $destinationName = $transaction['destination_name'] ?? null; $validDestination = $accountValidator->validateDestination(['id' => $destinationId, 'name' => $destinationName]); // do something with result: @@ -123,7 +123,7 @@ trait RecurrenceValidation $repetitions = $data['repetitions'] ?? []; // need at least one transaction if (!is_countable($repetitions) || 0 === count($repetitions)) { - $validator->errors()->add('repetitions', (string)trans('validation.at_least_one_repetition')); + $validator->errors()->add('repetitions', (string) trans('validation.at_least_one_repetition')); } } @@ -139,7 +139,7 @@ trait RecurrenceValidation } // need at least one transaction if (0 === count($repetitions)) { - $validator->errors()->add('repetitions', (string)trans('validation.at_least_one_repetition')); + $validator->errors()->add('repetitions', (string) trans('validation.at_least_one_repetition')); } } @@ -154,15 +154,15 @@ trait RecurrenceValidation $repeatUntil = $data['repeat_until'] ?? null; if (null !== $repetitions && null !== $repeatUntil) { // expect a date OR count: - $validator->errors()->add('repeat_until', (string)trans('validation.require_repeat_until')); - $validator->errors()->add('nr_of_repetitions', (string)trans('validation.require_repeat_until')); + $validator->errors()->add('repeat_until', (string) trans('validation.require_repeat_until')); + $validator->errors()->add('nr_of_repetitions', (string) trans('validation.require_repeat_until')); } } public function validateRecurringConfig(Validator $validator): void { $data = $validator->getData(); - $reps = array_key_exists('nr_of_repetitions', $data) ? (int)$data['nr_of_repetitions'] : null; + $reps = array_key_exists('nr_of_repetitions', $data) ? (int) $data['nr_of_repetitions'] : null; $repeatUntil = array_key_exists('repeat_until', $data) ? new Carbon($data['repeat_until']) : null; if (null === $reps && null === $repeatUntil) { @@ -182,7 +182,7 @@ trait RecurrenceValidation $data = $validator->getData(); $repetitions = $data['repetitions'] ?? []; if (!is_array($repetitions)) { - $validator->errors()->add(sprintf('repetitions.%d.type', 0), (string)trans('validation.valid_recurrence_rep_type')); + $validator->errors()->add(sprintf('repetitions.%d.type', 0), (string) trans('validation.valid_recurrence_rep_type')); return; } @@ -201,32 +201,32 @@ trait RecurrenceValidation switch ($repetition['type'] ?? 'empty') { default: - $validator->errors()->add(sprintf('repetitions.%d.type', $index), (string)trans('validation.valid_recurrence_rep_type')); + $validator->errors()->add(sprintf('repetitions.%d.type', $index), (string) trans('validation.valid_recurrence_rep_type')); return; case 'daily': - $this->validateDaily($validator, $index, (string)$repetition['moment']); + $this->validateDaily($validator, $index, (string) $repetition['moment']); break; case 'monthly': - $this->validateMonthly($validator, $index, (int)$repetition['moment']); + $this->validateMonthly($validator, $index, (int) $repetition['moment']); break; case 'ndom': - $this->validateNdom($validator, $index, (string)$repetition['moment']); + $this->validateNdom($validator, $index, (string) $repetition['moment']); break; case 'weekly': - $this->validateWeekly($validator, $index, (int)$repetition['moment']); + $this->validateWeekly($validator, $index, (int) $repetition['moment']); break; case 'yearly': - $this->validateYearly($validator, $index, (string)$repetition['moment']); + $this->validateYearly($validator, $index, (string) $repetition['moment']); break; } @@ -239,7 +239,7 @@ trait RecurrenceValidation protected function validateDaily(Validator $validator, int $index, string $moment): void { if ('' !== $moment) { - $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); + $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string) trans('validation.valid_recurrence_rep_moment')); } } @@ -249,7 +249,7 @@ trait RecurrenceValidation protected function validateMonthly(Validator $validator, int $index, int $dayOfMonth): void { if ($dayOfMonth < 1 || $dayOfMonth > 31) { - $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); + $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string) trans('validation.valid_recurrence_rep_moment')); } } @@ -261,19 +261,19 @@ trait RecurrenceValidation { $parameters = explode(',', $moment); if (2 !== count($parameters)) { - $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); + $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string) trans('validation.valid_recurrence_rep_moment')); return; } - $nthDay = (int)($parameters[0] ?? 0.0); - $dayOfWeek = (int)($parameters[1] ?? 0.0); + $nthDay = (int) ($parameters[0] ?? 0.0); + $dayOfWeek = (int) ($parameters[1] ?? 0.0); if ($nthDay < 1 || $nthDay > 5) { - $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); + $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string) trans('validation.valid_recurrence_rep_moment')); return; } if ($dayOfWeek < 1 || $dayOfWeek > 7) { - $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); + $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string) trans('validation.valid_recurrence_rep_moment')); } } @@ -283,7 +283,7 @@ trait RecurrenceValidation protected function validateWeekly(Validator $validator, int $index, int $dayOfWeek): void { if ($dayOfWeek < 1 || $dayOfWeek > 7) { - $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); + $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string) trans('validation.valid_recurrence_rep_moment')); } } @@ -296,7 +296,7 @@ trait RecurrenceValidation Carbon::createFromFormat('Y-m-d', $moment); } catch (\InvalidArgumentException $e) { // @phpstan-ignore-line app('log')->debug(sprintf('Invalid argument for Carbon: %s', $e->getMessage())); - $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); + $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string) trans('validation.valid_recurrence_rep_moment')); } } @@ -311,7 +311,7 @@ trait RecurrenceValidation if (0 === $submittedTrCount) { app('log')->warning('[b] User submitted no transactions.'); - $validator->errors()->add('transactions', (string)trans('validation.at_least_one_transaction')); + $validator->errors()->add('transactions', (string) trans('validation.at_least_one_transaction')); return; } @@ -324,16 +324,16 @@ trait RecurrenceValidation return; // home safe! } $id = $first['id']; - if ('' === (string)$id) { + if ('' === (string) $id) { app('log')->debug('Single count and empty ID, done.'); return; // home safe! } - $integer = (int)$id; + $integer = (int) $id; $secondCount = $recurrence->recurrenceTransactions()->where('recurrences_transactions.id', $integer)->count(); app('log')->debug(sprintf('Result of ID count: %d', $secondCount)); if (0 === $secondCount) { - $validator->errors()->add('transactions.0.id', (string)trans('validation.id_does_not_match', ['id' => $integer])); + $validator->errors()->add('transactions.0.id', (string) trans('validation.id_does_not_match', ['id' => $integer])); } app('log')->debug('Single ID validation done.'); @@ -364,19 +364,19 @@ trait RecurrenceValidation app('log')->debug(sprintf('Now at %d/%d', $index + 1, $submittedTrCount)); if (!is_array($transaction)) { app('log')->warning('Not an array. Give error.'); - $validator->errors()->add(sprintf('transactions.%d.id', $index), (string)trans('validation.at_least_one_transaction')); + $validator->errors()->add(sprintf('transactions.%d.id', $index), (string) trans('validation.at_least_one_transaction')); return; } if (!array_key_exists('id', $transaction) && $idsMandatory) { app('log')->warning('ID is mandatory but array has no ID.'); - $validator->errors()->add(sprintf('transactions.%d.id', $index), (string)trans('validation.need_id_to_match')); + $validator->errors()->add(sprintf('transactions.%d.id', $index), (string) trans('validation.need_id_to_match')); return; } if (array_key_exists('id', $transaction)) { // don't matter if $idsMandatory app('log')->debug('Array has ID.'); - $idCount = $recurrence->recurrenceTransactions()->where('recurrences_transactions.id', (int)$transaction['id'])->count(); + $idCount = $recurrence->recurrenceTransactions()->where('recurrences_transactions.id', (int) $transaction['id'])->count(); if (0 === $idCount) { app('log')->debug('ID does not exist or no match. Count another unmatched ID.'); ++$unmatchedIds; @@ -392,7 +392,7 @@ trait RecurrenceValidation app('log')->debug(sprintf('Submitted: %d. Original: %d. User can submit %d unmatched transactions.', $submittedTrCount, $originalTrCount, $maxUnmatched)); if ($unmatchedIds > $maxUnmatched) { app('log')->warning(sprintf('Too many unmatched transactions (%d).', $unmatchedIds)); - $validator->errors()->add('transactions.0.id', (string)trans('validation.too_many_unmatched')); + $validator->errors()->add('transactions.0.id', (string) trans('validation.too_many_unmatched')); return; } diff --git a/app/Validation/TransactionValidation.php b/app/Validation/TransactionValidation.php index 8cb6224894..81f77a0c1c 100644 --- a/app/Validation/TransactionValidation.php +++ b/app/Validation/TransactionValidation.php @@ -109,10 +109,10 @@ trait TransactionValidation $accountValidator->setTransactionType($transactionType); // validate source account. - $sourceId = array_key_exists('source_id', $transaction) ? (int)$transaction['source_id'] : null; - $sourceName = array_key_exists('source_name', $transaction) ? (string)$transaction['source_name'] : null; - $sourceIban = array_key_exists('source_iban', $transaction) ? (string)$transaction['source_iban'] : null; - $sourceNumber = array_key_exists('source_number', $transaction) ? (string)$transaction['source_number'] : null; + $sourceId = array_key_exists('source_id', $transaction) ? (int) $transaction['source_id'] : null; + $sourceName = array_key_exists('source_name', $transaction) ? (string) $transaction['source_name'] : null; + $sourceIban = array_key_exists('source_iban', $transaction) ? (string) $transaction['source_iban'] : null; + $sourceNumber = array_key_exists('source_number', $transaction) ? (string) $transaction['source_number'] : null; $source = [ 'id' => $sourceId, 'name' => $sourceName, @@ -129,10 +129,10 @@ trait TransactionValidation return; } // validate destination account - $destinationId = array_key_exists('destination_id', $transaction) ? (int)$transaction['destination_id'] : null; - $destinationName = array_key_exists('destination_name', $transaction) ? (string)$transaction['destination_name'] : null; - $destinationIban = array_key_exists('destination_iban', $transaction) ? (string)$transaction['destination_iban'] : null; - $destinationNumber = array_key_exists('destination_number', $transaction) ? (string)$transaction['destination_number'] : null; + $destinationId = array_key_exists('destination_id', $transaction) ? (int) $transaction['destination_id'] : null; + $destinationName = array_key_exists('destination_name', $transaction) ? (string) $transaction['destination_name'] : null; + $destinationIban = array_key_exists('destination_iban', $transaction) ? (string) $transaction['destination_iban'] : null; + $destinationNumber = array_key_exists('destination_number', $transaction) ? (string) $transaction['destination_number'] : null; $destination = [ 'id' => $destinationId, 'name' => $destinationName, @@ -244,17 +244,17 @@ trait TransactionValidation // no foreign currency information is present: if (!$this->hasForeignCurrencyInfo($transaction)) { - $validator->errors()->add(sprintf('transactions.%d.foreign_amount', $index), (string)trans('validation.require_foreign_currency')); + $validator->errors()->add(sprintf('transactions.%d.foreign_amount', $index), (string) trans('validation.require_foreign_currency')); return; } // wrong currency information is present $foreignCurrencyCode = $transaction['foreign_currency_code'] ?? false; - $foreignCurrencyId = (int)($transaction['foreign_currency_id'] ?? 0); + $foreignCurrencyId = (int) ($transaction['foreign_currency_id'] ?? 0); app('log')->debug(sprintf('Foreign currency code seems to be #%d "%s"', $foreignCurrencyId, $foreignCurrencyCode), $transaction); if ($foreignCurrencyCode !== $sourceCurrency->code && $foreignCurrencyId !== $sourceCurrency->id) { - $validator->errors()->add(sprintf('transactions.%d.foreign_currency_code', $index), (string)trans('validation.require_foreign_src')); + $validator->errors()->add(sprintf('transactions.%d.foreign_currency_code', $index), (string) trans('validation.require_foreign_src')); return; } @@ -271,19 +271,19 @@ trait TransactionValidation // no foreign currency information is present: if (!$this->hasForeignCurrencyInfo($transaction)) { - $validator->errors()->add(sprintf('transactions.%d.foreign_amount', $index), (string)trans('validation.require_foreign_currency')); + $validator->errors()->add(sprintf('transactions.%d.foreign_amount', $index), (string) trans('validation.require_foreign_currency')); return; } // wrong currency information is present $foreignCurrencyCode = $transaction['foreign_currency_code'] ?? false; - $foreignCurrencyId = (int)($transaction['foreign_currency_id'] ?? 0); + $foreignCurrencyId = (int) ($transaction['foreign_currency_id'] ?? 0); app('log')->debug(sprintf('Foreign currency code seems to be #%d "%s"', $foreignCurrencyId, $foreignCurrencyCode), $transaction); if ($foreignCurrencyCode !== $destinationCurrency->code && $foreignCurrencyId !== $destinationCurrency->id) { app('log')->debug(sprintf('No match on code, "%s" vs "%s"', $foreignCurrencyCode, $destinationCurrency->code)); app('log')->debug(sprintf('No match on ID, #%d vs #%d', $foreignCurrencyId, $destinationCurrency->id)); - $validator->errors()->add(sprintf('transactions.%d.foreign_amount', $index), (string)trans('validation.require_foreign_dest')); + $validator->errors()->add(sprintf('transactions.%d.foreign_amount', $index), (string) trans('validation.require_foreign_dest')); } } } @@ -321,7 +321,7 @@ trait TransactionValidation if ('' === $transaction['foreign_amount']) { return false; } - if (0 === bccomp('0', (string)$transaction['foreign_amount'])) { + if (0 === bccomp('0', (string) $transaction['foreign_amount'])) { return false; } @@ -386,7 +386,7 @@ trait TransactionValidation || array_key_exists('source_number', $transaction) ) { app('log')->debug('Will try to validate source account information.'); - $sourceId = (int)($transaction['source_id'] ?? 0); + $sourceId = (int) ($transaction['source_id'] ?? 0); $sourceName = $transaction['source_name'] ?? null; $sourceIban = $transaction['source_iban'] ?? null; $sourceNumber = $transaction['source_number'] ?? null; @@ -425,7 +425,7 @@ trait TransactionValidation $accountValidator->source = $source; } } - $destinationId = (int)($transaction['destination_id'] ?? 0); + $destinationId = (int) ($transaction['destination_id'] ?? 0); $destinationName = $transaction['destination_name'] ?? null; $destinationIban = $transaction['destination_iban'] ?? null; $destinationNumber = $transaction['destination_number'] ?? null; @@ -444,7 +444,7 @@ trait TransactionValidation private function getTransactionType(TransactionGroup $group, array $transactions): string { - return $transactions[0]['type'] ?? strtolower((string)$group->transactionJournals()->first()?->transactionType->type); + return $transactions[0]['type'] ?? strtolower((string) $group->transactionJournals()->first()?->transactionType->type); } private function getOriginalSource(array $transaction, TransactionGroup $transactionGroup): ?Account @@ -457,7 +457,7 @@ trait TransactionValidation /** @var TransactionJournal $journal */ foreach ($transactionGroup->transactionJournals as $journal) { - $journalId = (int)($transaction['transaction_journal_id'] ?? 0); + $journalId = (int) ($transaction['transaction_journal_id'] ?? 0); if ($journal->id === $journalId) { return $journal->transactions()->where('amount', '<', 0)->first()?->account; } @@ -476,7 +476,7 @@ trait TransactionValidation // need at least one transaction if (0 === count($transactions)) { - $validator->errors()->add('transactions', (string)trans('validation.at_least_one_transaction')); + $validator->errors()->add('transactions', (string) trans('validation.at_least_one_transaction')); } } @@ -494,7 +494,7 @@ trait TransactionValidation $transactions = $this->getTransactionsArray($validator); // need at least one transaction if (0 === count($transactions)) { - $validator->errors()->add('transactions.0.description', (string)trans('validation.at_least_one_transaction')); + $validator->errors()->add('transactions.0.description', (string) trans('validation.at_least_one_transaction')); app('log')->debug('Added error: at_least_one_transaction.'); return; @@ -510,7 +510,7 @@ trait TransactionValidation $transactions = $this->getTransactionsArray($validator); foreach (array_keys($transactions) as $key) { if (!is_int($key)) { - $validator->errors()->add('transactions.0.description', (string)trans('validation.at_least_one_transaction')); + $validator->errors()->add('transactions.0.description', (string) trans('validation.at_least_one_transaction')); app('log')->debug('Added error: at_least_one_transaction.'); return; @@ -535,13 +535,13 @@ trait TransactionValidation } $unique = array_unique($types); if (count($unique) > 1) { - $validator->errors()->add('transactions.0.type', (string)trans('validation.transaction_types_equal')); + $validator->errors()->add('transactions.0.type', (string) trans('validation.transaction_types_equal')); return; } $first = $unique[0] ?? 'invalid'; if ('invalid' === $first) { - $validator->errors()->add('transactions.0.type', (string)trans('validation.invalid_transaction_type')); + $validator->errors()->add('transactions.0.type', (string) trans('validation.invalid_transaction_type')); } } @@ -554,14 +554,14 @@ trait TransactionValidation $transactions = $this->getTransactionsArray($validator); $types = []; foreach ($transactions as $transaction) { - $originalType = $this->getOriginalType((int)($transaction['transaction_journal_id'] ?? 0)); + $originalType = $this->getOriginalType((int) ($transaction['transaction_journal_id'] ?? 0)); // if type is not set, fall back to the type of the journal, if one is given. $types[] = $transaction['type'] ?? $originalType; } $unique = array_unique($types); if (count($unique) > 1) { app('log')->warning('Add error for mismatch transaction types.'); - $validator->errors()->add('transactions.0.type', (string)trans('validation.transaction_types_equal')); + $validator->errors()->add('transactions.0.type', (string) trans('validation.transaction_types_equal')); return; } @@ -609,22 +609,22 @@ trait TransactionValidation default: case 'withdrawal': if (count($sources) > 1) { - $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.source_id', (string) trans('validation.all_accounts_equal')); } break; case 'deposit': if (count($dests) > 1) { - $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.destination_id', (string) trans('validation.all_accounts_equal')); } break; case 'transfer': if (count($sources) > 1 || count($dests) > 1) { - $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal')); - $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.source_id', (string) trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.destination_id', (string) trans('validation.all_accounts_equal')); } break; @@ -658,14 +658,14 @@ trait TransactionValidation $result = $this->compareAccountData($type, $comparison); if (false === $result) { if ('withdrawal' === $type) { - $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.source_id', (string) trans('validation.all_accounts_equal')); } if ('deposit' === $type) { - $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.destination_id', (string) trans('validation.all_accounts_equal')); } if ('transfer' === $type) { - $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal')); - $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.source_id', (string) trans('validation.all_accounts_equal')); + $validator->errors()->add('transactions.0.destination_id', (string) trans('validation.all_accounts_equal')); } app('log')->warning('Add error about equal accounts.'); @@ -684,7 +684,7 @@ trait TransactionValidation /** @var array $transaction */ foreach ($transactions as $transaction) { // source or destination may be omitted. If this is the case, use the original source / destination name + ID. - $originalData = $this->getOriginalData((int)($transaction['transaction_journal_id'] ?? 0)); + $originalData = $this->getOriginalData((int) ($transaction['transaction_journal_id'] ?? 0)); // get field. $comparison[$field][] = $transaction[$field] ?? $originalData[$field]; diff --git a/changelog.md b/changelog.md index 0a743732d8..5d416e3168 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,44 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 6.2.0 - 2025-01-xx + +### Added + +- Multi-currency support. If you set `ENABLE_EXCHANGE_RATES=true` and optionally `ENABLE_EXTERNAL_RATES=true` Firefly III will try to calculate all foreign currencies back to your native currency. This is a work in progress, not all fields and all places will support this yet. Please check out the [documentation](https://docs.firefly-iii.org/explanation/financial-concepts/exchange-rates/). +- Notifications support Nfty, Pushover, Slack and Discord. +- Many new security related notifications. +- [Issue 5523](https://github.com/firefly-iii/firefly-iii/issues/5523) (Add comment on a budget for a given month) reported by @n-serrette +- [Issue 8531](https://github.com/firefly-iii/firefly-iii/issues/8531) (Add `notes` to transaction audit report) reported by @clouserw +- [Issue 8307](https://github.com/firefly-iii/firefly-iii/issues/8307) (Notification support for Ntfy (and other push notification tools)) reported by @ragnarkarlsson +- [Issue 7945](https://github.com/firefly-iii/firefly-iii/issues/7945) ("Rules" that only trigger manually) reported by @SekoiaTree +- [Issue 6760](https://github.com/firefly-iii/firefly-iii/issues/6760) (Add a new trigger for automated rules) reported by @Gsyltc +- [Issue 6557](https://github.com/firefly-iii/firefly-iii/issues/6557) (Piggy Banks - Draw Funds from Multiple Accounts) reported by @BugPhobic +- [Issue 5532](https://github.com/firefly-iii/firefly-iii/issues/5532) (Asset prices and exchange rates) reported by @svozniuk +- [Issue 6314](https://github.com/firefly-iii/firefly-iii/issues/6314) (Currencies and exchange rates) reported by @JC5 +- [Issue 9586](https://github.com/firefly-iii/firefly-iii/issues/9586) (Non en_US translated string in sign-up mail) reported by @benni347 + +### Changed + +- Firefly III requires PHP 8.4. +- [Issue 9501](https://github.com/firefly-iii/firefly-iii/issues/9501) (PHP8.4 support) reported by @JC5 +- Docker container no longer runs under root. +- "Bills" are now called "subscriptions" to better reflect their purpose. + +### Removed + +- Removed support for PHP 8.3 and lower. +- Removed Docker support for linux/arm/v7, linux/arm/v8 and linux/386. Sorry. + +### Fixed + +- [Issue 9532](https://github.com/firefly-iii/firefly-iii/issues/9532) (ReportSum Integrity Check fails due to empty foreign_amount) reported by @SircasticFox +- [Issue 7288](https://github.com/firefly-iii/firefly-iii/issues/7288) (currentMonthStart/currentMonthEnd not working for no-budget view) reported by @bradsk88 + +### API + +- API changes related to new features are [documented](#). + ## 6.1.25 - 2024-12-19 ### Fixed diff --git a/composer.json b/composer.json index 2fe0d54e56..cbabb0f7ef 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,7 @@ } ], "require": { - "php": ">=8.3", + "php": ">=8.4", "ext-bcmath": "*", "ext-curl": "*", "ext-fileinfo": "*", @@ -84,12 +84,10 @@ "bacon/bacon-qr-code": "2.*", "diglactic/laravel-breadcrumbs": "^9", "gdbots/query-parser": "^3.0", - "genealabs/laravel-model-caching": "^11.0", "guzzlehttp/guzzle": "^7.8", "jc5/google2fa-laravel": "^2.0", "jc5/recovery": "^2", - "laravel-json-api/laravel": "^5.0", - "laravel-json-api/non-eloquent": "^4.0", + "laravel-notification-channels/pushover": "^4.0", "laravel/framework": "^11", "laravel/passport": "^12", "laravel/sanctum": "^4", @@ -110,7 +108,8 @@ "spatie/period": "^2.4", "symfony/expression-language": "^7.0", "symfony/http-client": "^7.1", - "symfony/mailgun-mailer": "^7.1" + "symfony/mailgun-mailer": "^7.1", + "wijourdil/ntfy-notification-channel": "^3.0" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.9", @@ -170,13 +169,11 @@ "@php artisan clear-compiled", "@php artisan cache:clear", "@php artisan firefly-iii:upgrade-database", - "@php artisan firefly-iii:correct-database", - "@php artisan firefly-iii:report-integrity", "@php artisan firefly-iii:laravel-passport-keys", - "@php artisan firefly:instructions update" + "@php artisan firefly-iii:instructions update" ], "post-install-cmd": [ - "@php artisan firefly:instructions install", + "@php artisan firefly-iii:instructions install", "@php artisan firefly-iii:verify-security-alerts" ], "unit-test": [ diff --git a/composer.lock b/composer.lock index 6c33138fdd..2e5b9f9a3c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0022d91fa553233dbd10f00845a672cb", + "content-hash": "19d10c2c3cd2c2a8d4b12d9d01a34491", "packages": [ { "name": "bacon/bacon-qr-code", @@ -545,12 +545,12 @@ "type": "library", "extra": { "laravel": { - "providers": [ - "Diglactic\\Breadcrumbs\\ServiceProvider" - ], "aliases": { "Breadcrumbs": "Diglactic\\Breadcrumbs\\Breadcrumbs" - } + }, + "providers": [ + "Diglactic\\Breadcrumbs\\ServiceProvider" + ] } }, "autoload": { @@ -820,16 +820,16 @@ }, { "name": "egulias/email-validator", - "version": "4.0.2", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" + "reference": "b115554301161fa21467629f1e1391c1936de517" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", - "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", + "reference": "b115554301161fa21467629f1e1391c1936de517", "shasum": "" }, "require": { @@ -875,7 +875,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" }, "funding": [ { @@ -883,7 +883,7 @@ "type": "github" } ], - "time": "2023-10-06T06:47:41+00:00" + "time": "2024-12-27T00:36:43+00:00" }, { "name": "facade/ignition-contracts", @@ -1182,130 +1182,6 @@ }, "time": "2021-12-05T19:44:35+00:00" }, - { - "name": "genealabs/laravel-model-caching", - "version": "11.0.1", - "source": { - "type": "git", - "url": "https://github.com/mikebronner/laravel-model-caching.git", - "reference": "2a38f0f1ed3554cf2da272d66c4d08a7885f196b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mikebronner/laravel-model-caching/zipball/2a38f0f1ed3554cf2da272d66c4d08a7885f196b", - "reference": "2a38f0f1ed3554cf2da272d66c4d08a7885f196b", - "shasum": "" - }, - "require": { - "genealabs/laravel-pivot-events": "^10.0|^11.0", - "illuminate/cache": "^10.0|^11.0", - "illuminate/config": "^10.0|^11.0", - "illuminate/console": "^10.0|^11.0", - "illuminate/container": "^10.0|^11.0", - "illuminate/database": "^10.0|^11.0", - "illuminate/http": "^10.0|^11.0", - "illuminate/support": "^10.0|^11.0", - "php": ">=8.1" - }, - "require-dev": { - "doctrine/dbal": "^3.3", - "fakerphp/faker": "^1.11", - "laravel/legacy-factories": "^1.3", - "laravel/nova": "^4.0", - "orchestra/testbench": "^8.0|^9.0", - "orchestra/testbench-browser-kit": "^8.0", - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.0", - "slevomat/coding-standard": "^7.0|^8.14", - "squizlabs/php_codesniffer": "^3.6", - "symfony/thanks": "^1.2" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "GeneaLabs\\LaravelModelCaching\\Providers\\Service" - ] - } - }, - "autoload": { - "psr-4": { - "GeneaLabs\\LaravelModelCaching\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike Bronner", - "email": "hello@genealabs.com" - } - ], - "description": "Automatic caching for Eloquent models.", - "support": { - "issues": "https://github.com/mikebronner/laravel-model-caching/issues", - "source": "https://github.com/mikebronner/laravel-model-caching/tree/11.0.1" - }, - "time": "2024-03-14T23:34:57+00:00" - }, - { - "name": "genealabs/laravel-pivot-events", - "version": "11.0.0", - "source": { - "type": "git", - "url": "https://github.com/mikebronner/laravel-pivot-events.git", - "reference": "16e974d80160774641f4323f5ffb757b79f300d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mikebronner/laravel-pivot-events/zipball/16e974d80160774641f4323f5ffb757b79f300d3", - "reference": "16e974d80160774641f4323f5ffb757b79f300d3", - "shasum": "" - }, - "require": { - "illuminate/database": "^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0" - }, - "require-dev": { - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5", - "symfony/thanks": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "GeneaLabs\\LaravelPivotEvents\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike Bronner", - "email": "hello@genealabs.com", - "homepage": "https://genealabs.com", - "role": "Developer" - } - ], - "description": "This package introduces new eloquent events for sync(), attach(), detach() or updateExistingPivot() methods on BelongsToMany relation.", - "homepage": "https://github.com/mikebronner/laravel-pivot-events", - "keywords": [ - "eloquent events", - "eloquent extra events", - "laravel BelongsToMany events", - "laravel pivot events", - "laravel sync events" - ], - "support": { - "issues": "https://github.com/mikebronner/laravel-pivot-events/issues", - "source": "https://github.com/mikebronner/laravel-pivot-events" - }, - "time": "2024-03-14T23:24:54+00:00" - }, { "name": "graham-campbell/result-type", "version": "v1.1.3", @@ -1935,173 +1811,45 @@ "time": "2022-03-31T05:55:34+00:00" }, { - "name": "laravel-json-api/core", - "version": "v5.0.1", + "name": "laravel-notification-channels/pushover", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/laravel-json-api/core.git", - "reference": "e2f6696580166f7b6384318e28168252c2bd20f4" + "url": "https://github.com/laravel-notification-channels/pushover.git", + "reference": "31ff8c124f54de69673145c90836dd6cf7a01223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/core/zipball/e2f6696580166f7b6384318e28168252c2bd20f4", - "reference": "e2f6696580166f7b6384318e28168252c2bd20f4", + "url": "https://api.github.com/repos/laravel-notification-channels/pushover/zipball/31ff8c124f54de69673145c90836dd6cf7a01223", + "reference": "31ff8c124f54de69673145c90836dd6cf7a01223", "shasum": "" }, "require": { - "ext-json": "*", - "illuminate/auth": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/http": "^11.0", - "illuminate/support": "^11.0", - "php": "^8.2" + "guzzlehttp/guzzle": "^7.0.1", + "illuminate/notifications": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^10.5" + "dms/phpunit-arraysubset-asserts": ">=0.1.0", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.0 || ^9.0", + "phpunit/phpunit": "^9.3 || ^10.5" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "LaravelJsonApi\\Core\\": "src/Core", - "LaravelJsonApi\\Contracts\\": "src/Contracts" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "Contracts and support classes for Laravel JSON:API packages.", - "homepage": "https://github.com/laravel-json-api/core", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel-json-api/core/issues", - "source": "https://github.com/laravel-json-api/core/tree/v5.0.1" - }, - "time": "2024-11-30T16:31:42+00:00" - }, - { - "name": "laravel-json-api/eloquent", - "version": "v4.4.0", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/eloquent.git", - "reference": "fd7ebf898fb6e78bcbb8cba1a3b0e0d2554df244" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/eloquent/zipball/fd7ebf898fb6e78bcbb8cba1a3b0e0d2554df244", - "reference": "fd7ebf898fb6e78bcbb8cba1a3b0e0d2554df244", - "shasum": "" - }, - "require": { - "ext-json": "*", - "illuminate/database": "^11.0", - "illuminate/support": "^11.0", - "laravel-json-api/core": "^4.3.2|^5.0.1", - "php": "^8.2" - }, - "require-dev": { - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "LaravelJsonApi\\Eloquent\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "Serialize Eloquent models as JSON:API resources.", - "homepage": "https://github.com/laravel-json-api/eloquent", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel-json-api/eloquent/issues", - "source": "https://github.com/laravel-json-api/eloquent/tree/v4.4.0" - }, - "time": "2024-11-30T17:36:37+00:00" - }, - { - "name": "laravel-json-api/encoder-neomerx", - "version": "v4.1.0", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/encoder-neomerx.git", - "reference": "077c745a0c3caca535d30cacbb36b8daee29b812" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/encoder-neomerx/zipball/077c745a0c3caca535d30cacbb36b8daee29b812", - "reference": "077c745a0c3caca535d30cacbb36b8daee29b812", - "shasum": "" - }, - "require": { - "ext-json": "*", - "illuminate/contracts": "^11.0", - "illuminate/support": "^11.0", - "laravel-json-api/core": "^4.3.2|^5.0.1", - "laravel-json-api/neomerx-json-api": "^5.0.3", - "php": "^8.2" - }, - "require-dev": { - "phpunit/phpunit": "^10.5" + "suggest": { + "ext-exif": "Required for image attachment support" }, "type": "library", "extra": { "laravel": { "providers": [ - "LaravelJsonApi\\Encoder\\Neomerx\\ServiceProvider" + "NotificationChannels\\Pushover\\PushoverServiceProvider" ] - }, - "branch-alias": { - "dev-develop": "4.x-dev" } }, "autoload": { "psr-4": { - "LaravelJsonApi\\Encoder\\Neomerx\\": "src/" + "NotificationChannels\\Pushover\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2110,441 +1858,19 @@ ], "authors": [ { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" + "name": "Casper Boone", + "email": "mail@casperboone.nl", + "homepage": "https://casperboone.nl", + "role": "Developer" } ], - "description": "Encode JSON:API resources using the neomerx/json-api package.", - "homepage": "https://github.com/laravel-json-api/encoder-neomerx", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], + "description": "Pushover notifications for Laravel.", + "homepage": "https://github.com/laravel-notification-channels/pushover", "support": { - "issues": "https://github.com/laravel-json-api/encoder-neomerx/issues", - "source": "https://github.com/laravel-json-api/encoder-neomerx/tree/v4.1.0" + "issues": "https://github.com/laravel-notification-channels/pushover/issues", + "source": "https://github.com/laravel-notification-channels/pushover/tree/4.0.0" }, - "time": "2024-11-30T17:28:56+00:00" - }, - { - "name": "laravel-json-api/exceptions", - "version": "v3.1.0", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/exceptions.git", - "reference": "1969ba640de3ec288e0a447532c9b43e0941d475" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/exceptions/zipball/1969ba640de3ec288e0a447532c9b43e0941d475", - "reference": "1969ba640de3ec288e0a447532c9b43e0941d475", - "shasum": "" - }, - "require": { - "ext-json": "*", - "illuminate/contracts": "^11.0", - "illuminate/pipeline": "^11.0", - "laravel-json-api/core": "^4.3.2|^5.0.1", - "laravel-json-api/validation": "^4.2", - "php": "^8.2" - }, - "require-dev": { - "laravel-json-api/testing": "^3.0", - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "LaravelJsonApi\\Exceptions\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "JSON:API exception parsing for Laravel applications.", - "homepage": "https://github.com/laravel-json-api/exceptions", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel-json-api/exceptions/issues", - "source": "https://github.com/laravel-json-api/exceptions/tree/v3.1.0" - }, - "time": "2024-11-30T17:20:13+00:00" - }, - { - "name": "laravel-json-api/laravel", - "version": "v5.0.2", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/laravel.git", - "reference": "53045c6a55b4923e3cfadc085fd5f0b7c8c79cfc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/laravel/zipball/53045c6a55b4923e3cfadc085fd5f0b7c8c79cfc", - "reference": "53045c6a55b4923e3cfadc085fd5f0b7c8c79cfc", - "shasum": "" - }, - "require": { - "ext-json": "*", - "laravel-json-api/core": "^5.0.1", - "laravel-json-api/eloquent": "^4.4", - "laravel-json-api/encoder-neomerx": "^4.1", - "laravel-json-api/exceptions": "^3.1", - "laravel-json-api/spec": "^3.1", - "laravel-json-api/validation": "^4.2", - "laravel/framework": "^11.0", - "php": "^8.2" - }, - "require-dev": { - "laravel-json-api/testing": "^3.0.2", - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "JsonApi": "LaravelJsonApi\\Core\\Facades\\JsonApi", - "JsonApiRoute": "LaravelJsonApi\\Laravel\\Facades\\JsonApiRoute" - }, - "providers": [ - "LaravelJsonApi\\Laravel\\ServiceProvider" - ] - }, - "branch-alias": { - "dev-develop": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "LaravelJsonApi\\Laravel\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "JSON:API for Laravel applications.", - "homepage": "https://github.com/laravel-json-api/laravel", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel-json-api/laravel/issues", - "source": "https://github.com/laravel-json-api/laravel/tree/v5.0.2" - }, - "time": "2024-12-03T20:43:07+00:00" - }, - { - "name": "laravel-json-api/neomerx-json-api", - "version": "v5.0.3", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/neomerx-json-api.git", - "reference": "836342be5eb4bcf6c3734c7f8e82c9ceec3be550" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/neomerx-json-api/zipball/836342be5eb4bcf6c3734c7f8e82c9ceec3be550", - "reference": "836342be5eb4bcf6c3734c7f8e82c9ceec3be550", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.4|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.17", - "mockery/mockery": "^1.4.4", - "phpmd/phpmd": "^2.11.1", - "phpunit/phpunit": "^9.5.10", - "scrutinizer/ocular": "^1.8", - "squizlabs/php_codesniffer": "^3.6.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "5.x-dev" - } - }, - "autoload": { - "files": [ - "src/I18n/format.php" - ], - "psr-4": { - "Neomerx\\JsonApi\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "neomerx", - "email": "info@neomerx.com" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "Framework agnostic JSON API (jsonapi.org) implementation", - "homepage": "https://github.com/neomerx/json-api", - "keywords": [ - "JSON-API", - "api", - "json", - "jsonapi", - "jsonapi.org", - "neomerx" - ], - "support": { - "issues": "https://github.com/neomerx/json-api/issues", - "source": "https://github.com/laravel-json-api/neomerx-json-api/tree/v5.0.3" - }, - "time": "2024-11-29T17:49:31+00:00" - }, - { - "name": "laravel-json-api/non-eloquent", - "version": "v4.1.0", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/non-eloquent.git", - "reference": "3a28054ba3abd38323ec7c926419fc2a229766dd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/non-eloquent/zipball/3a28054ba3abd38323ec7c926419fc2a229766dd", - "reference": "3a28054ba3abd38323ec7c926419fc2a229766dd", - "shasum": "" - }, - "require": { - "laravel-json-api/core": "^4.3.2|^5.0.1", - "php": "^8.2" - }, - "require-dev": { - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "LaravelJsonApi\\NonEloquent\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "Construct JSON:API resources for non-Eloquent classes.", - "homepage": "https://github.com/laravel-json-api/non-eloquent", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel-json-api/non-eloquent/issues", - "source": "https://github.com/laravel-json-api/non-eloquent/tree/v4.1.0" - }, - "time": "2024-11-30T17:51:30+00:00" - }, - { - "name": "laravel-json-api/spec", - "version": "v3.1.0", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/spec.git", - "reference": "75a7a77de4421d58b0f38e2c94fae728b9d3690e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/spec/zipball/75a7a77de4421d58b0f38e2c94fae728b9d3690e", - "reference": "75a7a77de4421d58b0f38e2c94fae728b9d3690e", - "shasum": "" - }, - "require": { - "ext-json": "*", - "illuminate/contracts": "^11.0", - "illuminate/pipeline": "^11.0", - "illuminate/support": "^11.0", - "laravel-json-api/core": "^4.3.2|^5.0.1", - "php": "^8.2" - }, - "require-dev": { - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "LaravelJsonApi\\Spec\\ServiceProvider" - ] - }, - "branch-alias": { - "dev-develop": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "LaravelJsonApi\\Spec\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "Validate JSON documents for compliance with the JSON:API specification.", - "homepage": "https://github.com/laravel-json-api/spec", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel-json-api/spec/issues", - "source": "https://github.com/laravel-json-api/spec/tree/v3.1.0" - }, - "time": "2024-11-30T16:59:19+00:00" - }, - { - "name": "laravel-json-api/validation", - "version": "v4.2.0", - "source": { - "type": "git", - "url": "https://github.com/laravel-json-api/validation.git", - "reference": "635e942e44f01186ddea1c7a29d8e2d0de2c1540" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/validation/zipball/635e942e44f01186ddea1c7a29d8e2d0de2c1540", - "reference": "635e942e44f01186ddea1c7a29d8e2d0de2c1540", - "shasum": "" - }, - "require": { - "ext-json": "*", - "illuminate/contracts": "^11.0", - "illuminate/support": "^11.0", - "laravel-json-api/core": "^4.3.2|^5.0.1", - "php": "^8.2" - }, - "require-dev": { - "orchestra/testbench": "^9.0", - "phpunit/phpunit": "^10.5" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "LaravelJsonApi\\Validation\\ServiceProvider" - ] - }, - "branch-alias": { - "dev-develop": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "LaravelJsonApi\\Validation\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cloud Creativity Ltd", - "email": "info@cloudcreativity.co.uk" - }, - { - "name": "Christopher Gammie", - "email": "contact@gammie.co.uk" - } - ], - "description": "Laravel validation for JSON:API resources.", - "homepage": "https://github.com/laravel-json-api/validation", - "keywords": [ - "JSON-API", - "jsonapi", - "jsonapi.org", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel-json-api/validation/issues", - "source": "https://github.com/laravel-json-api/validation/tree/v4.2.0" - }, - "time": "2024-11-30T17:11:17+00:00" + "time": "2024-03-20T08:14:56+00:00" }, { "name": "laravel/framework", @@ -3114,13 +2440,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - }, "laravel": { "providers": [ "Laravel\\Ui\\UiServiceProvider" ] + }, + "branch-alias": { + "dev-master": "4.x-dev" } }, "autoload": { @@ -3288,16 +2614,16 @@ }, { "name": "league/commonmark", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d150f911e0079e90ae3c106734c93137c184f932" + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d150f911e0079e90ae3c106734c93137c184f932", - "reference": "d150f911e0079e90ae3c106734c93137c184f932", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", "shasum": "" }, "require": { @@ -3391,7 +2717,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T15:34:16+00:00" + "time": "2024-12-29T14:10:59+00:00" }, { "name": "league/config", @@ -3876,16 +3202,16 @@ }, { "name": "league/oauth2-server", - "version": "8.5.4", + "version": "8.5.5", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "ab7714d073844497fd222d5d0a217629089936bc" + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/ab7714d073844497fd222d5d0a217629089936bc", - "reference": "ab7714d073844497fd222d5d0a217629089936bc", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/cc8778350f905667e796b3c2364a9d3bd7a73518", + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518", "shasum": "" }, "require": { @@ -3952,7 +3278,7 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-server/issues", - "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.4" + "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.5" }, "funding": [ { @@ -3960,7 +3286,7 @@ "type": "github" } ], - "time": "2023-08-25T22:35:12+00:00" + "time": "2024-12-20T23:06:10+00:00" }, { "name": "league/uri", @@ -4377,16 +3703,16 @@ }, { "name": "nesbot/carbon", - "version": "3.8.2", + "version": "3.8.4", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947" + "reference": "129700ed449b1f02d70272d2ac802357c8c30c58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947", - "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/129700ed449b1f02d70272d2ac802357c8c30c58", + "reference": "129700ed449b1f02d70272d2ac802357c8c30c58", "shasum": "" }, "require": { @@ -4418,10 +3744,6 @@ ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev", - "dev-2.x": "2.x-dev" - }, "laravel": { "providers": [ "Carbon\\Laravel\\ServiceProvider" @@ -4431,6 +3753,10 @@ "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" } }, "autoload": { @@ -4479,7 +3805,7 @@ "type": "tidelift" } ], - "time": "2024-11-07T17:46:48+00:00" + "time": "2024-12-27T09:25:35+00:00" }, { "name": "nette/schema", @@ -7036,6 +6362,66 @@ ], "time": "2024-12-02T08:43:31+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.18.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "8332205b90d17164913244f4a8e13ab7e6761d29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/8332205b90d17164913244f4a8e13ab7e6761d29", + "reference": "8332205b90d17164913244f4a8e13ab7e6761d29", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0", + "pestphp/pest": "^1.22|^2", + "phpunit/phpunit": "^9.5.24|^10.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.18.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-12-30T13:13:39+00:00" + }, { "name": "spatie/period", "version": "2.4.0", @@ -7208,12 +6594,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -7515,12 +6901,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -7738,12 +7124,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -9548,12 +8934,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -9808,12 +9194,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -10101,27 +9487,27 @@ "time": "2024-10-18T07:58:17+00:00" }, { - "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.2.7", + "name": "thecodingmachine/safe", + "version": "v2.5.0", "source": { "type": "git", - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb" + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/83ee6f38df0a63106a9e4536e3060458b74ccedb", - "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "php": "^5.5 || ^7.0 || ^8.0", - "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "php": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^1.0" }, "type": "library", "extra": { @@ -10129,6 +9515,147 @@ "dev-master": "2.2.x-dev" } }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", + "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "deprecated/strings.php", + "lib/special_cases.php", + "deprecated/mysqli.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" + }, + "time": "2023-04-05T11:54:14+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, "autoload": { "psr-4": { "TijsVerkoyen\\CssToInlineStyles\\": "src" @@ -10149,22 +9676,22 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.2.7" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" }, - "time": "2023-12-08T13:03:43+00:00" + "time": "2024-12-21T16:25:41+00:00" }, { "name": "twig/twig", - "version": "v3.17.1", + "version": "v3.18.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "677ef8da6497a03048192aeeb5aa3018e379ac71" + "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/677ef8da6497a03048192aeeb5aa3018e379ac71", - "reference": "677ef8da6497a03048192aeeb5aa3018e379ac71", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50", + "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50", "shasum": "" }, "require": { @@ -10219,7 +9746,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.17.1" + "source": "https://github.com/twigphp/Twig/tree/v3.18.0" }, "funding": [ { @@ -10231,7 +9758,61 @@ "type": "tidelift" } ], - "time": "2024-12-12T09:58:10+00:00" + "time": "2024-12-29T10:51:50+00:00" + }, + { + "name": "verifiedjoseph/ntfy-php-library", + "version": "v4.7.0", + "source": { + "type": "git", + "url": "https://github.com/VerifiedJoseph/ntfy-php-library.git", + "reference": "c84d4dd7074d4cd89bfc1c10b1aa7f2568105412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VerifiedJoseph/ntfy-php-library/zipball/c84d4dd7074d4cd89bfc1c10b1aa7f2568105412", + "reference": "c84d4dd7074d4cd89bfc1c10b1aa7f2568105412", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.4", + "php": "^8.2" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^11.4", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ntfy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "VerifiedJoseph", + "homepage": "https://github.com/VerifiedJoseph" + } + ], + "description": "PHP library for interacting with a Ntfy server", + "homepage": "https://github.com/VerifiedJoseph/ntfy-php-library", + "keywords": [ + "Ntfy" + ], + "support": { + "issues": "https://github.com/VerifiedJoseph/ntfy-php-library/issues", + "source": "https://github.com/VerifiedJoseph/ntfy-php-library/tree/v4.7.0" + }, + "time": "2024-12-10T10:48:38+00:00" }, { "name": "vlucas/phpdotenv", @@ -10448,21 +10029,96 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "wijourdil/ntfy-notification-channel", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/wijourdil/ntfy-notification-channel.git", + "reference": "508ad1f0e1852b0bed966e9ebbf6f760ca2bac7b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wijourdil/ntfy-notification-channel/zipball/508ad1f0e1852b0bed966e9ebbf6f760ca2bac7b", + "reference": "508ad1f0e1852b0bed966e9ebbf6f760ca2bac7b", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.13.0", + "thecodingmachine/safe": "^2.4", + "verifiedjoseph/ntfy-php-library": "^4.0", + "webmozart/assert": "^1.11" + }, + "require-dev": { + "larastan/larastan": "^2.0.1", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^8.1", + "orchestra/testbench": "^9.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^10.1", + "thecodingmachine/phpstan-safe-rule": "^1.2" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "NtfyNotificationChannel": "Wijourdil\\NtfyNotificationChannel\\Facades\\NtfyNotificationChannel" + }, + "providers": [ + "Wijourdil\\NtfyNotificationChannel\\NtfyNotificationChannelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Wijourdil\\NtfyNotificationChannel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Wilfried Jourdil", + "email": "wijourdil@protonmail.com", + "role": "Developer" + } + ], + "description": "ntfy.sh Notification Channel for Laravel", + "homepage": "https://github.com/wijourdil/ntfy-notification-channel", + "keywords": [ + "laravel", + "notification", + "ntfy-notification-channel", + "ntfy.sh", + "wijourdil" + ], + "support": { + "issues": "https://github.com/wijourdil/ntfy-notification-channel/issues", + "source": "https://github.com/wijourdil/ntfy-notification-channel/tree/3.0.0" + }, + "time": "2024-04-17T12:37:09+00:00" } ], "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.14.9", + "version": "v3.14.10", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "2e805a6bd4e1aa83774316bb062703c65d0691ef" + "reference": "56b9bd235e3fe62e250124804009ce5bab97cc63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/2e805a6bd4e1aa83774316bb062703c65d0691ef", - "reference": "2e805a6bd4e1aa83774316bb062703c65d0691ef", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/56b9bd235e3fe62e250124804009ce5bab97cc63", + "reference": "56b9bd235e3fe62e250124804009ce5bab97cc63", "shasum": "" }, "require": { @@ -10521,7 +10177,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.9" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.10" }, "funding": [ { @@ -10533,24 +10189,24 @@ "type": "github" } ], - "time": "2024-11-25T14:51:20+00:00" + "time": "2024-12-23T10:10:42+00:00" }, { "name": "barryvdh/laravel-ide-helper", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "b7675670f75914bf34afdea52a6c2fe3781f7c44" + "reference": "2a41415f01bf3c409d200f6cdd940c1e7d86cfd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/b7675670f75914bf34afdea52a6c2fe3781f7c44", - "reference": "b7675670f75914bf34afdea52a6c2fe3781f7c44", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/2a41415f01bf3c409d200f6cdd940c1e7d86cfd3", + "reference": "2a41415f01bf3c409d200f6cdd940c1e7d86cfd3", "shasum": "" }, "require": { - "barryvdh/reflection-docblock": "^2.1.2", + "barryvdh/reflection-docblock": "^2.2", "composer/class-map-generator": "^1.0", "ext-json": "*", "illuminate/console": "^11.15", @@ -10583,7 +10239,7 @@ ] }, "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -10615,7 +10271,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-ide-helper/issues", - "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.3.0" + "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.4.0" }, "funding": [ { @@ -10627,24 +10283,24 @@ "type": "github" } ], - "time": "2024-12-18T08:24:19+00:00" + "time": "2024-12-29T12:10:58+00:00" }, { "name": "barryvdh/reflection-docblock", - "version": "v2.1.3", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/barryvdh/ReflectionDocBlock.git", - "reference": "c6fad15f7c878be21650c51e1f841bca7e49752e" + "reference": "818be8de6af4d16ef3ad51ea9234b3d37026ee5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/c6fad15f7c878be21650c51e1f841bca7e49752e", - "reference": "c6fad15f7c878be21650c51e1f841bca7e49752e", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/818be8de6af4d16ef3ad51ea9234b3d37026ee5f", + "reference": "818be8de6af4d16ef3ad51ea9234b3d37026ee5f", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "^8.5.14|^9" @@ -10656,7 +10312,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.3.x-dev" } }, "autoload": { @@ -10677,9 +10333,9 @@ } ], "support": { - "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.1.3" + "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.3.0" }, - "time": "2024-10-23T11:41:03+00:00" + "time": "2024-12-30T10:35:04+00:00" }, { "name": "cloudcreativity/json-api-testing", @@ -10839,13 +10495,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { @@ -11280,16 +10936,16 @@ }, { "name": "maximebf/debugbar", - "version": "v1.23.4", + "version": "v1.23.5", "source": { "type": "git", - "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "0815f47bdd867b816b4bf2ca1c7bd7f89e1527ca" + "url": "https://github.com/php-debugbar/php-debugbar.git", + "reference": "eeabd61a1f19ba5dcd5ac4585a477130ee03ce25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/0815f47bdd867b816b4bf2ca1c7bd7f89e1527ca", - "reference": "0815f47bdd867b816b4bf2ca1c7bd7f89e1527ca", + "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/eeabd61a1f19ba5dcd5ac4585a477130ee03ce25", + "reference": "eeabd61a1f19ba5dcd5ac4585a477130ee03ce25", "shasum": "" }, "require": { @@ -11341,10 +10997,10 @@ "debugbar" ], "support": { - "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.4" + "issues": "https://github.com/php-debugbar/php-debugbar/issues", + "source": "https://github.com/php-debugbar/php-debugbar/tree/v1.23.5" }, - "time": "2024-12-05T10:36:51+00:00" + "time": "2024-12-15T19:20:42+00:00" }, { "name": "mockery/mockery", @@ -11491,16 +11147,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -11543,9 +11199,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -12435,16 +12091,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.39", + "version": "10.5.40", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4e89eff200b801db58f3d580ad7426431949eaa9" + "reference": "e6ddda95af52f69c1e0c7b4f977cccb58048798c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4e89eff200b801db58f3d580ad7426431949eaa9", - "reference": "4e89eff200b801db58f3d580ad7426431949eaa9", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e6ddda95af52f69c1e0c7b4f977cccb58048798c", + "reference": "e6ddda95af52f69c1e0c7b4f977cccb58048798c", "shasum": "" }, "require": { @@ -12516,7 +12172,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.39" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.40" }, "funding": [ { @@ -12532,7 +12188,7 @@ "type": "tidelift" } ], - "time": "2024-12-11T10:51:07+00:00" + "time": "2024-12-21T05:49:06+00:00" }, { "name": "sebastian/cli-parser", @@ -13562,7 +13218,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3", + "php": ">=8.4", "ext-bcmath": "*", "ext-curl": "*", "ext-fileinfo": "*", diff --git a/config/auth.php b/config/auth.php index 6f95c59d94..64c07ce59b 100644 --- a/config/auth.php +++ b/config/auth.php @@ -24,7 +24,7 @@ declare(strict_types=1); use FireflyIII\User; -if ('ldap' === strtolower((string)env('AUTHENTICATION_GUARD'))) { +if ('ldap' === strtolower((string) env('AUTHENTICATION_GUARD'))) { exit('LDAP is no longer supported by Firefly III v5.7+. Sorry about that. You will have to switch to "remote_user_guard", and use tools like Authelia or Keycloak to use LDAP together with Firefly III.'); } diff --git a/config/bindables.php b/config/bindables.php new file mode 100644 index 0000000000..4467cb466d --- /dev/null +++ b/config/bindables.php @@ -0,0 +1,127 @@ + [ + // models + 'account' => Account::class, + 'attachment' => Attachment::class, + 'availableBudget' => AvailableBudget::class, + 'bill' => Bill::class, + 'budget' => Budget::class, + 'budgetLimit' => BudgetLimit::class, + 'category' => Category::class, + 'linkType' => LinkType::class, + 'transactionType' => TransactionType::class, + 'journalLink' => TransactionJournalLink::class, + 'currency' => TransactionCurrency::class, + 'objectGroup' => ObjectGroup::class, + 'piggyBank' => PiggyBank::class, + 'preference' => Preference::class, + 'tj' => TransactionJournal::class, + 'tag' => Tag::class, + 'recurrence' => Recurrence::class, + 'rule' => Rule::class, + 'ruleGroup' => RuleGroup::class, + 'transactionGroup' => TransactionGroup::class, + 'user' => User::class, + 'webhook' => Webhook::class, + 'webhookMessage' => WebhookMessage::class, + 'webhookAttempt' => WebhookAttempt::class, + 'invitedUser' => InvitedUser::class, + + // strings + 'currency_code' => CurrencyCode::class, + + // dates + 'start_date' => Date::class, + 'end_date' => Date::class, + 'date' => Date::class, + + // lists + 'accountList' => AccountList::class, + 'doubleList' => AccountList::class, + 'budgetList' => BudgetList::class, + 'journalList' => JournalList::class, + 'categoryList' => CategoryList::class, + 'tagList' => TagList::class, + + // others + 'fromCurrencyCode' => CurrencyCode::class, + 'toCurrencyCode' => CurrencyCode::class, + 'cliToken' => CLIToken::class, + 'tagOrId' => TagOrId::class, + 'dynamicConfigKey' => DynamicConfigKey::class, + 'eitherConfigKey' => EitherConfigKey::class, + + // V2 API endpoints: + 'userGroupAccount' => UserGroupAccount::class, + 'userGroupTransaction' => UserGroupTransaction::class, + 'userGroupBill' => UserGroupBill::class, + 'userGroupExchangeRate' => UserGroupExchangeRate::class, + 'userGroup' => UserGroup::class, + ], +]; diff --git a/config/cache.php b/config/cache.php index 5da5576828..7b0e7d97b5 100644 --- a/config/cache.php +++ b/config/cache.php @@ -83,7 +83,7 @@ return [ 'servers' => [ [ 'host' => env('MEMCACHED_HOST', '127.0.0.1'), - 'port' => (int)env('MEMCACHED_PORT', 11211), + 'port' => (int) env('MEMCACHED_PORT', 11211), 'weight' => 100, ], ], diff --git a/config/cer.php b/config/cer.php index ddfde2d15b..6ef20aebfb 100644 --- a/config/cer.php +++ b/config/cer.php @@ -28,47 +28,48 @@ return [ 'download_enabled' => env('ENABLE_EXTERNAL_RATES', false), // if currencies are added, default rates must be added as well! - // last exchange rate update: 6-6-2022 + // last exchange rate update: 2024-12-30 // source: https://www.xe.com/currencyconverter/ - 'date' => '2022-06-06', + 'date' => '2024-12-30', // all rates are from EUR to $currency: 'rates' => [ // europa 'EUR' => 1, - 'HUF' => 387.9629, - 'GBP' => 0.85420754, - 'UAH' => 31.659752, - 'PLN' => 4.581788, - 'TRY' => 17.801397, - 'DKK' => 7.4389753, + 'HUF' => 410.79798, + 'GBP' => 0.82858703, + 'UAH' => 43.485934, + 'PLN' => 4.2708542, + 'TRY' => 36.804124, + 'DKK' => 7.4591, + 'RON' => 4.9768699, // Americas - 'USD' => 1.0722281, - 'BRL' => 5.0973173, - 'CAD' => 1.3459969, - 'MXN' => 20.899824, + 'USD' => 1.0430046, + 'BRL' => 6.4639113, + 'CAD' => 1.5006908, + 'MXN' => 21.249542, // Oceania currencies - 'IDR' => 15466.299, - 'AUD' => 1.4838549, - 'NZD' => 1.6425829, + 'IDR' => 16860.057, + 'AUD' => 1.6705648, + 'NZD' => 1.8436945, // africa - 'EGP' => 19.99735, - 'MAD' => 10.573307, - 'ZAR' => 16.413167, + 'EGP' => 53.038174, + 'MAD' => 10.521629, + 'ZAR' => 19.460263, // asia - 'JPY' => 140.15257, - 'RMB' => 7.1194265, - 'CNY' => 1, - 'RUB' => 66.000895, - 'INR' => 83.220481, + 'JPY' => 164.74767, + 'RMB' => 7.6138994, + 'CNY' => 7.6138994, + 'RUB' => 108.56771, + 'INR' => 89.157391, // int - 'ILS' => 3.5712508, - 'CHF' => 1.0323891, - 'HRK' => 7.5220845, + 'ILS' => 3.8428028, + 'CHF' => 0.94044969, + 'HRK' => 7.5345, // replaced by EUR ], ]; diff --git a/config/firefly.php b/config/firefly.php index 299e3481d0..0f7ff0fc66 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -22,48 +22,19 @@ declare(strict_types=1); +use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; -use FireflyIII\Models\Attachment; -use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; -use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Category; -use FireflyIII\Models\InvitedUser; -use FireflyIII\Models\LinkType; -use FireflyIII\Models\ObjectGroup; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\Preference; use FireflyIII\Models\Recurrence; -use FireflyIII\Models\Rule; -use FireflyIII\Models\RuleGroup; use FireflyIII\Models\Tag; use FireflyIII\Models\Transaction; -use FireflyIII\Models\TransactionCurrency; -use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\TransactionJournalLink; -use FireflyIII\Models\TransactionType as TransactionTypeModel; -use FireflyIII\Models\UserGroup; -use FireflyIII\Models\Webhook; -use FireflyIII\Models\WebhookAttempt; -use FireflyIII\Models\WebhookMessage; -use FireflyIII\Support\Binder\AccountList; -use FireflyIII\Support\Binder\BudgetList; -use FireflyIII\Support\Binder\CategoryList; -use FireflyIII\Support\Binder\CLIToken; -use FireflyIII\Support\Binder\CurrencyCode; use FireflyIII\Support\Binder\Date; -use FireflyIII\Support\Binder\DynamicConfigKey; -use FireflyIII\Support\Binder\EitherConfigKey; -use FireflyIII\Support\Binder\JournalList; -use FireflyIII\Support\Binder\TagList; -use FireflyIII\Support\Binder\TagOrId; -use FireflyIII\Support\Binder\UserGroupAccount; -use FireflyIII\Support\Binder\UserGroupBill; -use FireflyIII\Support\Binder\UserGroupTransaction; use FireflyIII\TransactionRules\Actions\AddTag; use FireflyIII\TransactionRules\Actions\ClearBudget; use FireflyIII\TransactionRules\Actions\ClearCategory; @@ -110,7 +81,7 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => '6.1.25', + 'version' => 'develop/2024-12-30', 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 25, @@ -147,9 +118,6 @@ return [ 'update_endpoint' => 'https://version.firefly-iii.org/index.json', 'update_minimum_age' => 7, - // notifications - 'available_notifications' => ['bill_reminder', 'new_access_token', 'transaction_creation', 'user_login', 'rule_action_failures'], - 'admin_notifications' => ['admin_new_reg', 'user_new_reg', 'new_version', 'invite_created', 'invite_redeemed'], // enabled languages 'languages' => [ @@ -226,14 +194,14 @@ return [ // account types that may have or set a currency 'valid_currency_account_types' => [ - AccountType::ASSET, - AccountType::LOAN, - AccountType::DEBT, - AccountType::MORTGAGE, - AccountType::CASH, - AccountType::INITIAL_BALANCE, - AccountType::LIABILITY_CREDIT, - AccountType::RECONCILIATION, + AccountTypeEnum::ASSET->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::CASH->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LIABILITY_CREDIT->value, + AccountTypeEnum::RECONCILIATION->value, ], // "value must be in this list" values @@ -324,7 +292,7 @@ return [ 'application/json', ], 'accountRoles' => ['defaultAsset', 'sharedAsset', 'savingAsset', 'ccAsset', 'cashWalletAsset'], - 'valid_liabilities' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], + 'valid_liabilities' => [AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value], 'ccTypes' => ['monthlyFull' => 'Full payment every month'], 'credit_card_types' => ['monthlyFull'], @@ -351,60 +319,60 @@ return [ 'liability' => 'Liabilities', ], 'subIconsByIdentifier' => [ - 'asset' => 'fa-money', - AccountType::ASSET => 'fa-money', - AccountType::DEFAULT => 'fa-money', - AccountType::CASH => 'fa-money', - 'expense' => 'fa-shopping-cart', - AccountType::EXPENSE => 'fa-shopping-cart', - AccountType::BENEFICIARY => 'fa-shopping-cart', - 'revenue' => 'fa-download', - AccountType::REVENUE => 'fa-download', - 'import' => 'fa-download', - AccountType::IMPORT => 'fa-download', - 'liabilities' => 'fa-ticket', + 'asset' => 'fa-money', + AccountTypeEnum::ASSET->value => 'fa-money', + AccountTypeEnum::DEFAULT->value => 'fa-money', + AccountTypeEnum::CASH->value => 'fa-money', + 'expense' => 'fa-shopping-cart', + AccountTypeEnum::EXPENSE->value => 'fa-shopping-cart', + AccountTypeEnum::BENEFICIARY->value => 'fa-shopping-cart', + 'revenue' => 'fa-download', + AccountTypeEnum::REVENUE->value => 'fa-download', + 'import' => 'fa-download', + AccountTypeEnum::IMPORT->value => 'fa-download', + 'liabilities' => 'fa-ticket', ], 'accountTypesByIdentifier' => [ - 'asset' => [AccountType::DEFAULT, AccountType::ASSET], - 'expense' => [AccountType::EXPENSE, AccountType::BENEFICIARY], - 'revenue' => [AccountType::REVENUE], - 'import' => [AccountType::IMPORT], - 'liabilities' => [AccountType::LOAN, AccountType::DEBT, AccountType::CREDITCARD, AccountType::MORTGAGE], + 'asset' => [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value], + 'expense' => [AccountTypeEnum::EXPENSE->value, AccountTypeEnum::BENEFICIARY->value], + 'revenue' => [AccountTypeEnum::REVENUE->value], + 'import' => [AccountTypeEnum::IMPORT->value], + 'liabilities' => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::MORTGAGE->value], ], 'accountTypeByIdentifier' => [ - 'asset' => [AccountType::ASSET], - 'expense' => [AccountType::EXPENSE], - 'revenue' => [AccountType::REVENUE], - 'opening' => [AccountType::INITIAL_BALANCE], - 'initial' => [AccountType::INITIAL_BALANCE], - 'import' => [AccountType::IMPORT], - 'reconcile' => [AccountType::RECONCILIATION], - 'loan' => [AccountType::LOAN], - 'debt' => [AccountType::DEBT], - 'mortgage' => [AccountType::MORTGAGE], - 'liabilities' => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CREDITCARD], - 'liability' => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CREDITCARD], + 'asset' => [AccountTypeEnum::ASSET->value], + 'expense' => [AccountTypeEnum::EXPENSE->value], + 'revenue' => [AccountTypeEnum::REVENUE->value], + 'opening' => [AccountTypeEnum::INITIAL_BALANCE->value], + 'initial' => [AccountTypeEnum::INITIAL_BALANCE->value], + 'import' => [AccountTypeEnum::IMPORT->value], + 'reconcile' => [AccountTypeEnum::RECONCILIATION->value], + 'loan' => [AccountTypeEnum::LOAN->value], + 'debt' => [AccountTypeEnum::DEBT->value], + 'mortgage' => [AccountTypeEnum::MORTGAGE->value], + 'liabilities' => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::CREDITCARD->value], + 'liability' => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::CREDITCARD->value], ], 'shortNamesByFullName' => [ - AccountType::DEFAULT => 'asset', - AccountType::ASSET => 'asset', - AccountType::IMPORT => 'import', - AccountType::EXPENSE => 'expense', - AccountType::BENEFICIARY => 'expense', - AccountType::REVENUE => 'revenue', - AccountType::CASH => 'cash', - AccountType::INITIAL_BALANCE => 'initial-balance', - AccountType::RECONCILIATION => 'reconciliation', - AccountType::CREDITCARD => 'liabilities', - AccountType::LOAN => 'liabilities', - AccountType::DEBT => 'liabilities', - AccountType::MORTGAGE => 'liabilities', + AccountTypeEnum::DEFAULT->value => 'asset', + AccountTypeEnum::ASSET->value => 'asset', + AccountTypeEnum::IMPORT->value => 'import', + AccountTypeEnum::EXPENSE->value => 'expense', + AccountTypeEnum::BENEFICIARY->value => 'expense', + AccountTypeEnum::REVENUE->value => 'revenue', + AccountTypeEnum::CASH->value => 'cash', + AccountTypeEnum::INITIAL_BALANCE->value => 'initial-balance', + AccountTypeEnum::RECONCILIATION->value => 'reconciliation', + AccountTypeEnum::CREDITCARD->value => 'liabilities', + AccountTypeEnum::LOAN->value => 'liabilities', + AccountTypeEnum::DEBT->value => 'liabilities', + AccountTypeEnum::MORTGAGE->value => 'liabilities', ], 'shortLiabilityNameByFullName' => [ - AccountType::CREDITCARD => 'creditcard', - AccountType::LOAN => AccountType::LOAN, - AccountType::DEBT => AccountType::DEBT, - AccountType::MORTGAGE => AccountType::MORTGAGE, + AccountTypeEnum::CREDITCARD->value => 'creditcard', + AccountTypeEnum::LOAN->value => AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value => AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value => AccountTypeEnum::MORTGAGE->value, ], 'transactionTypesByType' => [ 'expenses' => ['Withdrawal'], @@ -430,64 +398,7 @@ return [ 'transfers' => 'fa-exchange', ], - 'bindables' => [ - // models - 'account' => Account::class, - 'attachment' => Attachment::class, - 'availableBudget' => AvailableBudget::class, - 'bill' => Bill::class, - 'budget' => Budget::class, - 'budgetLimit' => BudgetLimit::class, - 'category' => Category::class, - 'linkType' => LinkType::class, - 'transactionType' => TransactionTypeModel::class, - 'journalLink' => TransactionJournalLink::class, - 'currency' => TransactionCurrency::class, - 'objectGroup' => ObjectGroup::class, - 'piggyBank' => PiggyBank::class, - 'preference' => Preference::class, - 'tj' => TransactionJournal::class, - 'tag' => Tag::class, - 'recurrence' => Recurrence::class, - 'rule' => Rule::class, - 'ruleGroup' => RuleGroup::class, - 'transactionGroup' => TransactionGroup::class, - 'user' => User::class, - 'webhook' => Webhook::class, - 'webhookMessage' => WebhookMessage::class, - 'webhookAttempt' => WebhookAttempt::class, - 'invitedUser' => InvitedUser::class, - // strings - 'currency_code' => CurrencyCode::class, - - // dates - 'start_date' => Date::class, - 'end_date' => Date::class, - 'date' => Date::class, - - // lists - 'accountList' => AccountList::class, - 'doubleList' => AccountList::class, - 'budgetList' => BudgetList::class, - 'journalList' => JournalList::class, - 'categoryList' => CategoryList::class, - 'tagList' => TagList::class, - - // others - 'fromCurrencyCode' => CurrencyCode::class, - 'toCurrencyCode' => CurrencyCode::class, - 'cliToken' => CLIToken::class, - 'tagOrId' => TagOrId::class, - 'dynamicConfigKey' => DynamicConfigKey::class, - 'eitherConfigKey' => EitherConfigKey::class, - - // V2 API endpoints: - 'userGroupAccount' => UserGroupAccount::class, - 'userGroupTransaction' => UserGroupTransaction::class, - 'userGroupBill' => UserGroupBill::class, - 'userGroup' => UserGroup::class, - ], 'rule-actions' => [ 'set_category' => SetCategory::class, 'clear_category' => ClearCategory::class, @@ -549,309 +460,309 @@ return [ // expected source types for each transaction type, in order of preference. 'expected_source_types' => [ 'source' => [ - TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeEnum::DEPOSIT->value => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::REVENUE, AccountType::CASH], - TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeModel::OPENING_BALANCE => [ - AccountType::INITIAL_BALANCE, - AccountType::ASSET, - AccountType::LOAN, - AccountType::DEBT, - AccountType::MORTGAGE, + TransactionTypeEnum::WITHDRAWAL->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + TransactionTypeEnum::DEPOSIT->value => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::REVENUE->value, AccountTypeEnum::CASH->value], + TransactionTypeEnum::TRANSFER->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + TransactionTypeEnum::OPENING_BALANCE->value => [ + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::ASSET->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, ], - TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], - TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeEnum::RECONCILIATION->value => [AccountTypeEnum::RECONCILIATION->value, AccountTypeEnum::ASSET->value], + TransactionTypeEnum::LIABILITY_CREDIT->value => [AccountTypeEnum::LIABILITY_CREDIT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], // in case no transaction type is known yet, it could be anything. - 'none' => [ - AccountType::ASSET, - AccountType::EXPENSE, - AccountType::REVENUE, - AccountType::LOAN, - AccountType::DEBT, - AccountType::MORTGAGE, + 'none' => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::REVENUE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, ], ], 'destination' => [ - TransactionTypeModel::WITHDRAWAL => [ - AccountType::LOAN, - AccountType::DEBT, - AccountType::MORTGAGE, - AccountType::EXPENSE, - AccountType::CASH, + TransactionTypeEnum::WITHDRAWAL->value => [ + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::CASH->value, ], - TransactionTypeEnum::DEPOSIT->value => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionTypeModel::OPENING_BALANCE => [ - AccountType::INITIAL_BALANCE, - AccountType::ASSET, - AccountType::LOAN, - AccountType::DEBT, - AccountType::MORTGAGE, + TransactionTypeEnum::DEPOSIT->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + TransactionTypeEnum::TRANSFER->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + TransactionTypeEnum::OPENING_BALANCE->value => [ + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::ASSET->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, ], - TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], - TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeEnum::RECONCILIATION->value => [AccountTypeEnum::RECONCILIATION->value, AccountTypeEnum::ASSET->value], + TransactionTypeEnum::LIABILITY_CREDIT->value => [AccountTypeEnum::LIABILITY_CREDIT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], ], ], 'allowed_opposing_types' => [ 'source' => [ - AccountType::ASSET => [ - AccountType::ASSET, - AccountType::CASH, - AccountType::DEBT, - AccountType::EXPENSE, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::RECONCILIATION, - AccountType::MORTGAGE, + AccountTypeEnum::ASSET->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::CASH->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::RECONCILIATION->value, + AccountTypeEnum::MORTGAGE->value, ], - AccountType::CASH => [AccountType::ASSET], - AccountType::DEBT => [ - AccountType::ASSET, - AccountType::DEBT, - AccountType::EXPENSE, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::MORTGAGE, - AccountType::LIABILITY_CREDIT, + AccountTypeEnum::CASH->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::DEBT->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::LIABILITY_CREDIT->value, ], - AccountType::EXPENSE => [], // is not allowed as a source. - AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], - AccountType::LOAN => [ - AccountType::ASSET, - AccountType::DEBT, - AccountType::EXPENSE, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::MORTGAGE, - AccountType::LIABILITY_CREDIT, + AccountTypeEnum::EXPENSE->value => [], // is not allowed as a source. + AccountTypeEnum::INITIAL_BALANCE->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::LOAN->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::LIABILITY_CREDIT->value, ], - AccountType::MORTGAGE => [ - AccountType::ASSET, - AccountType::DEBT, - AccountType::EXPENSE, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::MORTGAGE, - AccountType::LIABILITY_CREDIT, + AccountTypeEnum::MORTGAGE->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::LIABILITY_CREDIT->value, ], - AccountType::RECONCILIATION => [AccountType::ASSET], - AccountType::REVENUE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], - AccountType::LIABILITY_CREDIT => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], + AccountTypeEnum::RECONCILIATION->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::REVENUE->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::LIABILITY_CREDIT->value => [AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value], ], 'destination' => [ - AccountType::ASSET => [ - AccountType::ASSET, - AccountType::CASH, - AccountType::DEBT, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::MORTGAGE, - AccountType::RECONCILIATION, - AccountType::REVENUE, + AccountTypeEnum::ASSET->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::CASH->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::RECONCILIATION->value, + AccountTypeEnum::REVENUE->value, ], - AccountType::CASH => [AccountType::ASSET], - AccountType::DEBT => [ - AccountType::ASSET, - AccountType::DEBT, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::MORTGAGE, - AccountType::REVENUE, + AccountTypeEnum::CASH->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::DEBT->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::REVENUE->value, ], - AccountType::EXPENSE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], - AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], - AccountType::LOAN => [ - AccountType::ASSET, - AccountType::DEBT, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::MORTGAGE, - AccountType::REVENUE, + AccountTypeEnum::EXPENSE->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::INITIAL_BALANCE->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::LOAN->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::REVENUE->value, ], - AccountType::MORTGAGE => [ - AccountType::ASSET, - AccountType::DEBT, - AccountType::INITIAL_BALANCE, - AccountType::LOAN, - AccountType::MORTGAGE, - AccountType::REVENUE, + AccountTypeEnum::MORTGAGE->value => [ + AccountTypeEnum::ASSET->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::MORTGAGE->value, + AccountTypeEnum::REVENUE->value, ], - AccountType::RECONCILIATION => [AccountType::ASSET], - AccountType::REVENUE => [], // is not allowed as a destination - AccountType::LIABILITY_CREDIT => [], // is not allowed as a destination + AccountTypeEnum::RECONCILIATION->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::REVENUE->value => [], // is not allowed as a destination + AccountTypeEnum::LIABILITY_CREDIT->value => [], // is not allowed as a destination ], ], // depending on the account type, return the allowed transaction types: 'allowed_transaction_types' => [ 'source' => [ - AccountType::ASSET => [ - TransactionTypeModel::WITHDRAWAL, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, - TransactionTypeModel::RECONCILIATION, + AccountTypeEnum::ASSET->value => [ + TransactionTypeEnum::WITHDRAWAL->value, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, + TransactionTypeEnum::RECONCILIATION->value, ], - AccountType::EXPENSE => [], // is not allowed as a source. - AccountType::REVENUE => [TransactionTypeEnum::DEPOSIT->value], - AccountType::LOAN => [ - TransactionTypeModel::WITHDRAWAL, + AccountTypeEnum::EXPENSE->value => [], // is not allowed as a source. + AccountTypeEnum::REVENUE->value => [TransactionTypeEnum::DEPOSIT->value], + AccountTypeEnum::LOAN->value => [ + TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, - TransactionTypeModel::LIABILITY_CREDIT, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, + TransactionTypeEnum::LIABILITY_CREDIT->value, ], - AccountType::DEBT => [ - TransactionTypeModel::WITHDRAWAL, + AccountTypeEnum::DEBT->value => [ + TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, - TransactionTypeModel::LIABILITY_CREDIT, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, + TransactionTypeEnum::LIABILITY_CREDIT->value, ], - AccountType::MORTGAGE => [ - TransactionTypeModel::WITHDRAWAL, + AccountTypeEnum::MORTGAGE->value => [ + TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, - TransactionTypeModel::LIABILITY_CREDIT, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, + TransactionTypeEnum::LIABILITY_CREDIT->value, ], - AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], - AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], - AccountType::LIABILITY_CREDIT => [TransactionTypeModel::LIABILITY_CREDIT], + AccountTypeEnum::INITIAL_BALANCE->value => [TransactionTypeEnum::OPENING_BALANCE->value], + AccountTypeEnum::RECONCILIATION->value => [TransactionTypeEnum::RECONCILIATION->value], + AccountTypeEnum::LIABILITY_CREDIT->value => [TransactionTypeEnum::LIABILITY_CREDIT->value], ], 'destination' => [ - AccountType::ASSET => [ + AccountTypeEnum::ASSET->value => [ TransactionTypeEnum::DEPOSIT->value, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, - TransactionTypeModel::RECONCILIATION, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, + TransactionTypeEnum::RECONCILIATION->value, ], - AccountType::EXPENSE => [TransactionTypeModel::WITHDRAWAL], - AccountType::REVENUE => [], // is not allowed as destination. - AccountType::LOAN => [ - TransactionTypeModel::WITHDRAWAL, + AccountTypeEnum::EXPENSE->value => [TransactionTypeEnum::WITHDRAWAL->value], + AccountTypeEnum::REVENUE->value => [], // is not allowed as destination. + AccountTypeEnum::LOAN->value => [ + TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, ], - AccountType::DEBT => [ - TransactionTypeModel::WITHDRAWAL, + AccountTypeEnum::DEBT->value => [ + TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, ], - AccountType::MORTGAGE => [ - TransactionTypeModel::WITHDRAWAL, + AccountTypeEnum::MORTGAGE->value => [ + TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, - TransactionTypeModel::TRANSFER, - TransactionTypeModel::OPENING_BALANCE, + TransactionTypeEnum::TRANSFER->value, + TransactionTypeEnum::OPENING_BALANCE->value, ], - AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], - AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], - AccountType::LIABILITY_CREDIT => [], // is not allowed as a destination + AccountTypeEnum::INITIAL_BALANCE->value => [TransactionTypeEnum::OPENING_BALANCE->value], + AccountTypeEnum::RECONCILIATION->value => [TransactionTypeEnum::RECONCILIATION->value], + AccountTypeEnum::LIABILITY_CREDIT->value => [], // is not allowed as a destination ], ], // having the source + dest will tell you the transaction type. 'account_to_transaction' => [ - AccountType::ASSET => [ - AccountType::ASSET => TransactionTypeModel::TRANSFER, - AccountType::CASH => TransactionTypeModel::WITHDRAWAL, - AccountType::DEBT => TransactionTypeModel::WITHDRAWAL, - AccountType::EXPENSE => TransactionTypeModel::WITHDRAWAL, - AccountType::INITIAL_BALANCE => TransactionTypeModel::OPENING_BALANCE, - AccountType::LOAN => TransactionTypeModel::WITHDRAWAL, - AccountType::MORTGAGE => TransactionTypeModel::WITHDRAWAL, - AccountType::RECONCILIATION => TransactionTypeModel::RECONCILIATION, + AccountTypeEnum::ASSET->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::TRANSFER->value, + AccountTypeEnum::CASH->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::DEBT->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::EXPENSE->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::INITIAL_BALANCE->value => TransactionTypeEnum::OPENING_BALANCE->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::RECONCILIATION->value => TransactionTypeEnum::RECONCILIATION->value, ], - AccountType::CASH => [ - AccountType::ASSET => TransactionTypeModel::DEPOSIT, - AccountType::LOAN => TransactionTypeModel::DEPOSIT, - AccountType::DEBT => TransactionTypeModel::DEPOSIT, - AccountType::MORTGAGE => TransactionTypeModel::DEPOSIT, + AccountTypeEnum::CASH->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::DEBT->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::DEPOSIT->value, ], - AccountType::DEBT => [ - AccountType::ASSET => TransactionTypeEnum::DEPOSIT->value, - AccountType::DEBT => TransactionTypeModel::TRANSFER, - AccountType::EXPENSE => TransactionTypeModel::WITHDRAWAL, - AccountType::INITIAL_BALANCE => TransactionTypeModel::OPENING_BALANCE, - AccountType::LOAN => TransactionTypeModel::TRANSFER, - AccountType::MORTGAGE => TransactionTypeModel::TRANSFER, + AccountTypeEnum::DEBT->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::DEBT->value => TransactionTypeEnum::TRANSFER->value, + AccountTypeEnum::EXPENSE->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::INITIAL_BALANCE->value => TransactionTypeEnum::OPENING_BALANCE->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::TRANSFER->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::TRANSFER->value, ], - AccountType::INITIAL_BALANCE => [ - AccountType::ASSET => TransactionTypeModel::OPENING_BALANCE, - AccountType::DEBT => TransactionTypeModel::OPENING_BALANCE, - AccountType::LOAN => TransactionTypeModel::OPENING_BALANCE, - AccountType::MORTGAGE => TransactionTypeModel::OPENING_BALANCE, + AccountTypeEnum::INITIAL_BALANCE->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::OPENING_BALANCE->value, + AccountTypeEnum::DEBT->value => TransactionTypeEnum::OPENING_BALANCE->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::OPENING_BALANCE->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::OPENING_BALANCE->value, ], - AccountType::LOAN => [ - AccountType::ASSET => TransactionTypeEnum::DEPOSIT->value, - AccountType::DEBT => TransactionTypeModel::TRANSFER, - AccountType::EXPENSE => TransactionTypeModel::WITHDRAWAL, - AccountType::INITIAL_BALANCE => TransactionTypeModel::OPENING_BALANCE, - AccountType::LOAN => TransactionTypeModel::TRANSFER, - AccountType::MORTGAGE => TransactionTypeModel::TRANSFER, + AccountTypeEnum::LOAN->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::DEBT->value => TransactionTypeEnum::TRANSFER->value, + AccountTypeEnum::EXPENSE->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::INITIAL_BALANCE->value => TransactionTypeEnum::OPENING_BALANCE->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::TRANSFER->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::TRANSFER->value, ], - AccountType::MORTGAGE => [ - AccountType::ASSET => TransactionTypeEnum::DEPOSIT->value, - AccountType::DEBT => TransactionTypeModel::TRANSFER, - AccountType::EXPENSE => TransactionTypeModel::WITHDRAWAL, - AccountType::INITIAL_BALANCE => TransactionTypeModel::OPENING_BALANCE, - AccountType::LOAN => TransactionTypeModel::TRANSFER, - AccountType::MORTGAGE => TransactionTypeModel::TRANSFER, + AccountTypeEnum::MORTGAGE->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::DEBT->value => TransactionTypeEnum::TRANSFER->value, + AccountTypeEnum::EXPENSE->value => TransactionTypeEnum::WITHDRAWAL->value, + AccountTypeEnum::INITIAL_BALANCE->value => TransactionTypeEnum::OPENING_BALANCE->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::TRANSFER->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::TRANSFER->value, ], - AccountType::RECONCILIATION => [ - AccountType::ASSET => TransactionTypeModel::RECONCILIATION, + AccountTypeEnum::RECONCILIATION->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::RECONCILIATION->value, ], - AccountType::REVENUE => [ - AccountType::ASSET => TransactionTypeEnum::DEPOSIT->value, - AccountType::DEBT => TransactionTypeEnum::DEPOSIT->value, - AccountType::LOAN => TransactionTypeEnum::DEPOSIT->value, - AccountType::MORTGAGE => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::REVENUE->value => [ + AccountTypeEnum::ASSET->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::DEBT->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::DEPOSIT->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::DEPOSIT->value, ], - AccountType::LIABILITY_CREDIT => [ - AccountType::DEBT => TransactionTypeModel::LIABILITY_CREDIT, - AccountType::LOAN => TransactionTypeModel::LIABILITY_CREDIT, - AccountType::MORTGAGE => TransactionTypeModel::LIABILITY_CREDIT, + AccountTypeEnum::LIABILITY_CREDIT->value => [ + AccountTypeEnum::DEBT->value => TransactionTypeEnum::LIABILITY_CREDIT->value, + AccountTypeEnum::LOAN->value => TransactionTypeEnum::LIABILITY_CREDIT->value, + AccountTypeEnum::MORTGAGE->value => TransactionTypeEnum::LIABILITY_CREDIT->value, ], - // AccountType::EXPENSE unlisted because it cant be a source + // AccountTypeEnum::EXPENSE->value unlisted because it cant be a source ], // allowed source -> destination accounts. 'source_dests' => [ - TransactionTypeModel::WITHDRAWAL => [ - AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH], - AccountType::LOAN => [AccountType::EXPENSE, AccountType::CASH], - AccountType::DEBT => [AccountType::EXPENSE, AccountType::CASH], - AccountType::MORTGAGE => [AccountType::EXPENSE, AccountType::CASH], + TransactionTypeEnum::WITHDRAWAL->value => [ + AccountTypeEnum::ASSET->value => [AccountTypeEnum::EXPENSE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::CASH->value], + AccountTypeEnum::LOAN->value => [AccountTypeEnum::EXPENSE->value, AccountTypeEnum::CASH->value], + AccountTypeEnum::DEBT->value => [AccountTypeEnum::EXPENSE->value, AccountTypeEnum::CASH->value], + AccountTypeEnum::MORTGAGE->value => [AccountTypeEnum::EXPENSE->value, AccountTypeEnum::CASH->value], ], - TransactionTypeEnum::DEPOSIT->value => [ - AccountType::REVENUE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - AccountType::CASH => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - AccountType::LOAN => [AccountType::ASSET], - AccountType::DEBT => [AccountType::ASSET], - AccountType::MORTGAGE => [AccountType::ASSET], + TransactionTypeEnum::DEPOSIT->value => [ + AccountTypeEnum::REVENUE->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::CASH->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::LOAN->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::DEBT->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::MORTGAGE->value => [AccountTypeEnum::ASSET->value], ], - TransactionTypeModel::TRANSFER => [ - AccountType::ASSET => [AccountType::ASSET], - AccountType::LOAN => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - AccountType::DEBT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeEnum::TRANSFER->value => [ + AccountTypeEnum::ASSET->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::LOAN->value => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::DEBT->value => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], + AccountTypeEnum::MORTGAGE->value => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], ], - TransactionTypeModel::OPENING_BALANCE => [ - AccountType::ASSET => [AccountType::INITIAL_BALANCE], - AccountType::LOAN => [AccountType::INITIAL_BALANCE], - AccountType::DEBT => [AccountType::INITIAL_BALANCE], - AccountType::MORTGAGE => [AccountType::INITIAL_BALANCE], - AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeEnum::OPENING_BALANCE->value => [ + AccountTypeEnum::ASSET->value => [AccountTypeEnum::INITIAL_BALANCE->value], + AccountTypeEnum::LOAN->value => [AccountTypeEnum::INITIAL_BALANCE->value], + AccountTypeEnum::DEBT->value => [AccountTypeEnum::INITIAL_BALANCE->value], + AccountTypeEnum::MORTGAGE->value => [AccountTypeEnum::INITIAL_BALANCE->value], + AccountTypeEnum::INITIAL_BALANCE->value => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], ], - TransactionTypeModel::RECONCILIATION => [ - AccountType::RECONCILIATION => [AccountType::ASSET], - AccountType::ASSET => [AccountType::RECONCILIATION], + TransactionTypeEnum::RECONCILIATION->value => [ + AccountTypeEnum::RECONCILIATION->value => [AccountTypeEnum::ASSET->value], + AccountTypeEnum::ASSET->value => [AccountTypeEnum::RECONCILIATION->value], ], - TransactionTypeModel::LIABILITY_CREDIT => [ - AccountType::LOAN => [AccountType::LIABILITY_CREDIT], - AccountType::DEBT => [AccountType::LIABILITY_CREDIT], - AccountType::MORTGAGE => [AccountType::LIABILITY_CREDIT], - AccountType::LIABILITY_CREDIT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionTypeEnum::LIABILITY_CREDIT->value => [ + AccountTypeEnum::LOAN->value => [AccountTypeEnum::LIABILITY_CREDIT->value], + AccountTypeEnum::DEBT->value => [AccountTypeEnum::LIABILITY_CREDIT->value], + AccountTypeEnum::MORTGAGE->value => [AccountTypeEnum::LIABILITY_CREDIT->value], + AccountTypeEnum::LIABILITY_CREDIT->value => [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], ], ], // if you add fields to this array, don't forget to update the export routine (ExportDataGenerator). @@ -892,14 +803,14 @@ return [ 'webhooks' => [ 'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3), ], - 'can_have_virtual_amounts' => [AccountType::ASSET], - 'can_have_opening_balance' => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + 'can_have_virtual_amounts' => [AccountTypeEnum::ASSET->value], + 'can_have_opening_balance' => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], 'dynamic_creation_allowed' => [ - AccountType::EXPENSE, - AccountType::REVENUE, - AccountType::INITIAL_BALANCE, - AccountType::RECONCILIATION, - AccountType::LIABILITY_CREDIT, + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::REVENUE->value, + AccountTypeEnum::INITIAL_BALANCE->value, + AccountTypeEnum::RECONCILIATION->value, + AccountTypeEnum::LIABILITY_CREDIT->value, ], 'valid_asset_fields' => ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], 'valid_cc_fields' => ['account_role', 'cc_monthly_payment_date', 'cc_type', 'account_number', 'currency_id', 'BIC', 'include_net_worth'], @@ -913,4 +824,7 @@ return [ // preselected account lists possibilities: 'preselected_accounts' => ['all', 'assets', 'liabilities'], + + // allowed to store a piggy bank in: + 'piggy_bank_account_types' => [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value], ]; diff --git a/config/logging.php b/config/logging.php index 145811d215..707db4249c 100644 --- a/config/logging.php +++ b/config/logging.php @@ -34,8 +34,8 @@ $validChannels = ['single', 'papertrail', 'stdout', 'daily', 'syslog', 'err $validAuditChannels = ['audit_papertrail', 'audit_stdout', 'audit_stdout', 'audit_daily', 'audit_syslog', 'audit_errorlog']; // which settings did the user set, if any? -$defaultLogChannel = (string)envNonEmpty('LOG_CHANNEL', 'stack'); -$auditLogChannel = (string)envNonEmpty('AUDIT_LOG_CHANNEL', ''); +$defaultLogChannel = (string) envNonEmpty('LOG_CHANNEL', 'stack'); +$auditLogChannel = (string) envNonEmpty('AUDIT_LOG_CHANNEL', ''); if ('stack' === $defaultLogChannel) { $defaultChannels = ['daily', 'stdout']; diff --git a/config/mail.php b/config/mail.php index c095afa351..e9742e651a 100644 --- a/config/mail.php +++ b/config/mail.php @@ -39,7 +39,7 @@ return [ 'smtp' => [ 'transport' => 'smtp', 'host' => envNonEmpty('MAIL_HOST', 'smtp.mailtrap.io'), - 'port' => (int)env('MAIL_PORT', 2525), + 'port' => (int) env('MAIL_PORT', 2525), 'encryption' => envNonEmpty('MAIL_ENCRYPTION', 'tls'), 'username' => envNonEmpty('MAIL_USERNAME', 'user@example.com'), 'password' => envNonEmpty('MAIL_PASSWORD', 'password'), diff --git a/config/notifications.php b/config/notifications.php new file mode 100644 index 0000000000..a3e1bd7630 --- /dev/null +++ b/config/notifications.php @@ -0,0 +1,67 @@ + [ + 'email' => ['enabled' => true, 'ui_configurable' => 0], + 'slack' => ['enabled' => true, 'ui_configurable' => 1], + 'ntfy' => ['enabled' => true, 'ui_configurable' => 1], + 'pushover' => ['enabled' => true, 'ui_configurable' => 1], + // 'gotify' => ['enabled' => false, 'ui_configurable' => 0], + // 'pushbullet' => ['enabled' => false, 'ui_configurable' => 0], + ], + 'notifications' => [ + 'user' => [ + // normal reminders + 'bill_reminder' => ['enabled' => true, 'configurable' => true], + 'transaction_creation' => ['enabled' => true, 'configurable' => true], + 'rule_action_failures' => ['enabled' => true, 'configurable' => true], + + // security reminders + 'new_access_token' => ['enabled' => true, 'configurable' => true], + 'user_login' => ['enabled' => true, 'configurable' => true], + 'login_failure' => ['enabled' => true, 'configurable' => true], + 'new_password' => ['enabled' => true, 'configurable' => false], + 'enabled_mfa' => ['enabled' => true, 'configurable' => false], + 'disabled_mfa' => ['enabled' => true, 'configurable' => false], + 'few_left_mfa' => ['enabled' => true, 'configurable' => false], + 'no_left_mfa' => ['enabled' => true, 'configurable' => false], + 'many_failed_mfa' => ['enabled' => true, 'configurable' => false], + 'new_backup_codes' => ['enabled' => true, 'configurable' => false], + ], + 'owner' => [ + // 'invitation_created' => ['enabled' => true], + // 'some_notification' => ['enabled' => true], + 'admin_new_reg' => ['enabled' => true], + 'user_new_reg' => ['enabled' => true], + 'new_version' => ['enabled' => true], + 'invite_created' => ['enabled' => true], + 'invite_redeemed' => ['enabled' => true], + 'unknown_user_attempt' => ['enabled' => true], + ], + ], + // // notifications + // 'available_notifications' => ['bill_reminder', 'new_access_token', 'transaction_creation', 'user_login', 'rule_action_failures'], + // 'admin_notifications' => ['admin_new_reg', 'user_new_reg', 'new_version', 'invite_created', 'invite_redeemed'], +]; diff --git a/config/ntfy-notification-channel.php b/config/ntfy-notification-channel.php new file mode 100644 index 0000000000..0b2725f073 --- /dev/null +++ b/config/ntfy-notification-channel.php @@ -0,0 +1,16 @@ + 'https://ntfy.sh', + 'topic' => '', + 'authentication' => [ + 'enabled' => false, + 'username' => '', + 'password' => '', + ], + +]; diff --git a/config/search.php b/config/search.php index 021da929f3..304cd080a0 100644 --- a/config/search.php +++ b/config/search.php @@ -24,221 +24,233 @@ declare(strict_types=1); return [ 'operators' => [ - 'user_action' => ['alias' => false, 'needs_context' => true], - 'account_id' => ['alias' => false, 'needs_context' => true], - 'reconciled' => ['alias' => false, 'needs_context' => false], - 'source_account_id' => ['alias' => false, 'needs_context' => true], - 'destination_account_id' => ['alias' => false, 'needs_context' => true], - 'transaction_type' => ['alias' => false, 'needs_context' => true], - 'type' => ['alias' => true, 'alias_for' => 'transaction_type', 'needs_context' => true], - 'tag_is' => ['alias' => false, 'needs_context' => true], - 'tag_is_not' => ['alias' => false, 'needs_context' => true], - 'tag' => ['alias' => true, 'alias_for' => 'tag_is', 'needs_context' => true], - 'tag_contains' => ['alias' => false, 'needs_context' => true], - 'tag_ends' => ['alias' => false, 'needs_context' => true], - 'tag_starts' => ['alias' => false, 'needs_context' => true], - 'description_is' => ['alias' => false, 'needs_context' => true], - 'description' => ['alias' => true, 'alias_for' => 'description_is', 'needs_context' => true], - 'description_contains' => ['alias' => false, 'needs_context' => true], - 'description_ends' => ['alias' => false, 'needs_context' => true], - 'description_starts' => ['alias' => false, 'needs_context' => true], - 'notes_is' => ['alias' => false, 'needs_context' => true], - 'notes_are' => ['alias' => true, 'alias_for' => 'notes_is', 'needs_context' => true], - 'notes_contains' => ['alias' => false, 'needs_context' => true], - 'notes_contain' => ['alias' => true, 'alias_for' => 'notes_contains', 'needs_context' => true], - 'notes' => ['alias' => true, 'alias_for' => 'notes_contains', 'needs_context' => true], - 'notes_ends' => ['alias' => false, 'needs_context' => true], - 'notes_end' => ['alias' => true, 'alias_for' => 'notes_ends', 'needs_context' => true], - 'notes_starts' => ['alias' => false, 'needs_context' => true], - 'notes_start' => ['alias' => true, 'alias_for' => 'notes_starts', 'needs_context' => true], - 'source_account_is' => ['alias' => false, 'needs_context' => true], - 'from_account_is' => ['alias' => true, 'alias_for' => 'source_account_is', 'needs_context' => true], - 'source_account_contains' => ['alias' => false, 'needs_context' => true], - 'source' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true], - 'from' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true], - 'from_account_contains' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true], - 'source_account_ends' => ['alias' => false, 'needs_context' => true], - 'from_account_ends' => ['alias' => true, 'alias_for' => 'source_account_ends', 'needs_context' => true], - 'source_account_starts' => ['alias' => false, 'needs_context' => true], - 'from_account_starts' => ['alias' => true, 'alias_for' => 'source_account_starts', 'needs_context' => true], - 'source_account_nr_is' => ['alias' => false, 'needs_context' => true], - 'from_account_nr_is' => ['alias' => true, 'alias_for' => 'source_account_nr_is', 'needs_context' => true], - 'source_account_nr_contains' => ['alias' => false, 'needs_context' => true], - 'from_account_nr_contains' => ['alias' => true, 'alias_for' => 'source_account_nr_contains', 'needs_context' => true], - 'source_account_nr_ends' => ['alias' => false, 'needs_context' => true], - 'from_account_nr_ends' => ['alias' => true, 'alias_for' => 'source_account_nr_ends', 'needs_context' => true], - 'source_account_nr_starts' => ['alias' => false, 'needs_context' => true], - 'from_account_nr_starts' => ['alias' => true, 'alias_for' => 'source_account_nr_starts', 'needs_context' => true], - 'destination_account_is' => ['alias' => false, 'needs_context' => true], - 'to_account_is' => ['alias' => true, 'alias_for' => 'destination_account_is', 'needs_context' => true], - 'destination_account_contains' => ['alias' => false, 'needs_context' => true], - 'destination' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true], - 'to' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true], - 'to_account_contains' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true], - 'destination_account_ends' => ['alias' => false, 'needs_context' => true], - 'to_account_ends' => ['alias' => true, 'alias_for' => 'destination_account_ends', 'needs_context' => true], - 'destination_account_starts' => ['alias' => false, 'needs_context' => true], - 'to_account_starts' => ['alias' => true, 'alias_for' => 'destination_account_starts', 'needs_context' => true], - 'destination_account_nr_is' => ['alias' => false, 'needs_context' => true], - 'to_account_nr_is' => ['alias' => true, 'alias_for' => 'destination_account_nr_is', 'needs_context' => true], - 'destination_account_nr_contains' => ['alias' => false, 'needs_context' => true], - 'to_account_nr_contains' => ['alias' => true, 'alias_for' => 'destination_account_nr_contains', 'needs_context' => true], - 'destination_account_nr_ends' => ['alias' => false, 'needs_context' => true], - 'to_account_nr_ends' => ['alias' => true, 'alias_for' => 'destination_account_nr_ends', 'needs_context' => true], - 'destination_account_nr_starts' => ['alias' => false, 'needs_context' => true], - 'to_account_nr_starts' => ['alias' => true, 'alias_for' => 'destination_account_nr_starts', 'needs_context' => true], - 'account_is' => ['alias' => false, 'needs_context' => true], - 'account_contains' => ['alias' => false, 'needs_context' => true], - 'account_ends' => ['alias' => false, 'needs_context' => true], - 'account_starts' => ['alias' => false, 'needs_context' => true], - 'account_nr_is' => ['alias' => false, 'needs_context' => true], - 'account_nr_contains' => ['alias' => false, 'needs_context' => true], - 'account_nr_ends' => ['alias' => false, 'needs_context' => true], - 'account_nr_starts' => ['alias' => false, 'needs_context' => true], - 'category_is' => ['alias' => false, 'needs_context' => true], - 'category_contains' => ['alias' => false, 'needs_context' => true], - 'category' => ['alias' => true, 'alias_for' => 'category_contains', 'needs_context' => true], - 'category_ends' => ['alias' => false, 'needs_context' => true], - 'category_starts' => ['alias' => false, 'needs_context' => true], - 'budget_is' => ['alias' => false, 'needs_context' => true], - 'budget_contains' => ['alias' => false, 'needs_context' => true], - 'budget' => ['alias' => true, 'alias_for' => 'budget_contains', 'needs_context' => true], - 'budget_ends' => ['alias' => false, 'needs_context' => true], - 'budget_starts' => ['alias' => false, 'needs_context' => true], - 'bill_is' => ['alias' => false, 'needs_context' => true], - 'bill_contains' => ['alias' => false, 'needs_context' => true], - 'bill' => ['alias' => true, 'alias_for' => 'bill_contains', 'needs_context' => true], - 'bill_ends' => ['alias' => false, 'needs_context' => true], - 'bill_starts' => ['alias' => false, 'needs_context' => true], - 'external_id_is' => ['alias' => false, 'needs_context' => true], - 'external_id_contains' => ['alias' => false, 'needs_context' => true], - 'external_id' => ['alias' => true, 'alias_for' => 'external_id_contains', 'needs_context' => true], - 'external_id_ends' => ['alias' => false, 'needs_context' => true], - 'external_id_starts' => ['alias' => false, 'needs_context' => true], - 'internal_reference_is' => ['alias' => false, 'needs_context' => true], - 'internal_reference_contains' => ['alias' => false, 'needs_context' => true], - 'internal_reference' => ['alias' => true, 'alias_for' => 'internal_reference_contains', 'needs_context' => true], - 'internal_reference_ends' => ['alias' => false, 'needs_context' => true], - 'internal_reference_starts' => ['alias' => false, 'needs_context' => true], - 'external_url_is' => ['alias' => false, 'needs_context' => true], - 'external_url_contains' => ['alias' => false, 'needs_context' => true], - 'external_url' => ['alias' => true, 'alias_for' => 'external_url_contains', 'needs_context' => true], - 'external_url_ends' => ['alias' => false, 'needs_context' => true], - 'external_url_starts' => ['alias' => false, 'needs_context' => true], - 'has_attachments' => ['alias' => false, 'needs_context' => false], - 'has_any_category' => ['alias' => false, 'needs_context' => false], - 'has_any_budget' => ['alias' => false, 'needs_context' => false], - 'has_any_bill' => ['alias' => false, 'needs_context' => false], - 'has_any_tag' => ['alias' => false, 'needs_context' => false], - 'any_notes' => ['alias' => false, 'needs_context' => false], - 'has_any_notes' => ['alias' => true, 'alias_for' => 'any_notes', 'needs_context' => false], - 'has_notes' => ['alias' => true, 'alias_for' => 'any_notes', 'needs_context' => false], - 'any_external_url' => ['alias' => false, 'needs_context' => false], - 'has_any_external_url' => ['alias' => true, 'alias_for' => 'any_external_url', 'needs_context' => false], - 'has_no_attachments' => ['alias' => false, 'needs_context' => false], - 'has_no_category' => ['alias' => false, 'needs_context' => false], - 'has_no_budget' => ['alias' => false, 'needs_context' => false], - 'has_no_bill' => ['alias' => false, 'needs_context' => false], - 'has_no_tag' => ['alias' => false, 'needs_context' => false], - 'no_notes' => ['alias' => false, 'needs_context' => false], - 'no_external_url' => ['alias' => false, 'needs_context' => false], - 'source_is_cash' => ['alias' => false, 'needs_context' => false], - 'destination_is_cash' => ['alias' => false, 'needs_context' => false], - 'account_is_cash' => ['alias' => false, 'needs_context' => false], - 'currency_is' => ['alias' => false, 'needs_context' => true], - 'foreign_currency_is' => ['alias' => false, 'needs_context' => true], - 'id' => ['alias' => false, 'trigger_class' => '', 'needs_context' => true], - 'journal_id' => ['alias' => false, 'trigger_class' => '', 'needs_context' => true], - 'recurrence_id' => ['alias' => false, 'trigger_class' => '', 'needs_context' => true], - 'date_on' => ['alias' => false, 'needs_context' => true], - 'date' => ['alias' => true, 'alias_for' => 'date_on', 'needs_context' => true], - 'date_is' => ['alias' => true, 'alias_for' => 'date_on', 'needs_context' => true], - 'on' => ['alias' => true, 'alias_for' => 'date_on', 'needs_context' => true], - 'date_before' => ['alias' => false, 'needs_context' => true], - 'before' => ['alias' => true, 'alias_for' => 'date_before', 'needs_context' => true], - 'date_after' => ['alias' => false, 'needs_context' => true], - 'after' => ['alias' => true, 'alias_for' => 'date_after', 'needs_context' => true], - 'interest_date_on' => ['alias' => false, 'needs_context' => true], - 'interest_date' => ['alias' => true, 'alias_for' => 'interest_date_on', 'needs_context' => true], - 'interest_date_is' => ['alias' => true, 'alias_for' => 'interest_date_on', 'needs_context' => true], - 'interest_date_before' => ['alias' => false, 'needs_context' => true], - 'interest_date_after' => ['alias' => false, 'needs_context' => true], - 'book_date_on' => ['alias' => false, 'needs_context' => true], - 'book_date' => ['alias' => true, 'alias_for' => 'book_date_on', 'needs_context' => true], - 'book_date_is' => ['alias' => true, 'alias_for' => 'book_date_on', 'needs_context' => true], - 'book_date_before' => ['alias' => false, 'needs_context' => true], - 'book_date_after' => ['alias' => false, 'needs_context' => true], - 'process_date_on' => ['alias' => false, 'needs_context' => true], - 'process_date' => ['alias' => true, 'alias_for' => 'process_date_on', 'needs_context' => true], - 'process_date_is' => ['alias' => true, 'alias_for' => 'process_date_on', 'needs_context' => true], - 'process_date_before' => ['alias' => false, 'needs_context' => true], - 'process_date_after' => ['alias' => false, 'needs_context' => true], - 'due_date_on' => ['alias' => false, 'needs_context' => true], - 'due_date' => ['alias' => true, 'alias_for' => 'due_date_on', 'needs_context' => true], - 'due_date_is' => ['alias' => true, 'alias_for' => 'due_date_on', 'needs_context' => true], - 'due_date_before' => ['alias' => false, 'needs_context' => true], - 'due_date_after' => ['alias' => false, 'needs_context' => true], - 'payment_date_on' => ['alias' => false, 'needs_context' => true], - 'payment_date' => ['alias' => true, 'alias_for' => 'payment_date_on', 'needs_context' => true], - 'payment_date_is' => ['alias' => true, 'alias_for' => 'payment_date_on', 'needs_context' => true], - 'payment_date_before' => ['alias' => false, 'needs_context' => true], - 'payment_date_after' => ['alias' => false, 'needs_context' => true], - 'invoice_date_on' => ['alias' => false, 'needs_context' => true], - 'invoice_date' => ['alias' => true, 'alias_for' => 'invoice_date_on', 'needs_context' => true], - 'invoice_date_is' => ['alias' => true, 'alias_for' => 'invoice_date_on', 'needs_context' => true], - 'invoice_date_before' => ['alias' => false, 'needs_context' => true], - 'invoice_date_after' => ['alias' => false, 'needs_context' => true], - 'created_at_on' => ['alias' => false, 'needs_context' => true], - 'created_at' => ['alias' => true, 'alias_for' => 'created_at_on', 'needs_context' => true], - 'created_at_is' => ['alias' => true, 'alias_for' => 'created_at_on', 'needs_context' => true], - 'created_at_before' => ['alias' => false, 'needs_context' => true], - 'created_at_after' => ['alias' => false, 'needs_context' => true], - 'updated_at_on' => ['alias' => false, 'needs_context' => true], - 'updated_at' => ['alias' => true, 'alias_for' => 'updated_at_on', 'needs_context' => true], - 'updated_at_is' => ['alias' => true, 'alias_for' => 'updated_at_on', 'needs_context' => true], - 'updated_at_before' => ['alias' => false, 'needs_context' => true], - 'updated_at_after' => ['alias' => false, 'needs_context' => true], - 'created_on_on' => ['alias' => true, 'alias_for' => 'created_at_on', 'needs_context' => true], - 'created_on' => ['alias' => true, 'alias_for' => 'created_at', 'needs_context' => true], - 'created_on_before' => ['alias' => true, 'alias_for' => 'created_at_before', 'needs_context' => true], - 'created_on_after' => ['alias' => true, 'alias_for' => 'created_at_after', 'needs_context' => true], - 'updated_on_on' => ['alias' => true, 'alias_for' => 'updated_at_on', 'needs_context' => true], - 'updated_on' => ['alias' => true, 'alias_for' => 'updated_at', 'needs_context' => true], - 'updated_on_before' => ['alias' => true, 'alias_for' => 'updated_at_before', 'needs_context' => true], - 'updated_on_after' => ['alias' => true, 'alias_for' => 'updated_at_after', 'needs_context' => true], - 'amount_is' => ['alias' => false, 'needs_context' => true], - 'amount' => ['alias' => true, 'alias_for' => 'amount_is', 'needs_context' => true], - 'amount_exactly' => ['alias' => true, 'alias_for' => 'amount_is', 'needs_context' => true], - 'amount_less' => ['alias' => false, 'needs_context' => true], - 'amount_max' => ['alias' => true, 'alias_for' => 'amount_less', 'needs_context' => true], - 'less' => ['alias' => true, 'alias_for' => 'amount_less', 'needs_context' => true], - 'amount_more' => ['alias' => false, 'needs_context' => true], - 'amount_min' => ['alias' => true, 'alias_for' => 'amount_more', 'needs_context' => true], - 'more' => ['alias' => true, 'alias_for' => 'amount_more', 'needs_context' => true], - 'foreign_amount_is' => ['alias' => false, 'needs_context' => true], - 'foreign_amount' => ['alias' => true, 'alias_for' => 'foreign_amount_is', 'needs_context' => true], - 'foreign_amount_less' => ['alias' => false, 'needs_context' => true], - 'foreign_amount_max' => ['alias' => true, 'alias_for' => 'foreign_amount_less', 'needs_context' => true], - 'foreign_amount_more' => ['alias' => false, 'needs_context' => true], - 'foreign_amount_min' => ['alias' => true, 'alias_for' => 'foreign_amount_more', 'needs_context' => true], - 'attachment_name_is' => ['alias' => false, 'needs_context' => true], - 'attachment' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true], - 'attachment_is' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true], - 'attachment_name' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true], - 'attachment_name_contains' => ['alias' => false, 'needs_context' => true], - 'attachment_name_starts' => ['alias' => false, 'needs_context' => true], - 'attachment_name_ends' => ['alias' => false, 'needs_context' => true], - 'attachment_notes' => ['alias' => true, 'alias_for' => 'attachment_notes_are', 'needs_context' => true], - 'attachment_notes_are' => ['alias' => false, 'needs_context' => true], - 'attachment_notes_contains' => ['alias' => false, 'needs_context' => true], - 'attachment_notes_contain' => ['alias' => true, 'alias_for' => 'attachment_notes_contains', 'needs_context' => true], - 'attachment_notes_starts' => ['alias' => false, 'needs_context' => true], - 'attachment_notes_start' => ['alias' => true, 'alias_for' => 'attachment_notes_starts', 'needs_context' => true], - 'attachment_notes_ends' => ['alias' => false, 'needs_context' => true], - 'attachment_notes_end' => ['alias' => true, 'alias_for' => 'attachment_notes_ends', 'needs_context' => true], - 'exists' => ['alias' => false, 'needs_context' => false], - 'sepa_ct_is' => ['alias' => false, 'needs_context' => true], - 'no_external_id' => ['alias' => false, 'needs_context' => false], - 'any_external_id' => ['alias' => false, 'needs_context' => false], + 'user_action' => ['alias' => false, 'needs_context' => true], + 'account_id' => ['alias' => false, 'needs_context' => true], + 'reconciled' => ['alias' => false, 'needs_context' => false], + 'source_account_id' => ['alias' => false, 'needs_context' => true], + 'destination_account_id' => ['alias' => false, 'needs_context' => true], + 'transaction_type' => ['alias' => false, 'needs_context' => true], + 'type' => ['alias' => true, 'alias_for' => 'transaction_type', 'needs_context' => true], + 'tag_is' => ['alias' => false, 'needs_context' => true], + 'tag_is_not' => ['alias' => false, 'needs_context' => true], + 'tag' => ['alias' => true, 'alias_for' => 'tag_is', 'needs_context' => true], + 'tag_contains' => ['alias' => false, 'needs_context' => true], + 'tag_ends' => ['alias' => false, 'needs_context' => true], + 'tag_starts' => ['alias' => false, 'needs_context' => true], + 'description_is' => ['alias' => false, 'needs_context' => true], + 'description' => ['alias' => true, 'alias_for' => 'description_is', 'needs_context' => true], + 'description_contains' => ['alias' => false, 'needs_context' => true], + 'description_ends' => ['alias' => false, 'needs_context' => true], + 'description_starts' => ['alias' => false, 'needs_context' => true], + 'notes_is' => ['alias' => false, 'needs_context' => true], + 'notes_are' => ['alias' => true, 'alias_for' => 'notes_is', 'needs_context' => true], + 'notes_contains' => ['alias' => false, 'needs_context' => true], + 'notes_contain' => ['alias' => true, 'alias_for' => 'notes_contains', 'needs_context' => true], + 'notes' => ['alias' => true, 'alias_for' => 'notes_contains', 'needs_context' => true], + 'notes_ends' => ['alias' => false, 'needs_context' => true], + 'notes_end' => ['alias' => true, 'alias_for' => 'notes_ends', 'needs_context' => true], + 'notes_starts' => ['alias' => false, 'needs_context' => true], + 'notes_start' => ['alias' => true, 'alias_for' => 'notes_starts', 'needs_context' => true], + 'source_account_is' => ['alias' => false, 'needs_context' => true], + 'from_account_is' => ['alias' => true, 'alias_for' => 'source_account_is', 'needs_context' => true], + 'source_account_contains' => ['alias' => false, 'needs_context' => true], + 'source' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true], + 'from' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true], + 'from_account_contains' => ['alias' => true, 'alias_for' => 'source_account_contains', 'needs_context' => true], + 'source_account_ends' => ['alias' => false, 'needs_context' => true], + 'from_account_ends' => ['alias' => true, 'alias_for' => 'source_account_ends', 'needs_context' => true], + 'source_account_starts' => ['alias' => false, 'needs_context' => true], + 'from_account_starts' => ['alias' => true, 'alias_for' => 'source_account_starts', 'needs_context' => true], + 'source_account_nr_is' => ['alias' => false, 'needs_context' => true], + 'from_account_nr_is' => ['alias' => true, 'alias_for' => 'source_account_nr_is', 'needs_context' => true], + 'source_account_nr_contains' => ['alias' => false, 'needs_context' => true], + 'from_account_nr_contains' => ['alias' => true, 'alias_for' => 'source_account_nr_contains', 'needs_context' => true], + 'source_account_nr_ends' => ['alias' => false, 'needs_context' => true], + 'from_account_nr_ends' => ['alias' => true, 'alias_for' => 'source_account_nr_ends', 'needs_context' => true], + 'source_account_nr_starts' => ['alias' => false, 'needs_context' => true], + 'from_account_nr_starts' => ['alias' => true, 'alias_for' => 'source_account_nr_starts', 'needs_context' => true], + 'destination_account_is' => ['alias' => false, 'needs_context' => true], + 'to_account_is' => ['alias' => true, 'alias_for' => 'destination_account_is', 'needs_context' => true], + 'destination_account_contains' => ['alias' => false, 'needs_context' => true], + 'destination' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true], + 'to' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true], + 'to_account_contains' => ['alias' => true, 'alias_for' => 'destination_account_contains', 'needs_context' => true], + 'destination_account_ends' => ['alias' => false, 'needs_context' => true], + 'to_account_ends' => ['alias' => true, 'alias_for' => 'destination_account_ends', 'needs_context' => true], + 'destination_account_starts' => ['alias' => false, 'needs_context' => true], + 'to_account_starts' => ['alias' => true, 'alias_for' => 'destination_account_starts', 'needs_context' => true], + 'destination_account_nr_is' => ['alias' => false, 'needs_context' => true], + 'to_account_nr_is' => ['alias' => true, 'alias_for' => 'destination_account_nr_is', 'needs_context' => true], + 'destination_account_nr_contains' => ['alias' => false, 'needs_context' => true], + 'to_account_nr_contains' => ['alias' => true, 'alias_for' => 'destination_account_nr_contains', 'needs_context' => true], + 'destination_account_nr_ends' => ['alias' => false, 'needs_context' => true], + 'to_account_nr_ends' => ['alias' => true, 'alias_for' => 'destination_account_nr_ends', 'needs_context' => true], + 'destination_account_nr_starts' => ['alias' => false, 'needs_context' => true], + 'to_account_nr_starts' => ['alias' => true, 'alias_for' => 'destination_account_nr_starts', 'needs_context' => true], + 'account_is' => ['alias' => false, 'needs_context' => true], + 'account_contains' => ['alias' => false, 'needs_context' => true], + 'account_ends' => ['alias' => false, 'needs_context' => true], + 'account_starts' => ['alias' => false, 'needs_context' => true], + 'account_nr_is' => ['alias' => false, 'needs_context' => true], + 'account_nr_contains' => ['alias' => false, 'needs_context' => true], + 'account_nr_ends' => ['alias' => false, 'needs_context' => true], + 'account_nr_starts' => ['alias' => false, 'needs_context' => true], + 'category_is' => ['alias' => false, 'needs_context' => true], + 'category_contains' => ['alias' => false, 'needs_context' => true], + 'category' => ['alias' => true, 'alias_for' => 'category_contains', 'needs_context' => true], + 'category_ends' => ['alias' => false, 'needs_context' => true], + 'category_starts' => ['alias' => false, 'needs_context' => true], + 'budget_is' => ['alias' => false, 'needs_context' => true], + 'budget_contains' => ['alias' => false, 'needs_context' => true], + 'budget' => ['alias' => true, 'alias_for' => 'budget_contains', 'needs_context' => true], + 'budget_ends' => ['alias' => false, 'needs_context' => true], + 'budget_starts' => ['alias' => false, 'needs_context' => true], + 'bill_is' => ['alias' => false, 'needs_context' => true], + 'bill_contains' => ['alias' => false, 'needs_context' => true], + 'bill' => ['alias' => true, 'alias_for' => 'bill_contains', 'needs_context' => true], + 'bill_ends' => ['alias' => false, 'needs_context' => true], + 'bill_starts' => ['alias' => false, 'needs_context' => true], + 'external_id_is' => ['alias' => false, 'needs_context' => true], + 'external_id_contains' => ['alias' => false, 'needs_context' => true], + 'external_id' => ['alias' => true, 'alias_for' => 'external_id_contains', 'needs_context' => true], + 'external_id_ends' => ['alias' => false, 'needs_context' => true], + 'external_id_starts' => ['alias' => false, 'needs_context' => true], + 'internal_reference_is' => ['alias' => false, 'needs_context' => true], + 'internal_reference_contains' => ['alias' => false, 'needs_context' => true], + 'internal_reference' => ['alias' => true, 'alias_for' => 'internal_reference_contains', 'needs_context' => true], + 'internal_reference_ends' => ['alias' => false, 'needs_context' => true], + 'internal_reference_starts' => ['alias' => false, 'needs_context' => true], + 'external_url_is' => ['alias' => false, 'needs_context' => true], + 'external_url_contains' => ['alias' => false, 'needs_context' => true], + 'external_url' => ['alias' => true, 'alias_for' => 'external_url_contains', 'needs_context' => true], + 'external_url_ends' => ['alias' => false, 'needs_context' => true], + 'external_url_starts' => ['alias' => false, 'needs_context' => true], + 'has_attachments' => ['alias' => false, 'needs_context' => false], + 'has_any_category' => ['alias' => false, 'needs_context' => false], + 'has_any_budget' => ['alias' => false, 'needs_context' => false], + 'has_any_bill' => ['alias' => false, 'needs_context' => false], + 'has_any_tag' => ['alias' => false, 'needs_context' => false], + 'any_notes' => ['alias' => false, 'needs_context' => false], + 'has_any_notes' => ['alias' => true, 'alias_for' => 'any_notes', 'needs_context' => false], + 'has_notes' => ['alias' => true, 'alias_for' => 'any_notes', 'needs_context' => false], + 'any_external_url' => ['alias' => false, 'needs_context' => false], + 'has_any_external_url' => ['alias' => true, 'alias_for' => 'any_external_url', 'needs_context' => false], + 'has_no_attachments' => ['alias' => false, 'needs_context' => false], + 'has_no_category' => ['alias' => false, 'needs_context' => false], + 'has_no_budget' => ['alias' => false, 'needs_context' => false], + 'has_no_bill' => ['alias' => false, 'needs_context' => false], + 'has_no_tag' => ['alias' => false, 'needs_context' => false], + 'no_notes' => ['alias' => false, 'needs_context' => false], + 'no_external_url' => ['alias' => false, 'needs_context' => false], + 'source_is_cash' => ['alias' => false, 'needs_context' => false], + 'destination_is_cash' => ['alias' => false, 'needs_context' => false], + 'account_is_cash' => ['alias' => false, 'needs_context' => false], + 'currency_is' => ['alias' => false, 'needs_context' => true], + 'foreign_currency_is' => ['alias' => false, 'needs_context' => true], + 'id' => ['alias' => false, 'trigger_class' => '', 'needs_context' => true], + 'journal_id' => ['alias' => false, 'trigger_class' => '', 'needs_context' => true], + 'recurrence_id' => ['alias' => false, 'trigger_class' => '', 'needs_context' => true], + 'date_on' => ['alias' => false, 'needs_context' => true], + 'date' => ['alias' => true, 'alias_for' => 'date_on', 'needs_context' => true], + 'date_is' => ['alias' => true, 'alias_for' => 'date_on', 'needs_context' => true], + 'on' => ['alias' => true, 'alias_for' => 'date_on', 'needs_context' => true], + 'date_before' => ['alias' => false, 'needs_context' => true], + 'before' => ['alias' => true, 'alias_for' => 'date_before', 'needs_context' => true], + 'date_after' => ['alias' => false, 'needs_context' => true], + 'after' => ['alias' => true, 'alias_for' => 'date_after', 'needs_context' => true], + 'interest_date_on' => ['alias' => false, 'needs_context' => true], + 'interest_date' => ['alias' => true, 'alias_for' => 'interest_date_on', 'needs_context' => true], + 'interest_date_is' => ['alias' => true, 'alias_for' => 'interest_date_on', 'needs_context' => true], + 'interest_date_before' => ['alias' => false, 'needs_context' => true], + 'interest_date_after' => ['alias' => false, 'needs_context' => true], + 'book_date_on' => ['alias' => false, 'needs_context' => true], + 'book_date' => ['alias' => true, 'alias_for' => 'book_date_on', 'needs_context' => true], + 'book_date_is' => ['alias' => true, 'alias_for' => 'book_date_on', 'needs_context' => true], + 'book_date_before' => ['alias' => false, 'needs_context' => true], + 'book_date_after' => ['alias' => false, 'needs_context' => true], + 'process_date_on' => ['alias' => false, 'needs_context' => true], + 'process_date' => ['alias' => true, 'alias_for' => 'process_date_on', 'needs_context' => true], + 'process_date_is' => ['alias' => true, 'alias_for' => 'process_date_on', 'needs_context' => true], + 'process_date_before' => ['alias' => false, 'needs_context' => true], + 'process_date_after' => ['alias' => false, 'needs_context' => true], + 'due_date_on' => ['alias' => false, 'needs_context' => true], + 'due_date' => ['alias' => true, 'alias_for' => 'due_date_on', 'needs_context' => true], + 'due_date_is' => ['alias' => true, 'alias_for' => 'due_date_on', 'needs_context' => true], + 'due_date_before' => ['alias' => false, 'needs_context' => true], + 'due_date_after' => ['alias' => false, 'needs_context' => true], + 'payment_date_on' => ['alias' => false, 'needs_context' => true], + 'payment_date' => ['alias' => true, 'alias_for' => 'payment_date_on', 'needs_context' => true], + 'payment_date_is' => ['alias' => true, 'alias_for' => 'payment_date_on', 'needs_context' => true], + 'payment_date_before' => ['alias' => false, 'needs_context' => true], + 'payment_date_after' => ['alias' => false, 'needs_context' => true], + 'invoice_date_on' => ['alias' => false, 'needs_context' => true], + 'invoice_date' => ['alias' => true, 'alias_for' => 'invoice_date_on', 'needs_context' => true], + 'invoice_date_is' => ['alias' => true, 'alias_for' => 'invoice_date_on', 'needs_context' => true], + 'invoice_date_before' => ['alias' => false, 'needs_context' => true], + 'invoice_date_after' => ['alias' => false, 'needs_context' => true], + 'created_at_on' => ['alias' => false, 'needs_context' => true], + 'created_at' => ['alias' => true, 'alias_for' => 'created_at_on', 'needs_context' => true], + 'created_at_is' => ['alias' => true, 'alias_for' => 'created_at_on', 'needs_context' => true], + 'created_at_before' => ['alias' => false, 'needs_context' => true], + 'created_at_after' => ['alias' => false, 'needs_context' => true], + 'updated_at_on' => ['alias' => false, 'needs_context' => true], + 'updated_at' => ['alias' => true, 'alias_for' => 'updated_at_on', 'needs_context' => true], + 'updated_at_is' => ['alias' => true, 'alias_for' => 'updated_at_on', 'needs_context' => true], + 'updated_at_before' => ['alias' => false, 'needs_context' => true], + 'updated_at_after' => ['alias' => false, 'needs_context' => true], + 'created_on_on' => ['alias' => true, 'alias_for' => 'created_at_on', 'needs_context' => true], + 'created_on' => ['alias' => true, 'alias_for' => 'created_at', 'needs_context' => true], + 'created_on_before' => ['alias' => true, 'alias_for' => 'created_at_before', 'needs_context' => true], + 'created_on_after' => ['alias' => true, 'alias_for' => 'created_at_after', 'needs_context' => true], + 'updated_on_on' => ['alias' => true, 'alias_for' => 'updated_at_on', 'needs_context' => true], + 'updated_on' => ['alias' => true, 'alias_for' => 'updated_at', 'needs_context' => true], + 'updated_on_before' => ['alias' => true, 'alias_for' => 'updated_at_before', 'needs_context' => true], + 'updated_on_after' => ['alias' => true, 'alias_for' => 'updated_at_after', 'needs_context' => true], + 'amount_is' => ['alias' => false, 'needs_context' => true], + 'amount' => ['alias' => true, 'alias_for' => 'amount_is', 'needs_context' => true], + 'amount_exactly' => ['alias' => true, 'alias_for' => 'amount_is', 'needs_context' => true], + 'amount_less' => ['alias' => false, 'needs_context' => true], + 'amount_max' => ['alias' => true, 'alias_for' => 'amount_less', 'needs_context' => true], + 'less' => ['alias' => true, 'alias_for' => 'amount_less', 'needs_context' => true], + 'amount_more' => ['alias' => false, 'needs_context' => true], + 'amount_min' => ['alias' => true, 'alias_for' => 'amount_more', 'needs_context' => true], + 'more' => ['alias' => true, 'alias_for' => 'amount_more', 'needs_context' => true], + 'foreign_amount_is' => ['alias' => false, 'needs_context' => true], + 'foreign_amount' => ['alias' => true, 'alias_for' => 'foreign_amount_is', 'needs_context' => true], + 'foreign_amount_less' => ['alias' => false, 'needs_context' => true], + 'foreign_amount_max' => ['alias' => true, 'alias_for' => 'foreign_amount_less', 'needs_context' => true], + 'foreign_amount_more' => ['alias' => false, 'needs_context' => true], + 'foreign_amount_min' => ['alias' => true, 'alias_for' => 'foreign_amount_more', 'needs_context' => true], + 'attachment_name_is' => ['alias' => false, 'needs_context' => true], + 'attachment' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true], + 'attachment_is' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true], + 'attachment_name' => ['alias' => true, 'alias_for' => 'attachment_name_is', 'needs_context' => true], + 'attachment_name_contains' => ['alias' => false, 'needs_context' => true], + 'attachment_name_starts' => ['alias' => false, 'needs_context' => true], + 'attachment_name_ends' => ['alias' => false, 'needs_context' => true], + 'attachment_notes' => ['alias' => true, 'alias_for' => 'attachment_notes_are', 'needs_context' => true], + 'attachment_notes_are' => ['alias' => false, 'needs_context' => true], + 'attachment_notes_contains' => ['alias' => false, 'needs_context' => true], + 'attachment_notes_contain' => ['alias' => true, 'alias_for' => 'attachment_notes_contains', 'needs_context' => true], + 'attachment_notes_starts' => ['alias' => false, 'needs_context' => true], + 'attachment_notes_start' => ['alias' => true, 'alias_for' => 'attachment_notes_starts', 'needs_context' => true], + 'attachment_notes_ends' => ['alias' => false, 'needs_context' => true], + 'attachment_notes_end' => ['alias' => true, 'alias_for' => 'attachment_notes_ends', 'needs_context' => true], + 'exists' => ['alias' => false, 'needs_context' => false], + 'sepa_ct_is' => ['alias' => false, 'needs_context' => true], + 'no_external_id' => ['alias' => false, 'needs_context' => false], + 'any_external_id' => ['alias' => false, 'needs_context' => false], + + // based on source or destination balance. Very heavy search. + 'source_balance_gte' => ['alias' => false, 'needs_context' => true], + 'source_balance_gt' => ['alias' => false, 'needs_context' => true], + 'source_balance_lte' => ['alias' => false, 'needs_context' => true], + 'source_balance_lt' => ['alias' => false, 'needs_context' => true], + 'source_balance_is' => ['alias' => false, 'needs_context' => true], + 'destination_balance_gte' => ['alias' => false, 'needs_context' => true], + 'destination_balance_gt' => ['alias' => false, 'needs_context' => true], + 'destination_balance_lte' => ['alias' => false, 'needs_context' => true], + 'destination_balance_lt' => ['alias' => false, 'needs_context' => true], + 'destination_balance_is' => ['alias' => false, 'needs_context' => true], ], ]; diff --git a/config/services.php b/config/services.php index 3757b429bf..77b81cef34 100644 --- a/config/services.php +++ b/config/services.php @@ -61,4 +61,8 @@ return [ 'mandrill' => [ 'secret' => env('MANDRILL_SECRET'), ], + 'pushover' => [ + 'token' => 'fake_token', + 'user_token' => 'fake_token', + ], ]; diff --git a/config/translations.php b/config/translations.php index 3017b488ae..8bbe20401d 100644 --- a/config/translations.php +++ b/config/translations.php @@ -270,12 +270,22 @@ return [ 'response', 'visit_webhook_url', 'reset_webhook_secret', + 'header_exchange_rates', + 'exchange_rates_intro', + 'exchange_rates_from_to', + 'exchange_rates_intro_rates', + 'header_exchange_rates_rates', + 'header_exchange_rates_table', + 'help_rate_form', + 'add_new_rate', + 'save_new_rate', ], 'form' => [ 'url', 'active', 'interest_date', 'title', + 'date', 'book_date', 'process_date', 'due_date', @@ -286,6 +296,9 @@ return [ 'webhook_response', 'webhook_trigger', 'webhook_delivery', + 'from_currency_to_currency', + 'to_currency_from_currency', + 'rate', ], 'list' => [ 'active', diff --git a/config/twigbridge.php b/config/twigbridge.php index da8d491d82..a3a6558817 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -183,6 +183,7 @@ return [ 'file', 'staticText', 'password', + 'passwordWithValue', 'nonSelectableAmount', 'number', 'amountNoCurrency', @@ -197,6 +198,7 @@ return [ 'assetAccountCheckList', 'assetAccountList', 'longAccountList', + 'assetLiabilityMultiAccountList', ], ], 'CurrencyForm' => [ diff --git a/database/migrations/2021_08_28_073733_user_groups.php b/database/migrations/2021_08_28_073733_user_groups.php index 21a25beed4..1111ddc671 100644 --- a/database/migrations/2021_08_28_073733_user_groups.php +++ b/database/migrations/2021_08_28_073733_user_groups.php @@ -42,6 +42,7 @@ class UserGroups extends Migration 'categories', 'recurrences', 'object_groups', + 'preferences', 'rule_groups', 'rules', 'tags', diff --git a/database/migrations/2024_11_30_075826_multi_piggy.php b/database/migrations/2024_11_30_075826_multi_piggy.php new file mode 100644 index 0000000000..86af179a03 --- /dev/null +++ b/database/migrations/2024_11_30_075826_multi_piggy.php @@ -0,0 +1,109 @@ +dropForeign('piggy_banks_account_id_foreign'); + }); + } catch (RuntimeException $e) { + Log::error('Could not drop foreign key "piggy_banks_account_id_foreign". Probably not an issue.'); + } + Schema::table('piggy_banks', static function (Blueprint $table): void { + // 2. make column nullable. + $table->unsignedInteger('account_id')->nullable()->change(); + }); + Schema::table('piggy_banks', static function (Blueprint $table): void { + // 3. add currency + $table->integer('transaction_currency_id', false, true)->after('account_id')->nullable(); + $table->foreign('transaction_currency_id', 'unique_currency')->references('id')->on('transaction_currencies')->onDelete('cascade'); + }); + Schema::table('piggy_banks', static function (Blueprint $table): void { + // 4. rename columns + $table->renameColumn('targetamount', 'target_amount'); + $table->renameColumn('startdate', 'start_date'); + $table->renameColumn('targetdate', 'target_date'); + $table->renameColumn('startdate_tz', 'start_date_tz'); + $table->renameColumn('targetdate_tz', 'target_date_tz'); + }); + Schema::table('piggy_banks', static function (Blueprint $table): void { + // 5. add new index + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('set null'); + }); + + // rename some fields in piggy bank reps. + Schema::table('piggy_bank_repetitions', static function (Blueprint $table): void { + // 6. rename columns + $table->renameColumn('currentamount', 'current_amount'); + $table->renameColumn('startdate', 'start_date'); + $table->renameColumn('targetdate', 'target_date'); + $table->renameColumn('startdate_tz', 'start_date_tz'); + $table->renameColumn('targetdate_tz', 'target_date_tz'); + }); + + // create table account_piggy_bank + Schema::create('account_piggy_bank', static function (Blueprint $table): void { + $table->id(); + $table->integer('account_id', false, true); + $table->integer('piggy_bank_id', false, true); + $table->decimal('current_amount', 32, 12)->default('0'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('piggy_bank_id')->references('id')->on('piggy_banks')->onDelete('cascade'); + $table->unique(['account_id', 'piggy_bank_id'], 'unique_piggy_save'); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('piggy_banks', static function (Blueprint $table): void { + // 1. drop account index again. + $table->dropForeign('piggy_banks_account_id_foreign'); + + // rename columns again. + $table->renameColumn('target_amount', 'targetamount'); + $table->renameColumn('start_date', 'startdate'); + $table->renameColumn('target_date', 'targetdate'); + $table->renameColumn('start_date_tz', 'startdate_tz'); + $table->renameColumn('target_date_tz', 'targetdate_tz'); + + // 3. drop currency again + index + $table->dropForeign('unique_currency'); + $table->dropColumn('transaction_currency_id'); + + // 2. make column non-nullable. + $table->unsignedInteger('account_id')->change(); + + // 5. add new index + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + // rename some fields in piggy bank reps. + Schema::table('piggy_bank_repetitions', static function (Blueprint $table): void { + // 6. rename columns + $table->renameColumn('current_amount', 'currentamount'); + $table->renameColumn('start_date', 'startdate'); + $table->renameColumn('target_date', 'targetdate'); + $table->renameColumn('start_date_tz', 'startdate_tz'); + $table->renameColumn('target_date_tz', 'targetdate_tz'); + }); + + Schema::dropIfExists('account_piggy_bank'); + } +}; diff --git a/database/migrations/2024_12_19_061003_add_native_amount_column.php b/database/migrations/2024_12_19_061003_add_native_amount_column.php new file mode 100644 index 0000000000..74a738793b --- /dev/null +++ b/database/migrations/2024_12_19_061003_add_native_amount_column.php @@ -0,0 +1,56 @@ + ['native_virtual_balance'], // works. + 'account_piggy_bank' => ['native_current_amount'], // works + 'auto_budgets' => ['native_amount'], // works + 'available_budgets' => ['native_amount'], // works + 'bills' => ['native_amount_min', 'native_amount_max'], // works + 'budget_limits' => ['native_amount'], // works + 'piggy_bank_events' => ['native_amount'], // works + 'piggy_banks' => ['native_target_amount'], // works + 'transactions' => ['native_amount', 'native_foreign_amount'], // works + + // TODO button to recalculate all native amounts on selected pages? + + ]; + + /** + * Run the migrations. + */ + public function up(): void + { + foreach ($this->tables as $table => $fields) { + foreach ($fields as $field) { + Schema::table($table, static function (Blueprint $table) use ($field): void { + // add amount column + $table->decimal($field, 32, 12)->nullable(); + }); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + foreach ($this->tables as $table => $fields) { + foreach ($fields as $field) { + Schema::table($table, static function (Blueprint $table) use ($field): void { + // add amount column + $table->dropColumn($field); + }); + } + } + } +}; diff --git a/package-lock.json b/package-lock.json index 7e2d14368b..bc0e566e97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,9 +44,9 @@ } }, "node_modules/@ag-grid-community/styles": { - "version": "33.0.2", - "resolved": "https://registry.npmjs.org/@ag-grid-community/styles/-/styles-33.0.2.tgz", - "integrity": "sha512-oMUdolN8l34k6ubLQm3AiO53SqB59YbPDSanVDrwiN7xIq5NGczVQ2Yks032XWqa0bMYkLU9L9opCBSCwZdKsg==", + "version": "33.0.3", + "resolved": "https://registry.npmjs.org/@ag-grid-community/styles/-/styles-33.0.3.tgz", + "integrity": "sha512-1bXrYoVj5TIpLvyjgXHvKnYrC0/JvrdGDdJ3mYfT58GulDt1iNQqhjxMnZB+sxMdQoOc681jmKTGcWzOEDE1Dw==", "license": "MIT" }, "node_modules/@ampproject/remapping": { @@ -1709,9 +1709,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], @@ -1726,9 +1726,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "cpu": [ "arm" ], @@ -1743,9 +1743,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "cpu": [ "arm64" ], @@ -1760,9 +1760,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "cpu": [ "x64" ], @@ -1777,9 +1777,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "cpu": [ "arm64" ], @@ -1794,9 +1794,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "cpu": [ "x64" ], @@ -1811,9 +1811,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "cpu": [ "arm64" ], @@ -1828,9 +1828,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "cpu": [ "x64" ], @@ -1845,9 +1845,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "cpu": [ "arm" ], @@ -1862,9 +1862,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "cpu": [ "arm64" ], @@ -1879,9 +1879,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "cpu": [ "ia32" ], @@ -1896,9 +1896,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "cpu": [ "loong64" ], @@ -1913,9 +1913,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "cpu": [ "mips64el" ], @@ -1930,9 +1930,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "cpu": [ "ppc64" ], @@ -1947,9 +1947,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], @@ -1964,9 +1964,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], @@ -1981,9 +1981,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], @@ -1997,10 +1997,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "cpu": [ "x64" ], @@ -2015,9 +2032,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", "cpu": [ "arm64" ], @@ -2032,9 +2049,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], @@ -2049,9 +2066,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", "cpu": [ "x64" ], @@ -2066,9 +2083,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], @@ -2083,9 +2100,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], @@ -2100,9 +2117,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], @@ -2574,9 +2591,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", - "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", + "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", "cpu": [ "arm" ], @@ -2588,9 +2605,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", - "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", + "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", "cpu": [ "arm64" ], @@ -2602,9 +2619,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", - "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", + "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", "cpu": [ "arm64" ], @@ -2616,9 +2633,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", - "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", + "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", "cpu": [ "x64" ], @@ -2630,9 +2647,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", - "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", + "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", "cpu": [ "arm64" ], @@ -2644,9 +2661,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", - "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", + "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", "cpu": [ "x64" ], @@ -2658,9 +2675,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", - "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", + "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", "cpu": [ "arm" ], @@ -2672,9 +2689,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", - "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", + "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", "cpu": [ "arm" ], @@ -2686,9 +2703,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", - "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", + "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", "cpu": [ "arm64" ], @@ -2700,9 +2717,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", - "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", + "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", "cpu": [ "arm64" ], @@ -2714,9 +2731,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", - "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", + "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", "cpu": [ "loong64" ], @@ -2728,9 +2745,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", - "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", + "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", "cpu": [ "ppc64" ], @@ -2742,9 +2759,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", - "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", + "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", "cpu": [ "riscv64" ], @@ -2756,9 +2773,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", - "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", + "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", "cpu": [ "s390x" ], @@ -2770,9 +2787,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", - "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", + "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", "cpu": [ "x64" ], @@ -2784,9 +2801,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", - "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", + "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", "cpu": [ "x64" ], @@ -2798,9 +2815,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", - "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", + "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", "cpu": [ "arm64" ], @@ -2812,9 +2829,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", - "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", + "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", "cpu": [ "ia32" ], @@ -2826,9 +2843,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", - "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", + "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", "cpu": [ "x64" ], @@ -3621,9 +3638,9 @@ } }, "node_modules/admin-lte": { - "version": "4.0.0-beta2", - "resolved": "https://registry.npmjs.org/admin-lte/-/admin-lte-4.0.0-beta2.tgz", - "integrity": "sha512-Ofav0BKnCnz+IeeXrHQZ6JWnHouwv+fDYyfagRpjfFaMBmYCljA2Qo1+fCGkJuJn/SfNPhFpJhbUt+l2tH0LwA==", + "version": "4.0.0-beta3", + "resolved": "https://registry.npmjs.org/admin-lte/-/admin-lte-4.0.0-beta3.tgz", + "integrity": "sha512-q2VoAOu1DtZ7z41M2gQ05VMNYkFCAMxFU+j/HUMwCOlr/e3VhO+qww2SGJw4OxBw5nZQ7YV78+wK2RiB7ConzQ==", "license": "MIT" }, "node_modules/ag-charts-types": { @@ -3702,9 +3719,9 @@ } }, "node_modules/alpinejs": { - "version": "3.14.7", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.7.tgz", - "integrity": "sha512-ScnbydNBcWVnCiVupD3wWUvoMPm8244xkvDNMxVCspgmap9m4QuJ7pjc+77UtByU+1+Ejg0wzYkP4mQaOMcvng==", + "version": "3.14.8", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.8.tgz", + "integrity": "sha512-wT2fuP2DXpGk/jKaglwy7S/IJpm1FD+b7U6zUrhwErjoq5h27S4dxkJEXVvhbdwyPv9U+3OkUuNLkZT4h2Kfrg==", "license": "MIT", "dependencies": { "@vue/reactivity": "~3.1.1" @@ -4431,9 +4448,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001689", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", - "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -5646,9 +5663,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", - "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", + "version": "1.5.76", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", + "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==", "dev": true, "license": "ISC" }, @@ -5703,9 +5720,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5773,9 +5790,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -5793,9 +5810,9 @@ } }, "node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5806,30 +5823,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, "node_modules/escalade": { @@ -6104,9 +6122,9 @@ } }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "dev": true, "license": "ISC", "dependencies": { @@ -6932,9 +6950,9 @@ } }, "node_modules/i18next": { - "version": "24.1.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.2.tgz", - "integrity": "sha512-th/075GW0Ub1gYDMHLiZXMGSfGv1aP1VqjT3fma/12hNHCNlH8oJMftvlDzycT/R+KoULWk+xLU8H1JRwV85qw==", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", + "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", "funding": [ { "type": "individual", @@ -7219,9 +7237,9 @@ "license": "MIT" }, "node_modules/is-core-module": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", - "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -7442,9 +7460,9 @@ "license": "MIT" }, "node_modules/json-stable-stringify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.0.tgz", - "integrity": "sha512-ex8jk9BZHBolvbd5cRnAgwyaYcYB0qZldy1e+LCOdcF6+AUmVZ6LcGUMzsRTW83QMeu+GxZGrcLqxqrgfXGvIw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz", + "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==", "dev": true, "license": "MIT", "dependencies": { @@ -7845,9 +7863,9 @@ } }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -8368,15 +8386,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -9843,9 +9863,9 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { @@ -9856,6 +9876,9 @@ "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9940,9 +9963,9 @@ } }, "node_modules/rollup": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", - "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", + "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", "dev": true, "license": "MIT", "dependencies": { @@ -9956,25 +9979,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.1", - "@rollup/rollup-android-arm64": "4.28.1", - "@rollup/rollup-darwin-arm64": "4.28.1", - "@rollup/rollup-darwin-x64": "4.28.1", - "@rollup/rollup-freebsd-arm64": "4.28.1", - "@rollup/rollup-freebsd-x64": "4.28.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", - "@rollup/rollup-linux-arm-musleabihf": "4.28.1", - "@rollup/rollup-linux-arm64-gnu": "4.28.1", - "@rollup/rollup-linux-arm64-musl": "4.28.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", - "@rollup/rollup-linux-riscv64-gnu": "4.28.1", - "@rollup/rollup-linux-s390x-gnu": "4.28.1", - "@rollup/rollup-linux-x64-gnu": "4.28.1", - "@rollup/rollup-linux-x64-musl": "4.28.1", - "@rollup/rollup-win32-arm64-msvc": "4.28.1", - "@rollup/rollup-win32-ia32-msvc": "4.28.1", - "@rollup/rollup-win32-x64-msvc": "4.28.1", + "@rollup/rollup-android-arm-eabi": "4.29.1", + "@rollup/rollup-android-arm64": "4.29.1", + "@rollup/rollup-darwin-arm64": "4.29.1", + "@rollup/rollup-darwin-x64": "4.29.1", + "@rollup/rollup-freebsd-arm64": "4.29.1", + "@rollup/rollup-freebsd-x64": "4.29.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", + "@rollup/rollup-linux-arm-musleabihf": "4.29.1", + "@rollup/rollup-linux-arm64-gnu": "4.29.1", + "@rollup/rollup-linux-arm64-musl": "4.29.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", + "@rollup/rollup-linux-riscv64-gnu": "4.29.1", + "@rollup/rollup-linux-s390x-gnu": "4.29.1", + "@rollup/rollup-linux-x64-gnu": "4.29.1", + "@rollup/rollup-linux-x64-musl": "4.29.1", + "@rollup/rollup-win32-arm64-msvc": "4.29.1", + "@rollup/rollup-win32-ia32-msvc": "4.29.1", + "@rollup/rollup-win32-x64-msvc": "4.29.1", "fsevents": "~2.3.2" } }, @@ -10051,9 +10074,9 @@ } }, "node_modules/sass/node_modules/chokidar": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.2.tgz", - "integrity": "sha512-/b57FK+bblSU+dfewfFe0rT1YjVDfOmeLQwCAuC+vwvgLkXboATqqmy+Ipux6JrF6L5joe5CBnFOw+gLWH6yKg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { @@ -11250,13 +11273,13 @@ } }, "node_modules/vite": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", - "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.6.tgz", + "integrity": "sha512-NSjmUuckPmDU18bHz7QZ+bTYhRR0iA72cs2QAxCqDpafJ0S6qetco0LB3WW2OxlMHS0JmAv+yZ/R3uPmMyGTjQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.0", + "esbuild": "^0.24.2", "postcss": "^8.4.49", "rollup": "^4.23.0" }, @@ -12172,7 +12195,7 @@ "chartjs-adapter-date-fns": "^3.0.0", "chartjs-chart-sankey": "^0.14.0", "date-fns": "^4.0.0", - "i18next": "^24.0.0", + "i18next": "^24.2.0", "i18next-chained-backend": "^4.6.2", "i18next-http-backend": "^3.0.1", "i18next-localstorage-backend": "^4.2.0", diff --git a/public/v1/js/ff/budgets/index.js b/public/v1/js/ff/budgets/index.js index b6d702fa95..63444d096b 100644 --- a/public/v1/js/ff/budgets/index.js +++ b/public/v1/js/ff/budgets/index.js @@ -35,6 +35,8 @@ $(function () { $('.budget_amount').on('change', updateBudgetedAmount); $('.create_bl').on('click', createBudgetLimit); + $('.edit_bl').on('click', editBudgetLimit); + $('.show_bl').on('click', showBudgetLimit); $('.delete_bl').on('click', deleteBudgetLimit); @@ -216,6 +218,24 @@ function createBudgetLimit(e) { return false; } +function editBudgetLimit(e) { + var button = $(e.currentTarget); + var budgetLimitId = button.data('id'); + $('#defaultModal').empty().load(editBudgetLimitUrl.replace('REPLACEME', budgetLimitId.toString()), function () { + $('#defaultModal').modal('show'); + }); + return false; +} + +function showBudgetLimit(e) { + var button = $(e.currentTarget); + var budgetLimitId = button.data('id'); + $('#defaultModal').empty().load(showBudgetLimitUrl.replace('REPLACEME', budgetLimitId.toString()), function () { + $('#defaultModal').modal('show'); + }); + return false; +} + function deleteBudgetLimit(e) { e.preventDefault(); var button = $(e.currentTarget); diff --git a/public/v1/js/ff/currencies/index.js b/public/v1/js/ff/currencies/index.js index 99ecaf7e24..3a7acbe465 100644 --- a/public/v1/js/ff/currencies/index.js +++ b/public/v1/js/ff/currencies/index.js @@ -29,7 +29,15 @@ $(function () { }); function setDefaultCurrency(e) { + console.log('Setting default currency'); var button = $(e.currentTarget); + // disable everything. + button.prop('disabled', true); + $('a').css('pointer-events', 'none'); + + // change cursor to hourglass + $('body').css('cursor', 'wait'); + var currencyCode = button.data('code'); var params = { @@ -38,6 +46,7 @@ function setDefaultCurrency(e) { } $.ajax({ + timeout: 30000, // sets timeout to 30 seconds url: updateCurrencyUrl + '/' + currencyCode, data: JSON.stringify(params), type: 'PUT', diff --git a/public/v1/js/ff/index.js b/public/v1/js/ff/index.js index 5f963c126a..f053545079 100644 --- a/public/v1/js/ff/index.js +++ b/public/v1/js/ff/index.js @@ -26,6 +26,7 @@ $(function () { }); + function drawChart() { "use strict"; lineChart(accountFrontpageUrl, 'accounts-chart'); @@ -37,13 +38,77 @@ function drawChart() { columnChart('chart/category/frontpage', 'categories-chart'); columnChart(accountExpenseUrl, 'expense-accounts-chart'); columnChart(accountRevenueUrl, 'revenue-accounts-chart'); - - // get balance box: - getBalanceBox(); - getBillsBox(); - getAvailableBox(); - getNetWorthBox(); getPiggyBanks(); + getAllBoxes(); + + function getAllBoxes() { + // get summary. + $.getJSON('api/v1/summary/basic?start=' + sessionStart + '&end=' + sessionEnd).done(function (data) { + var key; + + // balance. + var balance_top = []; + var balance_bottom = []; + + // bills + var unpaid = []; + var paid = []; + + // left to spend. + var left_to_spend_top = []; + var left_to_spend_bottom = []; + + // net worth + var net_worth = []; + + + for (key in data) { + // balance + if (key.substring(0, 11) === 'balance-in-') { + balance_top.push(data[key].value_parsed); + balance_bottom.push(data[key].sub_title); + } + + // bills + if (key.substring(0, 16) === 'bills-unpaid-in-') { + unpaid.push(data[key].value_parsed); + } + if (key.substring(0, 14) === 'bills-paid-in-') { + paid.push(data[key].value_parsed); + } + + // left to spend + if (key.substring(0, 17) === 'left-to-spend-in-') { + left_to_spend_top.push(data[key].value_parsed); + left_to_spend_bottom.push(data[key].sub_title); + if(parseFloat(data[key].monetary_value) < 0) { + $('#box-left-to-spend-box').removeClass('bg-green-gradient').addClass('bg-red-gradient'); + } + } + + // net worth + if (key.substring(0, 13) === 'net-worth-in-') { + net_worth.push(data[key].value_parsed); + } + } + + // balance + $('#box-balance-sums').html(balance_top.join(', ')); + $('#box-balance-list').html(balance_bottom.join(', ')); + + // bills + $('#box-bills-unpaid').html(unpaid.join(', ')); + $('#box-bills-paid').html(paid.join(', ')); + + // left to spend + $('#box-left-to-spend').html(left_to_spend_top.join(', ')); + $('#box-left-per-day').html(left_to_spend_bottom.join(', ')); + + // net worth + $('#box-net-worth').html(net_worth.join(', ')); + + }); + } //getBoxAmounts(); } @@ -58,121 +123,3 @@ function getPiggyBanks() { } }); } - -function getNetWorthBox() { - // box-net-worth - $.getJSON('json/box/net-worth').done(function (data) { - $('#box-net-worth').html(data.net_worths.join(', ')); - }); -} - -/** - * - */ -function getAvailableBox() { - // box-left-to-spend - // box-left-per-day - // * 0) If the user has available amount this period and has overspent: overspent box. - // * 1) If the user has available amount this period and has NOT overspent: left to spend box. - // * 2) if the user has no available amount set this period: spent per day - $.getJSON('json/box/available').done(function (data) { - $('#box-left-to-spend-text').text(data.title); - if (0 === data.display) { - $('#box-left-to-spend-box').removeClass('bg-green-gradient').addClass('bg-red-gradient'); - $('#box-left-to-spend').html(data.left_to_spend); - $('#box-left-per-day').html(data.left_per_day); - } - if (1 === data.display) { - $('#box-left-to-spend').html(data.left_to_spend); - $('#box-left-per-day').html(data.left_per_day); - } - if (2 === data.display) { - $('#box-left-to-spend').html(data.spent_total); - $('#box-left-per-day').html(data.spent_per_day); - } - }); -} - -/** - * - */ -function getBillsBox() { - // box-bills-unpaid - // box-bills-paid - - // get summary. - - $.getJSON('api/v1/summary/basic?start=' + sessionStart + '&end=' + sessionEnd).done(function (data) { - var key; - var unpaid = []; - var paid = []; - for (key in data) { - //console.log(key); - if (key.substr(0, 16) === 'bills-unpaid-in-') { - // only when less than 3. - if (unpaid.length < 3) { - unpaid.push(data[key].value_parsed); - } - } - if (key.substr(0, 14) === 'bills-paid-in-') { - // only when less than 5. - if (paid.length < 3) { - paid.push(data[key].value_parsed); - } - } - } - $('#box-bills-unpaid').html(unpaid.join(', ')); - $('#box-bills-paid').html(paid.join(', ')); - }); -} - -/** - * - */ -function getBalanceBox() { - // box-balance-sums - // box-balance-list - $.getJSON('json/box/balance').done(function (data) { - if (data.size === 1) { - // show balance in "sums", show single entry in list. - for (var x in data.sums) { - $('#box-balance-sums').html(data.sums[x]); - $('#box-balance-list').html(data.incomes[x] + ' + ' + data.expenses[x]); - } - return; - } - // do not use "sums", only use list. - $('#box-balance-progress').remove(); - var expense, string, sum, income, current; - - // first loop, echo only "preferred". - for (x in data.sums) { - current = $('#box-balance-list').html(); - sum = data.sums[x]; - expense = data.expenses[x]; - income = data.incomes[x]; - string = income + ' + ' + expense + ': ' + sum; - if (data.preferred == x) { - $('#box-balance-list').html(current + '' + string + '' + '
'); - } - } - // then list the others (only 1 space) - - var count = 0; - for (x in data.sums) { - if (count > 2) { - return; - } - current = $('#box-balance-list').html(); - sum = data.sums[x]; - expense = data.expenses[x]; - income = data.incomes[x]; - string = income + ' + ' + expense + ': ' + sum; - if (data.preferred != x) { - $('#box-balance-list').html(current + '' + string + '' + '
'); - } - count++; - - } - }); -} \ No newline at end of file diff --git a/public/v1/js/ff/preferences/index.js b/public/v1/js/ff/preferences/index.js index ad3a19aa9f..5709280536 100644 --- a/public/v1/js/ff/preferences/index.js +++ b/public/v1/js/ff/preferences/index.js @@ -23,10 +23,19 @@ $(document).ready(function () { "use strict"; if (!Modernizr.inputtypes.date) { - $('input[type="date"]').datepicker( - { - dateFormat: 'yy-mm-dd' - } - ); + $('input[type="date"]').datepicker({ + dateFormat: 'yy-mm-dd' + }); } + $('.submit-test').click(submitTest); }); + +function submitTest(e) { + var current = $(e.currentTarget); + var channel = current.data('channel'); + + $.post(postUrl, {channel: channel}, function () { + window.location.reload(true); + }); + return false; +} diff --git a/resources/assets/v1/mix-manifest.json b/resources/assets/v1/mix-manifest.json index db8d5e4037..528ebcb2cf 100644 --- a/resources/assets/v1/mix-manifest.json +++ b/resources/assets/v1/mix-manifest.json @@ -8,6 +8,8 @@ "/build/webhooks/create.js": "/build/webhooks/create.js", "/build/webhooks/edit.js": "/build/webhooks/edit.js", "/build/webhooks/show.js": "/build/webhooks/show.js", + "/build/exchange-rates/index.js": "/build/exchange-rates/index.js", + "/build/exchange-rates/rates.js": "/build/exchange-rates/rates.js", "/public/v1/js/app.js": "/public/v1/js/app.js", "/public/v1/js/app.js.LICENSE.txt": "/public/v1/js/app.js.LICENSE.txt", "/public/v1/js/app_vue.js": "/public/v1/js/app_vue.js", @@ -16,6 +18,10 @@ "/public/v1/js/create_transaction.js.LICENSE.txt": "/public/v1/js/create_transaction.js.LICENSE.txt", "/public/v1/js/edit_transaction.js": "/public/v1/js/edit_transaction.js", "/public/v1/js/edit_transaction.js.LICENSE.txt": "/public/v1/js/edit_transaction.js.LICENSE.txt", + "/public/v1/js/exchange-rates/index.js": "/public/v1/js/exchange-rates/index.js", + "/public/v1/js/exchange-rates/index.js.LICENSE.txt": "/public/v1/js/exchange-rates/index.js.LICENSE.txt", + "/public/v1/js/exchange-rates/rates.js": "/public/v1/js/exchange-rates/rates.js", + "/public/v1/js/exchange-rates/rates.js.LICENSE.txt": "/public/v1/js/exchange-rates/rates.js.LICENSE.txt", "/public/v1/js/ff/accounts/create.js": "/public/v1/js/ff/accounts/create.js", "/public/v1/js/ff/accounts/edit-reconciliation.js": "/public/v1/js/ff/accounts/edit-reconciliation.js", "/public/v1/js/ff/accounts/edit.js": "/public/v1/js/ff/accounts/edit.js", diff --git a/resources/assets/v1/src/components/exchange-rates/Index.vue b/resources/assets/v1/src/components/exchange-rates/Index.vue new file mode 100644 index 0000000000..b06dd5f683 --- /dev/null +++ b/resources/assets/v1/src/components/exchange-rates/Index.vue @@ -0,0 +1,95 @@ + + + + + + + diff --git a/resources/assets/v1/src/components/exchange-rates/Rates.vue b/resources/assets/v1/src/components/exchange-rates/Rates.vue new file mode 100644 index 0000000000..04678f0eb4 --- /dev/null +++ b/resources/assets/v1/src/components/exchange-rates/Rates.vue @@ -0,0 +1,350 @@ + + + + + diff --git a/resources/assets/v1/src/create_transaction.js b/resources/assets/v1/src/create_transaction.js index 1dda1b3fd2..a5bb19dd7c 100644 --- a/resources/assets/v1/src/create_transaction.js +++ b/resources/assets/v1/src/create_transaction.js @@ -23,7 +23,7 @@ import CreateTransaction from './components/transactions/CreateTransaction'; import CustomDate from "./components/transactions/CustomDate"; import CustomString from "./components/transactions/CustomString"; import CustomTextarea from "./components/transactions/CustomTextarea"; -import StandardDate from "./components/transactions/StandardDate"; +import StandardDate from "./components/transactions/StandardDate"; import GroupDescription from "./components/transactions/GroupDescription"; import TransactionDescription from "./components/transactions/TransactionDescription"; import CustomTransactionFields from "./components/transactions/CustomTransactionFields"; diff --git a/resources/assets/v1/src/exchange-rates/index.js b/resources/assets/v1/src/exchange-rates/index.js new file mode 100644 index 0000000000..1545bf2f44 --- /dev/null +++ b/resources/assets/v1/src/exchange-rates/index.js @@ -0,0 +1,39 @@ +/* + * edit_transactions.js + * Copyright (c) 2019 james@firefly-iii.org + * + * This file is part of Firefly III (https://github.com/firefly-iii). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Index from "../components/exchange-rates/Index"; + +/** + * First we will load Axios via bootstrap.js + * jquery and bootstrap-sass preloaded in app.js + * vue, uiv and vuei18n are in app_vue.js + */ + +require('../bootstrap'); +const i18n = require('../i18n'); + +let props = {}; +const app = new Vue({ + i18n, + el: "#exchange_rates_index", + render: (createElement) => { + return createElement(Index, {props: props}) + }, +}); diff --git a/resources/assets/v1/src/exchange-rates/rates.js b/resources/assets/v1/src/exchange-rates/rates.js new file mode 100644 index 0000000000..ce02b5fe94 --- /dev/null +++ b/resources/assets/v1/src/exchange-rates/rates.js @@ -0,0 +1,39 @@ +/* + * edit_transactions.js + * Copyright (c) 2019 james@firefly-iii.org + * + * This file is part of Firefly III (https://github.com/firefly-iii). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Rates from "../components/exchange-rates/Rates"; + +/** + * First we will load Axios via bootstrap.js + * jquery and bootstrap-sass preloaded in app.js + * vue, uiv and vuei18n are in app_vue.js + */ + +require('../bootstrap'); +const i18n = require('../i18n'); + +let props = {}; +const app = new Vue({ + i18n, + el: "#exchange_rates_rates", + render: (createElement) => { + return createElement(Rates, {props: props}) + }, +}); diff --git a/resources/assets/v1/src/locales/.json b/resources/assets/v1/src/locales/.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/resources/assets/v1/src/locales/.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/resources/assets/v1/src/locales/bg.json b/resources/assets/v1/src/locales/bg.json index 819884d2d8..ae549a681c 100644 --- a/resources/assets/v1/src/locales/bg.json +++ b/resources/assets/v1/src/locales/bg.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "\u0418\u0437\u0433\u043b\u0435\u0436\u0434\u0430 \u0432\u0441\u0435 \u043e\u0449\u0435 \u043d\u044f\u043c\u0430\u0442\u0435 \u0431\u044e\u0434\u0436\u0435\u0442\u0438. \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u0442\u0435 \u043d\u044f\u043a\u043e\u0438 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0411\u044e\u0434\u0436\u0435\u0442\u0438 <\/a>. \u0411\u044e\u0434\u0436\u0435\u0442\u0438\u0442\u0435 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0432\u0438 \u043f\u043e\u043c\u043e\u0433\u043d\u0430\u0442 \u0434\u0430 \u0441\u043b\u0435\u0434\u0438\u0442\u0435 \u0440\u0430\u0437\u0445\u043e\u0434\u0438\u0442\u0435 \u0441\u0438.", - "no_bill_pointer": "\u0418\u0437\u0433\u043b\u0435\u0436\u0434\u0430 \u0432\u0441\u0435 \u043e\u0449\u0435 \u043d\u044f\u043c\u0430\u0442\u0435 \u0441\u043c\u0435\u0442\u043a\u0438. \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u0442\u0435 \u043d\u044f\u043a\u043e\u0438 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0421\u043c\u0435\u0442\u043a\u0438 <\/a>. \u0421\u043c\u0435\u0442\u043a\u0438\u0442\u0435 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0432\u0438 \u043f\u043e\u043c\u043e\u0433\u043d\u0430\u0442 \u0434\u0430 \u0441\u043b\u0435\u0434\u0438\u0442\u0435 \u0440\u0430\u0437\u0445\u043e\u0434\u0438\u0442\u0435 \u0441\u0438.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\u0420\u0430\u0437\u0445\u043e\u0434\u043d\u0430 \u0441\u043c\u0435\u0442\u043a\u0430", "hidden_fields_preferences": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u043f\u043e\u0432\u0435\u0447\u0435 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0432\u044a\u0432 \u0432\u0430\u0448\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438<\/a>.", "destination_account": "\u041f\u0440\u0438\u0445\u043e\u0434\u043d\u0430 \u0441\u043c\u0435\u0442\u043a\u0430", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "\u0415\u0442\u0438\u043a\u0435\u0442\u0438", "no_budget": "(\u0431\u0435\u0437 \u0431\u044e\u0434\u0436\u0435\u0442)", - "no_bill": "(\u043d\u044f\u043c\u0430 \u0441\u043c\u0435\u0442\u043a\u0430)", + "no_bill": "(no subscription)", "category": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f", "attachments": "\u041f\u0440\u0438\u043a\u0430\u0447\u0435\u043d\u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u0435", "notes": "\u0411\u0435\u043b\u0435\u0436\u043a\u0438", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u0442\u0435 \u043f\u0440\u0438\u0445\u043e\u0434\u043d\u0430\u0442\u0430 \u0441\u043c\u0435\u0442\u043a\u0430 \u043d\u0430 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u0437\u0430 \u0441\u044a\u0433\u043b\u0430\u0441\u0443\u0432\u0430\u043d\u0435.", "source_account_reconciliation": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u0442\u0435 \u0440\u0430\u0437\u0445\u043e\u0434\u043d\u0430\u0442\u0430 \u0441\u043c\u0435\u0442\u043a\u0430 \u043d\u0430 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u0437\u0430 \u0441\u044a\u0433\u043b\u0430\u0441\u0443\u0432\u0430\u043d\u0435.", "budget": "\u0411\u044e\u0434\u0436\u0435\u0442", - "bill": "\u0421\u043c\u0435\u0442\u043a\u0430", + "bill": "Subscription", "you_create_withdrawal": "\u0421\u044a\u0437\u0434\u0430\u0432\u0430\u0442\u0435 \u0442\u0435\u0433\u043b\u0435\u043d\u0435.", "you_create_transfer": "\u0421\u044a\u0437\u0434\u0430\u0432\u0430\u0442\u0435 \u043f\u0440\u0435\u0445\u0432\u044a\u0440\u043b\u044f\u043d\u0435.", "you_create_deposit": "\u0421\u044a\u0437\u0434\u0430\u0432\u0430\u0442\u0435 \u0434\u0435\u043f\u043e\u0437\u0438\u0442.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Response", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "\u0410\u043a\u0442\u0438\u0432\u0435\u043d", "interest_date": "\u041f\u0430\u0434\u0435\u0436 \u043d\u0430 \u043b\u0438\u0445\u0432\u0430", "title": "\u0417\u0430\u0433\u043b\u0430\u0432\u0438\u0435", + "date": "\u0414\u0430\u0442\u0430", "book_date": "\u0414\u0430\u0442\u0430 \u043d\u0430 \u043e\u0441\u0447\u0435\u0442\u043e\u0432\u043e\u0434\u044f\u0432\u0430\u043d\u0435", "process_date": "\u0414\u0430\u0442\u0430 \u043d\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430", "due_date": "\u0414\u0430\u0442\u0430 \u043d\u0430 \u043f\u0430\u0434\u0435\u0436", @@ -145,7 +155,10 @@ "internal_reference": "\u0412\u044a\u0442\u0440\u0435\u0448\u043d\u0430 \u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0446\u0438\u044f", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u0410\u043a\u0442\u0438\u0432\u0435\u043d \u043b\u0438 \u0435?", diff --git a/resources/assets/v1/src/locales/ca.json b/resources/assets/v1/src/locales/ca.json index c390c0f553..09c54614ca 100644 --- a/resources/assets/v1/src/locales/ca.json +++ b/resources/assets/v1/src/locales/ca.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Aplicar regles", "fire_webhooks_checkbox": "Disparar webhooks", "no_budget_pointer": "Sembla que encara no tens cap pressupost. N'hauries de crear alguns a la p\u00e0gina de pressuposts<\/a>. Els pressupostos et poden ajudar a fer el seguiment de les teves despeses.", - "no_bill_pointer": "Sembla que encara no tens cap factura. N'hauries de crear alguna a la p\u00e0gina de factures<\/a>. Les factures et poden ajudar a fer el seguiment de les teves despeses.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Compte d'origen", "hidden_fields_preferences": "Pots habilitar m\u00e9s opcions de transacci\u00f3 a la configuraci\u00f3<\/a>.", "destination_account": "Compte de dest\u00ed", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Com aquesta transacci\u00f3 est\u00e0 reconciliada, no podr\u00e0s actualitzar els comptes, ni les quantitats.", "tags": "Etiquetes", "no_budget": "(cap pressupost)", - "no_bill": "(cap factura)", + "no_bill": "(no subscription)", "category": "Categoria", "attachments": "Adjunts", "notes": "Notes", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "No pots editar el compte de dest\u00ed d'una transacci\u00f3 de reconciliaci\u00f3.", "source_account_reconciliation": "No pots editar el compte d'origen d'una transacci\u00f3 de consolidaci\u00f3.", "budget": "Pressupost", - "bill": "Factura", + "bill": "Subscription", "you_create_withdrawal": "Est\u00e0s creant una retirada.", "you_create_transfer": "Est\u00e0s creant una transfer\u00e8ncia.", "you_create_deposit": "Est\u00e0s creant un ingr\u00e9s.", @@ -129,13 +129,23 @@ "logs": "Registres", "response": "Resposta", "visit_webhook_url": "Visitar l'URL del webhook", - "reset_webhook_secret": "Reiniciar el secret del webhook" + "reset_webhook_secret": "Reiniciar el secret del webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Actiu", "interest_date": "Data d'inter\u00e8s", "title": "T\u00edtol", + "date": "Data", "book_date": "Data de registre", "process_date": "Data de processament", "due_date": "Data de venciment", @@ -145,7 +155,10 @@ "internal_reference": "Refer\u00e8ncia interna", "webhook_response": "Resposta", "webhook_trigger": "Activador", - "webhook_delivery": "Lliurament" + "webhook_delivery": "Lliurament", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Est\u00e0 actiu?", diff --git a/resources/assets/v1/src/locales/cs.json b/resources/assets/v1/src/locales/cs.json index f2c3d8fee9..c2bfa03955 100644 --- a/resources/assets/v1/src/locales/cs.json +++ b/resources/assets/v1/src/locales/cs.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Zd\u00e1 se, \u017ee je\u0161t\u011b nem\u00e1te \u017e\u00e1dn\u00e9 rozpo\u010dty. M\u011bli byste n\u011bkter\u00e9 vytvo\u0159it na rozpo\u010dty<\/a>-. Rozpo\u010dty v\u00e1m mohou pomoci sledovat v\u00fddaje.", - "no_bill_pointer": "Zd\u00e1 se, \u017ee je\u0161t\u011b nem\u00e1te \u017e\u00e1dn\u00e9 \u00fa\u010dty. M\u011bli byste n\u011bkter\u00e9 vytvo\u0159it na \u00fa\u010dtech<\/a>. \u00da\u010dty v\u00e1m mohou pomoci sledovat v\u00fddaje.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Zdrojov\u00fd \u00fa\u010det", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "C\u00edlov\u00fd \u00fa\u010det", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "\u0160t\u00edtky", "no_budget": "(\u017e\u00e1dn\u00fd rozpo\u010det)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "Kategorie", "attachments": "P\u0159\u00edlohy", "notes": "Pozn\u00e1mky", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "C\u00edlov\u00fd \u00fa\u010det odsouhlasen\u00e9 transakce nelze upravit.", "source_account_reconciliation": "Nem\u016f\u017eete upravovat zdrojov\u00fd \u00fa\u010det srovn\u00e1vac\u00ed transakce.", "budget": "Rozpo\u010det", - "bill": "\u00da\u010det", + "bill": "Subscription", "you_create_withdrawal": "You're creating a withdrawal.", "you_create_transfer": "You're creating a transfer.", "you_create_deposit": "You're creating a deposit.", @@ -129,13 +129,23 @@ "logs": "Logy", "response": "Odpov\u011b\u010f", "visit_webhook_url": "Nav\u0161t\u00edvit URL webhooku", - "reset_webhook_secret": "Restartovat tajn\u00fd kl\u00ed\u010d webhooku" + "reset_webhook_secret": "Restartovat tajn\u00fd kl\u00ed\u010d webhooku", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Aktivn\u00ed", "interest_date": "\u00darokov\u00e9 datum", "title": "N\u00e1zev", + "date": "Datum", "book_date": "Datum rezervace", "process_date": "Datum zpracov\u00e1n\u00ed", "due_date": "Datum splatnosti", @@ -145,7 +155,10 @@ "internal_reference": "Intern\u00ed reference", "webhook_response": "Response", "webhook_trigger": "Spou\u0161t\u011b\u010d", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Aktivn\u00ed?", diff --git a/resources/assets/v1/src/locales/da.json b/resources/assets/v1/src/locales/da.json index 03f0a3a0f5..c938f942c4 100644 --- a/resources/assets/v1/src/locales/da.json +++ b/resources/assets/v1/src/locales/da.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Det ser ud til, at du ikke har oprettet budgetter endnu. Du burde oprette nogle p\u00e5 budgetsiden<\/a>. Budgetter kan hj\u00e6lpe dig med at holde styr p\u00e5 udgifter.", - "no_bill_pointer": "Du synes ikke at have nogen regninger endnu. Du b\u00f8r oprette nogle p\u00e5 regninger<\/a>-siden. Regninger kan hj\u00e6lpe dig med at holde styr p\u00e5 udgifterne.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Kildekonto", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "Destinationskonto", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Etiketter", "no_budget": "(no budget)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "Kategori", "attachments": "Vedh\u00e6ftninger", "notes": "Noter", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Du kan ikke redigere destinationskontoen p\u00e5 en afstemningstransaktion.", "source_account_reconciliation": "Du kan ikke redigere kildekontoen p\u00e5 en afstemningstransaktion.", "budget": "Budget", - "bill": "Regning", + "bill": "Subscription", "you_create_withdrawal": "You're creating a withdrawal.", "you_create_transfer": "You're creating a transfer.", "you_create_deposit": "You're creating a deposit.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Svar", "visit_webhook_url": "Bes\u00f8g webhook-URL", - "reset_webhook_secret": "Nulstil webhook-hemmelighed" + "reset_webhook_secret": "Nulstil webhook-hemmelighed", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Aktiv", "interest_date": "Rentedato", "title": "Titel", + "date": "Dato", "book_date": "Bogf\u00f8ringsdato", "process_date": "Behandlingsdato", "due_date": "Forfaldsdato", @@ -145,7 +155,10 @@ "internal_reference": "Intern reference", "webhook_response": "Svar", "webhook_trigger": "Udl\u00f8ser", - "webhook_delivery": "Levering" + "webhook_delivery": "Levering", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Aktiv?", diff --git a/resources/assets/v1/src/locales/de.json b/resources/assets/v1/src/locales/de.json index 6d51c1872e..cfb4797d99 100644 --- a/resources/assets/v1/src/locales/de.json +++ b/resources/assets/v1/src/locales/de.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Regeln anwenden", "fire_webhooks_checkbox": "Webhooks abfeuern", "no_budget_pointer": "Sie scheinen noch keine Budgets festgelegt zu haben. Sie sollten einige davon auf der Seite Budgets<\/a> anlegen. Budgets k\u00f6nnen Ihnen dabei helfen, den \u00dcberblick \u00fcber die Ausgaben zu behalten.", - "no_bill_pointer": "Sie scheinen noch keine Rechnungen zu haben. Sie sollten einige auf der Seite Rechnungen<\/a> erstellen. Anhand der Rechnungen k\u00f6nnen Sie den \u00dcberblick \u00fcber Ihre Ausgaben behalten.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Quellkonto", "hidden_fields_preferences": "Sie k\u00f6nnen weitere Buchungsoptionen in Ihren Einstellungen<\/a> aktivieren.", "destination_account": "Zielkonto", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Da diese Buchung abgeglichen ist, k\u00f6nnen Sie weder die Konten noch den\/die Betrag\/Betr\u00e4ge aktualisieren.", "tags": "Schlagw\u00f6rter", "no_budget": "(kein Budget)", - "no_bill": "(keine Belege)", + "no_bill": "(no subscription)", "category": "Kategorie", "attachments": "Anh\u00e4nge", "notes": "Notizen", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Sie k\u00f6nnen das Zielkonto einer Kontenausgleichsbuchung nicht bearbeiten.", "source_account_reconciliation": "Sie k\u00f6nnen das Quellkonto einer Kontenausgleichsbuchung nicht bearbeiten.", "budget": "Budget", - "bill": "Rechnung", + "bill": "Subscription", "you_create_withdrawal": "Sie haben eine Ausgabe erstellt.", "you_create_transfer": "Sie erstellen eine Umbuchung.", "you_create_deposit": "Sie haben eine Einnahme erstellt.", @@ -129,13 +129,23 @@ "logs": "Protokolle", "response": "Antwort", "visit_webhook_url": "Webhook-URL besuchen", - "reset_webhook_secret": "Webhook Secret zur\u00fccksetzen" + "reset_webhook_secret": "Webhook Secret zur\u00fccksetzen", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Aktiv", "interest_date": "Zinstermin", "title": "Titel", + "date": "Datum", "book_date": "Buchungsdatum", "process_date": "Bearbeitungsdatum", "due_date": "F\u00e4lligkeitstermin", @@ -145,7 +155,10 @@ "internal_reference": "Interne Referenz", "webhook_response": "Antwort", "webhook_trigger": "Ausl\u00f6ser", - "webhook_delivery": "Zustellung" + "webhook_delivery": "Zustellung", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Aktiv?", diff --git a/resources/assets/v1/src/locales/el.json b/resources/assets/v1/src/locales/el.json index e2a6d65c7f..326ff98aab 100644 --- a/resources/assets/v1/src/locales/el.json +++ b/resources/assets/v1/src/locales/el.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd webhook", "no_budget_pointer": "\u03a6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c0\u03c9\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bf\u03c1\u03af\u03c3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03cb\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03b1\u03ba\u03cc\u03bc\u03b7. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03ac\u03c0\u03bf\u03b9\u03bf\u03bd \u03c3\u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03c0\u03c1\u03bf\u03cb\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03ce\u03bd<\/a>. \u039f\u03b9 \u03c0\u03c1\u03bf\u03cb\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03bf\u03af \u03c3\u03b1\u03c2 \u03b2\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd \u03bd\u03b1 \u03b5\u03c0\u03b9\u03b2\u03bb\u03ad\u03c0\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b4\u03b1\u03c0\u03ac\u03bd\u03b5\u03c2 \u03c3\u03b1\u03c2.", - "no_bill_pointer": "\u03a6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c0\u03c9\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bf\u03c1\u03af\u03c3\u03b5\u03b9 \u03c0\u03ac\u03b3\u03b9\u03b1 \u03ad\u03be\u03bf\u03b4\u03b1 \u03b1\u03ba\u03cc\u03bc\u03b7. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03ac\u03c0\u03bf\u03b9\u03bf \u03c3\u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03c0\u03ac\u03b3\u03b9\u03c9\u03bd \u03b5\u03be\u03cc\u03b4\u03c9\u03bd<\/a>. \u03a4\u03b1 \u03c0\u03ac\u03b3\u03b9\u03b1 \u03ad\u03be\u03bf\u03b4\u03b1 \u03c3\u03b1\u03c2 \u03b2\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd \u03bd\u03b1 \u03b5\u03c0\u03b9\u03b2\u03bb\u03ad\u03c0\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b4\u03b1\u03c0\u03ac\u03bd\u03b5\u03c2 \u03c3\u03b1\u03c2.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2", "hidden_fields_preferences": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ce\u03bd \u03c3\u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ae\u03c3\u03b5\u03b9\u03c2<\/a>.", "destination_account": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "\u0395\u03c4\u03b9\u03ba\u03ad\u03c4\u03b5\u03c2", "no_budget": "(\u03c7\u03c9\u03c1\u03af\u03c2 \u03c0\u03c1\u03bf\u03cb\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03cc)", - "no_bill": "(\u03c7\u03c9\u03c1\u03af\u03c2 \u03c0\u03ac\u03b3\u03b9\u03bf \u03ad\u03be\u03bf\u03b4\u03bf)", + "no_bill": "(no subscription)", "category": "\u039a\u03b1\u03c4\u03b7\u03b3\u03bf\u03c1\u03af\u03b1", "attachments": "\u03a3\u03c5\u03bd\u03b7\u03bc\u03bc\u03ad\u03bd\u03b1", "notes": "\u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03b5\u03b9\u03c2", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03c1\u03bf\u03c0\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03b1\u03ba\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2.", "source_account_reconciliation": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03c1\u03bf\u03c0\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03b1\u03ba\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2.", "budget": "\u03a0\u03c1\u03bf\u03cb\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03cc\u03c2", - "bill": "\u03a0\u03ac\u03b3\u03b9\u03bf \u03ad\u03be\u03bf\u03b4\u03bf", + "bill": "Subscription", "you_create_withdrawal": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7.", "you_create_transfer": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac.", "you_create_deposit": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03b8\u03b5\u03c3\u03b7.", @@ -129,13 +129,23 @@ "logs": "\u0391\u03c1\u03c7\u03b5\u03af\u03b1 \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 (Logs)", "response": "\u0391\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7", "visit_webhook_url": "\u0395\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf URL \u03c4\u03bf\u03c5 webhook", - "reset_webhook_secret": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd webhook" + "reset_webhook_secret": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", "active": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", "interest_date": "\u0397\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1 \u03c4\u03bf\u03ba\u03b9\u03c3\u03bc\u03bf\u03cd", "title": "\u03a4\u03af\u03c4\u03bb\u03bf\u03c2", + "date": "\u0397\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1", "book_date": "\u0397\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", "process_date": "\u0397\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2", "due_date": "\u0397\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1 \u03c0\u03c1\u03bf\u03b8\u03b5\u03c3\u03bc\u03af\u03b1\u03c2", @@ -145,7 +155,10 @@ "internal_reference": "\u0395\u03c3\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ae \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac", "webhook_response": "\u0391\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7", "webhook_trigger": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "webhook_delivery": "\u03a0\u03b1\u03c1\u03ac\u03b4\u03bf\u03c3\u03b7" + "webhook_delivery": "\u03a0\u03b1\u03c1\u03ac\u03b4\u03bf\u03c3\u03b7", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u0395\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03cc;", diff --git a/resources/assets/v1/src/locales/en-gb.json b/resources/assets/v1/src/locales/en-gb.json index 2999f99e7f..d4d66a3e07 100644 --- a/resources/assets/v1/src/locales/en-gb.json +++ b/resources/assets/v1/src/locales/en-gb.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "You seem to have no budgets yet. You should create some on the budgets<\/a>-page. Budgets can help you keep track of expenses.", - "no_bill_pointer": "You seem to have no bills yet. You should create some on the bills<\/a>-page. Bills can help you keep track of expenses.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Source account", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "Destination account", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Tags", "no_budget": "(no budget)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "Category", "attachments": "Attachments", "notes": "Notes", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "You can't edit the destination account of a reconciliation transaction.", "source_account_reconciliation": "You can't edit the source account of a reconciliation transaction.", "budget": "Budget", - "bill": "Bill", + "bill": "Subscription", "you_create_withdrawal": "You're creating a withdrawal.", "you_create_transfer": "You're creating a transfer.", "you_create_deposit": "You're creating a deposit.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Response", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Active", "interest_date": "Interest date", "title": "Title", + "date": "Date", "book_date": "Book date", "process_date": "Processing date", "due_date": "Due date", @@ -145,7 +155,10 @@ "internal_reference": "Internal reference", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Is active?", diff --git a/resources/assets/v1/src/locales/en.json b/resources/assets/v1/src/locales/en.json index e41ff89456..6581de5727 100644 --- a/resources/assets/v1/src/locales/en.json +++ b/resources/assets/v1/src/locales/en.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "You seem to have no budgets yet. You should create some on the budgets<\/a>-page. Budgets can help you keep track of expenses.", - "no_bill_pointer": "You seem to have no bills yet. You should create some on the bills<\/a>-page. Bills can help you keep track of expenses.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Source account", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "Destination account", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Tags", "no_budget": "(no budget)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "Category", "attachments": "Attachments", "notes": "Notes", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "You can't edit the destination account of a reconciliation transaction.", "source_account_reconciliation": "You can't edit the source account of a reconciliation transaction.", "budget": "Budget", - "bill": "Bill", + "bill": "Subscription", "you_create_withdrawal": "You're creating a withdrawal.", "you_create_transfer": "You're creating a transfer.", "you_create_deposit": "You're creating a deposit.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Response", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Active", "interest_date": "Interest date", "title": "Title", + "date": "Date", "book_date": "Book date", "process_date": "Processing date", "due_date": "Due date", @@ -145,7 +155,10 @@ "internal_reference": "Internal reference", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Is active?", diff --git a/resources/assets/v1/src/locales/es.json b/resources/assets/v1/src/locales/es.json index a48801ff0c..0d8fce0071 100644 --- a/resources/assets/v1/src/locales/es.json +++ b/resources/assets/v1/src/locales/es.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Aplicar reglas", "fire_webhooks_checkbox": "Disparar webhooks", "no_budget_pointer": "Parece que a\u00fan no tienes presupuestos. Debes crear algunos en la p\u00e1gina presupuestos<\/a>. Los presupuestos pueden ayudarle a realizar un seguimiento de los gastos.", - "no_bill_pointer": "Parece que a\u00fan no tienes facturas. Deber\u00edas crear algunas en la p\u00e1gina de facturas<\/a>. Las facturas pueden ayudarte a llevar un seguimiento de los gastos.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Cuenta origen", "hidden_fields_preferences": "Puede habilitar m\u00e1s opciones de transacci\u00f3n en sus ajustes <\/a>.", "destination_account": "Cuenta destino", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Debido a que esta transacci\u00f3n est\u00e1 reconciliada, no podr\u00e1 actualizar las cuentas, ni las cantidades.", "tags": "Etiquetas", "no_budget": "(sin presupuesto)", - "no_bill": "(sin factura)", + "no_bill": "(no subscription)", "category": "Categoria", "attachments": "Archivos adjuntos", "notes": "Notas", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "No puedes editar la cuenta de destino de una transacci\u00f3n de reconciliaci\u00f3n.", "source_account_reconciliation": "No puedes editar la cuenta de origen de una transacci\u00f3n de reconciliaci\u00f3n.", "budget": "Presupuesto", - "bill": "Factura", + "bill": "Subscription", "you_create_withdrawal": "Est\u00e1 creando un gasto.", "you_create_transfer": "Est\u00e1 creando una transferencia.", "you_create_deposit": "Est\u00e1 creando un ingreso.", @@ -129,13 +129,23 @@ "logs": "Registros", "response": "Respuesta", "visit_webhook_url": "Visita la URL del webhook", - "reset_webhook_secret": "Restablecer secreto del webhook" + "reset_webhook_secret": "Restablecer secreto del webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Activo", "interest_date": "Fecha de inter\u00e9s", "title": "T\u00edtulo", + "date": "Fecha", "book_date": "Fecha de registro", "process_date": "Fecha de procesamiento", "due_date": "Fecha de vencimiento", @@ -145,7 +155,10 @@ "internal_reference": "Referencia interna", "webhook_response": "Respuesta", "webhook_trigger": "Disparador", - "webhook_delivery": "Entrega" + "webhook_delivery": "Entrega", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u00bfEst\u00e1 Activo?", diff --git a/resources/assets/v1/src/locales/fi.json b/resources/assets/v1/src/locales/fi.json index 468931fead..9390460c6e 100644 --- a/resources/assets/v1/src/locales/fi.json +++ b/resources/assets/v1/src/locales/fi.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Sinulla ei n\u00e4yt\u00e4 olevan viel\u00e4 budjetteja. Sinun pit\u00e4isi luoda joitakin budjetit<\/a>-sivulla. Budjetit auttavat sinua pit\u00e4m\u00e4\u00e4n kirjaa kuluista.", - "no_bill_pointer": "Sinulla ei n\u00e4yt\u00e4 olevan viel\u00e4 laskuja. Sinun pit\u00e4isi luoda joitakin laskut<\/a>-sivulla. Laskut auttavat sinua pit\u00e4m\u00e4\u00e4n kirjaa kuluista.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "L\u00e4hdetili", "hidden_fields_preferences": "Voit ottaa k\u00e4ytt\u00f6\u00f6n lis\u00e4\u00e4 tapahtumavalintoja asetuksissa<\/a>.", "destination_account": "Kohdetili", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "T\u00e4git", "no_budget": "(ei budjettia)", - "no_bill": "(ei laskua)", + "no_bill": "(no subscription)", "category": "Kategoria", "attachments": "Liitteet", "notes": "Muistiinpanot", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Et voi muokata t\u00e4sm\u00e4ytystapahtuman kohdetili\u00e4.", "source_account_reconciliation": "Et voi muokata t\u00e4sm\u00e4ytystapahtuman l\u00e4hdetili\u00e4.", "budget": "Budjetti", - "bill": "Lasku", + "bill": "Subscription", "you_create_withdrawal": "Olet luomassa nostoa.", "you_create_transfer": "Olet luomassa siirtoa.", "you_create_deposit": "Olet luomassa talletusta.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Response", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL-osoite", "active": "Aktiivinen", "interest_date": "Korkop\u00e4iv\u00e4", "title": "Otsikko", + "date": "P\u00e4iv\u00e4m\u00e4\u00e4r\u00e4", "book_date": "Kirjausp\u00e4iv\u00e4", "process_date": "K\u00e4sittelyp\u00e4iv\u00e4", "due_date": "Er\u00e4p\u00e4iv\u00e4", @@ -145,7 +155,10 @@ "internal_reference": "Sis\u00e4inen viite", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Aktiivinen?", diff --git a/resources/assets/v1/src/locales/fr.json b/resources/assets/v1/src/locales/fr.json index 32c4fd54e2..845809188f 100644 --- a/resources/assets/v1/src/locales/fr.json +++ b/resources/assets/v1/src/locales/fr.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Appliquer les r\u00e8gles", "fire_webhooks_checkbox": "Lancer les webhooks", "no_budget_pointer": "Vous semblez n\u2019avoir encore aucun budget. Vous devriez en cr\u00e9er un sur la page des budgets<\/a>. Les budgets peuvent vous aider \u00e0 garder une trace des d\u00e9penses.", - "no_bill_pointer": "Vous semblez n'avoir encore aucune facture. Vous devriez en cr\u00e9er une sur la page factures<\/a>-. Les factures peuvent vous aider \u00e0 garder une trace des d\u00e9penses.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Compte source", "hidden_fields_preferences": "Vous pouvez activer plus d'options d'op\u00e9rations dans vos param\u00e8tres<\/a>.", "destination_account": "Compte de destination", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Comme cette op\u00e9ration est rapproch\u00e9e, vous ne pourrez pas modifier les comptes, ni le(s) montant(s).", "tags": "Tags", "no_budget": "(pas de budget)", - "no_bill": "(aucune facture)", + "no_bill": "(no subscription)", "category": "Cat\u00e9gorie", "attachments": "Pi\u00e8ces jointes", "notes": "Notes", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Vous ne pouvez pas modifier le compte de destination d'une op\u00e9ration de rapprochement.", "source_account_reconciliation": "Vous ne pouvez pas modifier le compte source d'une op\u00e9ration de rapprochement.", "budget": "Budget", - "bill": "Facture", + "bill": "Subscription", "you_create_withdrawal": "Vous saisissez une d\u00e9pense.", "you_create_transfer": "Vous saisissez un transfert.", "you_create_deposit": "Vous saisissez un d\u00e9p\u00f4t.", @@ -129,13 +129,23 @@ "logs": "Journaux", "response": "R\u00e9ponse", "visit_webhook_url": "Visiter l'URL du webhook", - "reset_webhook_secret": "R\u00e9initialiser le secret du webhook" + "reset_webhook_secret": "R\u00e9initialiser le secret du webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "Liens", "active": "Actif", "interest_date": "Date de valeur (int\u00e9r\u00eats)", "title": "Titre", + "date": "Date", "book_date": "Date d'enregistrement", "process_date": "Date de traitement", "due_date": "\u00c9ch\u00e9ance", @@ -145,7 +155,10 @@ "internal_reference": "R\u00e9f\u00e9rence interne", "webhook_response": "R\u00e9ponse", "webhook_trigger": "D\u00e9clencheur", - "webhook_delivery": "Distribution" + "webhook_delivery": "Distribution", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Actif ?", diff --git a/resources/assets/v1/src/locales/hu.json b/resources/assets/v1/src/locales/hu.json index 525f99061a..d1b1f4a337 100644 --- a/resources/assets/v1/src/locales/hu.json +++ b/resources/assets/v1/src/locales/hu.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "\u00dagy t\u0171nik, m\u00e9g nincsenek k\u00f6lts\u00e9gkeretek. K\u00f6lts\u00e9gkereteket a k\u00f6lts\u00e9gkeretek<\/a> oldalon lehet l\u00e9trehozni. A k\u00f6lts\u00e9gkeretek seg\u00edtenek nyomon k\u00f6vetni a k\u00f6lts\u00e9geket.", - "no_bill_pointer": "\u00dagy t\u0171nik, m\u00e9g nincsenek k\u00f6lts\u00e9gkeretek. K\u00f6lts\u00e9gkereteket a k\u00f6lts\u00e9gkeretek<\/a> oldalon lehet l\u00e9trehozni. A k\u00f6lts\u00e9gkeretek seg\u00edtenek nyomon k\u00f6vetni a k\u00f6lts\u00e9geket.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Forr\u00e1s sz\u00e1mla", "hidden_fields_preferences": "A be\u00e1ll\u00edt\u00e1sokban<\/a> t\u00f6bb mez\u0151 is enged\u00e9lyezhet\u0151.", "destination_account": "C\u00e9lsz\u00e1mla", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "C\u00edmk\u00e9k", "no_budget": "(nincs k\u00f6lts\u00e9gkeret)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "Kateg\u00f3ria", "attachments": "Mell\u00e9kletek", "notes": "Megjegyz\u00e9sek", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Nem lehet szerkeszteni egy egyeztetett tranzakci\u00f3 c\u00e9lsz\u00e1ml\u00e1j\u00e1t.", "source_account_reconciliation": "Nem lehet szerkeszteni egy egyeztetett tranzakci\u00f3 forr\u00e1ssz\u00e1ml\u00e1j\u00e1t.", "budget": "K\u00f6lts\u00e9gkeret", - "bill": "Sz\u00e1mla", + "bill": "Subscription", "you_create_withdrawal": "Egy k\u00f6lts\u00e9g l\u00e9trehoz\u00e1sa.", "you_create_transfer": "Egy \u00e1tutal\u00e1s l\u00e9trehoz\u00e1sa.", "you_create_deposit": "Egy bev\u00e9tel l\u00e9trehoz\u00e1sa.", @@ -129,13 +129,23 @@ "logs": "Napl\u00f3k", "response": "V\u00e1lasz", "visit_webhook_url": "Webhook URL megl\u00e1togat\u00e1sa", - "reset_webhook_secret": "Webhook titok vissza\u00e1ll\u00edt\u00e1sa" + "reset_webhook_secret": "Webhook titok vissza\u00e1ll\u00edt\u00e1sa", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Akt\u00edv", "interest_date": "Kamatfizet\u00e9si id\u0151pont", "title": "C\u00edm", + "date": "D\u00e1tum", "book_date": "K\u00f6nyvel\u00e9s d\u00e1tuma", "process_date": "Feldolgoz\u00e1s d\u00e1tuma", "due_date": "Lej\u00e1rati id\u0151pont", @@ -145,7 +155,10 @@ "internal_reference": "Bels\u0151 hivatkoz\u00e1s", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Akt\u00edv?", diff --git a/resources/assets/v1/src/locales/id.json b/resources/assets/v1/src/locales/id.json index 658f7bfcd4..e6b57a34e8 100644 --- a/resources/assets/v1/src/locales/id.json +++ b/resources/assets/v1/src/locales/id.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Anda tampaknya belum memiliki anggaran. Anda harus membuat beberapa di halaman-anggaran<\/a>. Anggaran dapat membantu anda melacak pengeluaran.", - "no_bill_pointer": "Anda tampaknya belum memiliki tagihan. Anda harus membuat beberapa di halaman-tagihan<\/a>. Tagihan dapat membantu anda melacak pengeluaran.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Akun sumber", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "Akun tujuan", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Tag", "no_budget": "(no budget)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "Kategori", "attachments": "Lampiran", "notes": "Notes", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "You can't edit the destination account of a reconciliation transaction.", "source_account_reconciliation": "Anda tidak dapat mengedit akun sumber dari transaksi rekonsiliasi.", "budget": "Anggaran", - "bill": "Tagihan", + "bill": "Subscription", "you_create_withdrawal": "You're creating a withdrawal.", "you_create_transfer": "You're creating a transfer.", "you_create_deposit": "You're creating a deposit.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Response", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Aktif", "interest_date": "Tanggal bunga", "title": "Judul", + "date": "Tanggal", "book_date": "Tanggal buku", "process_date": "Tanggal pemrosesan", "due_date": "Batas tanggal terakhir", @@ -145,7 +155,10 @@ "internal_reference": "Referensi internal", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Aktif?", diff --git a/resources/assets/v1/src/locales/it.json b/resources/assets/v1/src/locales/it.json index 83df157f95..d7fa97f7d7 100644 --- a/resources/assets/v1/src/locales/it.json +++ b/resources/assets/v1/src/locales/it.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Applica le regole", "fire_webhooks_checkbox": "Esegui webhook", "no_budget_pointer": "Sembra che tu non abbia ancora dei budget. Dovresti crearne alcuni nella pagina dei budget<\/a>. I budget possono aiutarti a tenere traccia delle spese.", - "no_bill_pointer": "Sembra che tu non abbia ancora delle bollette. Dovresti crearne alcune nella pagina delle bollette<\/a>. Le bollette possono aiutarti a tenere traccia delle spese.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Conto di origine", "hidden_fields_preferences": "Puoi abilitare maggiori opzioni per le transazioni nelle tue impostazioni<\/a>.", "destination_account": "Conto destinazione", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Poich\u00e9 questa transazione \u00e8 riconciliata, non potrai aggiornare i conti, n\u00e9 gli importi.", "tags": "Etichette", "no_budget": "(nessun budget)", - "no_bill": "(nessuna bolletta)", + "no_bill": "(no subscription)", "category": "Categoria", "attachments": "Allegati", "notes": "Note", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Non \u00e8 possibile modificare il conto di destinazione di una transazione di riconciliazione.", "source_account_reconciliation": "Non puoi modificare il conto di origine di una transazione di riconciliazione.", "budget": "Budget", - "bill": "Bolletta", + "bill": "Subscription", "you_create_withdrawal": "Stai creando un prelievo.", "you_create_transfer": "Stai creando un trasferimento.", "you_create_deposit": "Stai creando un deposito.", @@ -129,13 +129,23 @@ "logs": "Log", "response": "Risposta", "visit_webhook_url": "Visita URL webhook", - "reset_webhook_secret": "Reimposta il segreto del webhook" + "reset_webhook_secret": "Reimposta il segreto del webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Attivo", "interest_date": "Data di valuta", "title": "Titolo", + "date": "Data", "book_date": "Data contabile", "process_date": "Data elaborazione", "due_date": "Data scadenza", @@ -145,7 +155,10 @@ "internal_reference": "Riferimento interno", "webhook_response": "Risposta", "webhook_trigger": "Trigger", - "webhook_delivery": "Consegna" + "webhook_delivery": "Consegna", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Attivo", diff --git a/resources/assets/v1/src/locales/ja.json b/resources/assets/v1/src/locales/ja.json index 7c7c7d34e1..6a89c8c388 100644 --- a/resources/assets/v1/src/locales/ja.json +++ b/resources/assets/v1/src/locales/ja.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "\u30eb\u30fc\u30eb\u3092\u9069\u7528", "fire_webhooks_checkbox": "Webhook\u3092\u5b9f\u884c", "no_budget_pointer": "\u307e\u3060\u4e88\u7b97\u3092\u7acb\u3066\u3066\u3044\u306a\u3044\u3088\u3046\u3067\u3059\u3002\u4e88\u7b97<\/a>\u30da\u30fc\u30b8\u3067\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4e88\u7b97\u306f\u652f\u51fa\u306e\u628a\u63e1\u306b\u5f79\u7acb\u3061\u307e\u3059\u3002", - "no_bill_pointer": "\u307e\u3060\u8acb\u6c42\u304c\u306a\u3044\u3088\u3046\u3067\u3059\u3002\u8acb\u6c42<\/a>\u30da\u30fc\u30b8\u3067\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u8acb\u6c42\u306f\u652f\u51fa\u306e\u628a\u63e1\u306b\u5f79\u7acb\u3061\u307e\u3059\u3002", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\u5f15\u304d\u51fa\u3057\u53e3\u5ea7", "hidden_fields_preferences": "\u8a2d\u5b9a<\/a> \u3067\u8ffd\u52a0\u306e\u53d6\u5f15\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002", "destination_account": "\u9810\u3051\u5165\u308c\u53e3\u5ea7", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "\u3053\u306e\u53d6\u5f15\u306f\u7167\u5408\u6e08\u307f\u306e\u305f\u3081\u3001\u53e3\u5ea7\u3084\u91d1\u984d\u3092\u66f4\u65b0\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002", "tags": "\u30bf\u30b0", "no_budget": "(\u4e88\u7b97\u306a\u3057)", - "no_bill": "(\u8acb\u6c42\u306a\u3057)", + "no_bill": "(no subscription)", "category": "\u30ab\u30c6\u30b4\u30ea", "attachments": "\u6dfb\u4ed8\u30d5\u30a1\u30a4\u30eb", "notes": "\u5099\u8003", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "\u9810\u3051\u5165\u308c\u53e3\u5ea7\u306e\u53d6\u5f15\u7167\u5408\u3092\u7de8\u96c6\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002", "source_account_reconciliation": "\u5f15\u304d\u51fa\u3057\u53e3\u5ea7\u306e\u53d6\u5f15\u7167\u5408\u3092\u7de8\u96c6\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002", "budget": "\u4e88\u7b97", - "bill": "\u8acb\u6c42", + "bill": "Subscription", "you_create_withdrawal": "\u51fa\u91d1\u3092\u4f5c\u6210\u3057\u3066\u3044\u307e\u3059\u3002", "you_create_transfer": "\u9001\u91d1\u3092\u4f5c\u6210\u3057\u3066\u3044\u307e\u3059\u3002", "you_create_deposit": "\u5165\u91d1\u3092\u4f5c\u6210\u3057\u3066\u3044\u307e\u3059\u3002", @@ -129,13 +129,23 @@ "logs": "\u30ed\u30b0", "response": "\u30ec\u30b9\u30dd\u30f3\u30b9", "visit_webhook_url": "Webhook\u306eURL\u3092\u958b\u304f", - "reset_webhook_secret": "Webhook\u306e\u30b7\u30fc\u30af\u30ec\u30c3\u30c8\u3092\u30ea\u30bb\u30c3\u30c8" + "reset_webhook_secret": "Webhook\u306e\u30b7\u30fc\u30af\u30ec\u30c3\u30c8\u3092\u30ea\u30bb\u30c3\u30c8", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "\u6709\u52b9", "interest_date": "\u5229\u606f\u65e5", "title": "\u30bf\u30a4\u30c8\u30eb", + "date": "\u65e5\u4ed8", "book_date": "\u8a18\u5e33\u65e5", "process_date": "\u51e6\u7406\u65e5", "due_date": "\u671f\u65e5", @@ -145,7 +155,10 @@ "internal_reference": "\u5185\u90e8\u53c2\u7167", "webhook_response": "\u30ec\u30b9\u30dd\u30f3\u30b9", "webhook_trigger": "\u30c8\u30ea\u30ac\u30fc", - "webhook_delivery": "\u914d\u4fe1" + "webhook_delivery": "\u914d\u4fe1", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u6709\u52b9", diff --git a/resources/assets/v1/src/locales/ko.json b/resources/assets/v1/src/locales/ko.json index 74c4129281..2c90553265 100644 --- a/resources/assets/v1/src/locales/ko.json +++ b/resources/assets/v1/src/locales/ko.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "\uaddc\uce59 \uc801\uc6a9", "fire_webhooks_checkbox": "\uc6f9\ud6c5 \uc2e4\ud589", "no_budget_pointer": "\uc608\uc0b0\uc774 \uc544\uc9c1 \uc5c6\ub294 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \uc608\uc0b0<\/a> \ud398\uc774\uc9c0\uc5d0\uc11c \uc608\uc0b0\uc744 \ub9cc\ub4e4\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608\uc0b0\uc740 \uc9c0\ucd9c\uc744 \ucd94\uc801\ud558\ub294\ub370 \ub3c4\uc6c0\uc774 \ub429\ub2c8\ub2e4.", - "no_bill_pointer": "\uccad\uad6c\uc11c\uac00 \uc544\uc9c1 \uc5c6\ub294 \uac83 \uac19\uc2b5\ub2c8\ub2e4. \uccad\uad6c\uc11c<\/a> \ud398\uc774\uc9c0\uc5d0\uc11c \uccad\uad6c\uc11c\ub97c \ub9cc\ub4e4\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uccad\uad6c\uc11c\ub294 \ube44\uc6a9\uc744 \ucd94\uc801\ud558\ub294 \ub370 \ub3c4\uc6c0\uc774 \ub429\ub2c8\ub2e4.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\uc18c\uc2a4 \uacc4\uc815", "hidden_fields_preferences": "\ud658\uacbd\uc124\uc815<\/a>\uc5d0\uc11c \ub354 \ub9ce\uc740 \uac70\ub798 \uc635\uc158\uc744 \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "destination_account": "\ub300\uc0c1 \uacc4\uc815", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "\uc774 \uac70\ub798\uac00 \uc218\uc815\ub418\uc5c8\uae30 \ub54c\ubb38\uc5d0, \uacc4\uc88c\ub098 \uae08\uc561\uc744 \uc5c5\ub370\uc774\ud2b8 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "tags": "\ud0dc\uadf8", "no_budget": "(\uc608\uc0b0 \uc5c6\uc74c)", - "no_bill": "(\uccad\uad6c\uc11c \uc5c6\uc74c)", + "no_bill": "(no subscription)", "category": "\uce74\ud14c\uace0\ub9ac", "attachments": "\ucca8\ubd80 \ud30c\uc77c", "notes": "\ub178\ud2b8", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "\uc870\uc815 \uac70\ub798\uc758 \ub300\uc0c1 \uacc4\uc815\uc740 \ud3b8\uc9d1\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "source_account_reconciliation": "\uc870\uc815 \uac70\ub798\uc758 \uc18c\uc2a4 \uacc4\uc815\uc740 \ud3b8\uc9d1\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "budget": "\uc608\uc0b0", - "bill": "\uccad\uad6c\uc11c", + "bill": "Subscription", "you_create_withdrawal": "\ucd9c\uae08\uc744 \uc0dd\uc131\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.", "you_create_transfer": "\uc804\uc1a1\uc744 \uc0dd\uc131\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.", "you_create_deposit": "\uc785\uae08\uc744 \uc0dd\uc131\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4.", @@ -129,13 +129,23 @@ "logs": "\ub85c\uadf8", "response": "\uc751\ub2f5", "visit_webhook_url": "\uc6f9\ud6c5 URL \ubc29\ubb38", - "reset_webhook_secret": "\uc6f9\ud6c5 \uc2dc\ud06c\ub9bf \uc7ac\uc124\uc815" + "reset_webhook_secret": "\uc6f9\ud6c5 \uc2dc\ud06c\ub9bf \uc7ac\uc124\uc815", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "\ud65c\uc131", "interest_date": "\uc774\uc790 \ub0a0\uc9dc", "title": "\uc81c\ubaa9", + "date": "\ub0a0\uc9dc", "book_date": "\uc608\uc57d\uc77c", "process_date": "\ucc98\ub9ac\uc77c", "due_date": "\uae30\ud55c", @@ -145,7 +155,10 @@ "internal_reference": "\ub0b4\ubd80 \ucc38\uc870", "webhook_response": "\uc751\ub2f5", "webhook_trigger": "\ud2b8\ub9ac\uac70", - "webhook_delivery": "\uc804\ub2ec" + "webhook_delivery": "\uc804\ub2ec", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\ud65c\uc131 \uc0c1\ud0dc\uc785\ub2c8\uae4c?", diff --git a/resources/assets/v1/src/locales/nb.json b/resources/assets/v1/src/locales/nb.json index 3ee13ffde7..b23203f225 100644 --- a/resources/assets/v1/src/locales/nb.json +++ b/resources/assets/v1/src/locales/nb.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Bruk regler", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Det ser ikke ut til at du har noen budsjetter enn\u00e5. Du b\u00f8r opprette noen p\u00e5 budsjett<\/a>-siden. Budsjetter kan hjelpe deg med \u00e5 holde oversikt over utgifter.", - "no_bill_pointer": "Det ser ut til at du ikke har noen regninger enn\u00e5. Du b\u00f8r opprette noen p\u00e5 regninger<\/a>-side. Regninger kan hjelpe deg med \u00e5 holde oversikt over utgifter.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Kildekonto", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "Destinasjonskonto", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Fordi denne transaksjonen er avstemt, vil du ikke kunne oppdatere kontoene eller bel\u00f8pene.", "tags": "Tagger", "no_budget": "(ingen budsjett)", - "no_bill": "(ingen regning)", + "no_bill": "(no subscription)", "category": "Kategori", "attachments": "Vedlegg", "notes": "Notater", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Du kan ikke redigere kildekontoen for en avstemmingstransaksjon.", "source_account_reconciliation": "Du kan ikke redigere kildekontoen for en avstemmingstransaksjon.", "budget": "Budsjett", - "bill": "Regning", + "bill": "Subscription", "you_create_withdrawal": "Du lager et uttak.", "you_create_transfer": "Du lager en overf\u00f8ring.", "you_create_deposit": "Du lager en innskud.", @@ -129,13 +129,23 @@ "logs": "Logger", "response": "Respons", "visit_webhook_url": "Bes\u00f8k URL til webhook", - "reset_webhook_secret": "Tilbakestill Webhook n\u00f8kkel" + "reset_webhook_secret": "Tilbakestill Webhook n\u00f8kkel", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "Nettadresse", "active": "Aktiv", "interest_date": "Rentedato", "title": "Tittel", + "date": "Dato", "book_date": "Bokf\u00f8ringsdato", "process_date": "Prosesseringsdato", "due_date": "Forfallsdato", @@ -145,7 +155,10 @@ "internal_reference": "Intern referanse", "webhook_response": "Respons", "webhook_trigger": "Utl\u00f8ser", - "webhook_delivery": "Levering" + "webhook_delivery": "Levering", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Er aktiv?", diff --git a/resources/assets/v1/src/locales/nl.json b/resources/assets/v1/src/locales/nl.json index a0787e855a..b4dc805008 100644 --- a/resources/assets/v1/src/locales/nl.json +++ b/resources/assets/v1/src/locales/nl.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Regels toepassen", "fire_webhooks_checkbox": "Webhooks starten", "no_budget_pointer": "Je hebt nog geen budgetten. Maak er een aantal op de budgetten<\/a>-pagina. Met budgetten kan je je uitgaven beter bijhouden.", - "no_bill_pointer": "Je hebt nog geen contracten. Maak er een aantal op de contracten<\/a>-pagina. Met contracten kan je je uitgaven beter bijhouden.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Bronrekening", "hidden_fields_preferences": "Je kan meer transactieopties inschakelen in je instellingen<\/a>.", "destination_account": "Doelrekening", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Omdat deze transactie al is afgestemd, kan je het bedrag noch de rekeningen wijzigen.", "tags": "Tags", "no_budget": "(geen budget)", - "no_bill": "(geen contract)", + "no_bill": "(no subscription)", "category": "Categorie", "attachments": "Bijlagen", "notes": "Notities", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Je kan de doelrekening van een afstemming niet wijzigen.", "source_account_reconciliation": "Je kan de bronrekening van een afstemming niet wijzigen.", "budget": "Budget", - "bill": "Contract", + "bill": "Subscription", "you_create_withdrawal": "Je maakt een uitgave.", "you_create_transfer": "Je maakt een overschrijving.", "you_create_deposit": "Je maakt inkomsten.", @@ -129,13 +129,23 @@ "logs": "Logboeken", "response": "Reactie", "visit_webhook_url": "Bezoek URL van webhook", - "reset_webhook_secret": "Reset webhook-geheim" + "reset_webhook_secret": "Reset webhook-geheim", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Actief", "interest_date": "Rentedatum", "title": "Titel", + "date": "Datum", "book_date": "Boekdatum", "process_date": "Verwerkingsdatum", "due_date": "Vervaldatum", @@ -145,7 +155,10 @@ "internal_reference": "Interne verwijzing", "webhook_response": "Reactie", "webhook_trigger": "Trigger", - "webhook_delivery": "Bericht" + "webhook_delivery": "Bericht", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Actief?", diff --git a/resources/assets/v1/src/locales/nn.json b/resources/assets/v1/src/locales/nn.json index 0c57001a34..7e54e42824 100644 --- a/resources/assets/v1/src/locales/nn.json +++ b/resources/assets/v1/src/locales/nn.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Bruk reglar", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Det ser ikkje ut til at du har budsjett enda. Du b\u00f8r oppretta nokon p\u00e5 budsjett<\/a>-sida. Budsjett kan hjelpa deg med \u00e5 halda oversikt over utgifter.", - "no_bill_pointer": "Det ser ut til at du ikkje har nokon rekningar enda. Du b\u00f8r oppretta nokon p\u00e5 rekningar<\/a>-side. Rekningar kan hjelpa deg med \u00e5 holde oversikt over utgifter.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Kjeldekonto", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "M\u00e5lkonto", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "N\u00f8kkelord", "no_budget": "(ingen budsjett)", - "no_bill": "(ingen rekning)", + "no_bill": "(no subscription)", "category": "Kategori", "attachments": "Vedlegg", "notes": "Notat", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Du kan ikkje redigera kildekontoen for ein avstemmingstransaksjon.", "source_account_reconciliation": "Du kan ikkje redigera kildekontoen for ein avstemmingstransaksjon.", "budget": "Budsjett", - "bill": "Rekning", + "bill": "Subscription", "you_create_withdrawal": "Du lager eit uttak.", "you_create_transfer": "Du lager ein overf\u00f8ring.", "you_create_deposit": "Du lager ein innskud.", @@ -129,13 +129,23 @@ "logs": "Logger", "response": "Respons", "visit_webhook_url": "Bes\u00f8k URL til webhook", - "reset_webhook_secret": "Tilbakestill Webhook hemmelegheit" + "reset_webhook_secret": "Tilbakestill Webhook hemmelegheit", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "Nettadresse", "active": "Aktiv", "interest_date": "Rentedato", "title": "Tittel", + "date": "Dato", "book_date": "Bokf\u00f8ringsdato", "process_date": "Prosesseringsdato", "due_date": "Forfallsdato", @@ -145,7 +155,10 @@ "internal_reference": "Intern referanse", "webhook_response": "Respons", "webhook_trigger": "Utl\u00f8ser", - "webhook_delivery": "Levering" + "webhook_delivery": "Levering", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Er aktiv?", diff --git a/resources/assets/v1/src/locales/pl.json b/resources/assets/v1/src/locales/pl.json index 9b50dfea21..e9a0327069 100644 --- a/resources/assets/v1/src/locales/pl.json +++ b/resources/assets/v1/src/locales/pl.json @@ -6,7 +6,7 @@ "flash_success": "Sukces!", "close": "Zamknij", "select_dest_account": "Please select or type a valid destination account name", - "select_source_account": "Please select or type a valid source account name", + "select_source_account": "Wybierz lub wpisz prawid\u0142ow\u0105 nazw\u0119 konta \u017ar\u00f3d\u0142owego", "split_transaction_title": "Opis podzielonej transakcji", "errors_submission": "Co\u015b posz\u0142o nie tak w czasie zapisu. Prosz\u0119, sprawd\u017a b\u0142\u0119dy poni\u017cej.", "split": "Podziel", @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Zastosuj regu\u0142y", "fire_webhooks_checkbox": "Uruchom webhooki", "no_budget_pointer": "Wygl\u0105da na to, \u017ce nie masz jeszcze bud\u017cet\u00f3w. Powiniene\u015b utworzy\u0107 kilka na stronie bud\u017cet\u00f3w<\/a>. Bud\u017cety mog\u0105 Ci pom\u00f3c \u015bledzi\u0107 wydatki.", - "no_bill_pointer": "Wygl\u0105da na to, \u017ce nie masz jeszcze rachunk\u00f3w. Powiniene\u015b utworzy\u0107 kilka na stronie rachunk\u00f3w<\/a>. Rachunki mog\u0105 Ci pom\u00f3c \u015bledzi\u0107 wydatki.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Konto \u017ar\u00f3d\u0142owe", "hidden_fields_preferences": "Mo\u017cesz w\u0142\u0105czy\u0107 wi\u0119cej opcji transakcji w swoich ustawieniach<\/a>.", "destination_account": "Konto docelowe", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Poniewa\u017c ta transakcja jest uzgodniona, nie b\u0119dziesz w stanie zaktualizowa\u0107 ani kont, ani kwot.", "tags": "Tagi", "no_budget": "(brak bud\u017cetu)", - "no_bill": "(brak rachunku)", + "no_bill": "(no subscription)", "category": "Kategoria", "attachments": "Za\u0142\u0105czniki", "notes": "Notatki", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Nie mo\u017cesz edytowa\u0107 konta docelowego transakcji uzgadniania.", "source_account_reconciliation": "Nie mo\u017cesz edytowa\u0107 konta \u017ar\u00f3d\u0142owego transakcji uzgadniania.", "budget": "Bud\u017cet", - "bill": "Rachunek", + "bill": "Subscription", "you_create_withdrawal": "Tworzysz wydatek.", "you_create_transfer": "Tworzysz przelew.", "you_create_deposit": "Tworzysz wp\u0142at\u0119.", @@ -129,13 +129,23 @@ "logs": "Logi", "response": "Odpowied\u017a", "visit_webhook_url": "Odwied\u017a adres URL webhooka", - "reset_webhook_secret": "Resetuj sekret webhooka" + "reset_webhook_secret": "Resetuj sekret webhooka", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Aktywny", "interest_date": "Data odsetek", "title": "Tytu\u0142", + "date": "Data", "book_date": "Data ksi\u0119gowania", "process_date": "Data przetworzenia", "due_date": "Termin realizacji", @@ -145,7 +155,10 @@ "internal_reference": "Wewn\u0119trzny numer", "webhook_response": "Odpowied\u017a", "webhook_trigger": "Wyzwalacz", - "webhook_delivery": "Dor\u0119czenie" + "webhook_delivery": "Dor\u0119czenie", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Jest aktywny?", diff --git a/resources/assets/v1/src/locales/pt-br.json b/resources/assets/v1/src/locales/pt-br.json index c1634e05a2..3043c3c96a 100644 --- a/resources/assets/v1/src/locales/pt-br.json +++ b/resources/assets/v1/src/locales/pt-br.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Aplicar regras", "fire_webhooks_checkbox": "Acionar webhooks", "no_budget_pointer": "Parece que voc\u00ea ainda n\u00e3o tem or\u00e7amentos. Voc\u00ea deve criar alguns na p\u00e1gina de or\u00e7amentos<\/a>. Or\u00e7amentos podem ajud\u00e1-lo a manter o controle das despesas.", - "no_bill_pointer": "Parece que voc\u00ea ainda n\u00e3o tem faturas. Voc\u00ea deve criar algumas em faturas<\/a>. Faturas podem ajudar voc\u00ea a manter o controle de despesas.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Conta origem", "hidden_fields_preferences": "Voc\u00ea pode habilitar mais op\u00e7\u00f5es de transa\u00e7\u00e3o em suas prefer\u00eancias<\/a>.", "destination_account": "Conta destino", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Como a transa\u00e7\u00e3o est\u00e1 reconciliada, voc\u00ea n\u00e3o pode atualizar as contas, nem o(s) valor(es).", "tags": "Tags", "no_budget": "(sem or\u00e7amento)", - "no_bill": "(sem fatura)", + "no_bill": "(no subscription)", "category": "Categoria", "attachments": "Anexos", "notes": "Notas", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Voc\u00ea n\u00e3o pode editar a conta destino de uma transa\u00e7\u00e3o de reconcilia\u00e7\u00e3o.", "source_account_reconciliation": "Voc\u00ea n\u00e3o pode editar a conta de origem de uma transa\u00e7\u00e3o de reconcilia\u00e7\u00e3o.", "budget": "Or\u00e7amento", - "bill": "Fatura", + "bill": "Subscription", "you_create_withdrawal": "Voc\u00ea est\u00e1 criando uma sa\u00edda.", "you_create_transfer": "Voc\u00ea est\u00e1 criando uma transfer\u00eancia.", "you_create_deposit": "Voc\u00ea est\u00e1 criando uma entrada.", @@ -129,13 +129,23 @@ "logs": "Registros", "response": "Resposta", "visit_webhook_url": "Acesse a URL do webhook", - "reset_webhook_secret": "Redefinir chave do webhook" + "reset_webhook_secret": "Redefinir chave do webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Ativo", "interest_date": "Data do juros", "title": "T\u00edtulo", + "date": "Data", "book_date": "Data de lan\u00e7amento", "process_date": "Data de processamento", "due_date": "Data de vencimento", @@ -145,7 +155,10 @@ "internal_reference": "Refer\u00eancia interna", "webhook_response": "Resposta", "webhook_trigger": "Gatilho", - "webhook_delivery": "Entrega" + "webhook_delivery": "Entrega", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Est\u00e1 ativo?", diff --git a/resources/assets/v1/src/locales/pt.json b/resources/assets/v1/src/locales/pt.json index 88df19b096..956a62cfe6 100644 --- a/resources/assets/v1/src/locales/pt.json +++ b/resources/assets/v1/src/locales/pt.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Aplicar regras", "fire_webhooks_checkbox": "Ativar webhooks", "no_budget_pointer": "Parece que ainda n\u00e3o tem or\u00e7amentos. Pode cri\u00e1-los na p\u00e1gina de or\u00e7amentos<\/a>. Os or\u00e7amentos podem ajud\u00e1-lo a controlar as despesas.", - "no_bill_pointer": "Parece que ainda n\u00e3o tem encargos. Pode cri\u00e1-los na p\u00e1gina de encargos<\/a>. Os Encargos podem ajud\u00e1-lo a controlar as despesas.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Conta de origem", "hidden_fields_preferences": "Pode ativar mais op\u00e7\u00f5es de transa\u00e7\u00f5es nas suas prefer\u00eancias<\/a>.", "destination_account": "Conta de destino", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Como esta transa\u00e7\u00e3o est\u00e1 reconciliada, n\u00e3o pode atualizar as contas, nem os montantes.", "tags": "Etiquetas", "no_budget": "(sem or\u00e7amento)", - "no_bill": "(sem encargo)", + "no_bill": "(no subscription)", "category": "Categoria", "attachments": "Anexos", "notes": "Notas", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "N\u00e3o pode editar a conta de destino de uma transa\u00e7\u00e3o de reconcilia\u00e7\u00e3o.", "source_account_reconciliation": "N\u00e3o pode editar a conta de origem de uma transa\u00e7\u00e3o de reconcilia\u00e7\u00e3o.", "budget": "Or\u00e7amento", - "bill": "Encargo", + "bill": "Subscription", "you_create_withdrawal": "Est\u00e1 a criar um levantamento.", "you_create_transfer": "Est\u00e1 a criar uma transfer\u00eancia.", "you_create_deposit": "Est\u00e1 a criar um dep\u00f3sito.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Respostas", "visit_webhook_url": "Ir para URL do webhook", - "reset_webhook_secret": "Redefinir segredo webhook" + "reset_webhook_secret": "Redefinir segredo webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Ativo", "interest_date": "Data de juros", "title": "T\u00edtulo", + "date": "Data", "book_date": "Data de registo", "process_date": "Data de processamento", "due_date": "Data de vencimento", @@ -145,7 +155,10 @@ "internal_reference": "Refer\u00eancia interna", "webhook_response": "Resposta", "webhook_trigger": "Gatilho", - "webhook_delivery": "Entrega" + "webhook_delivery": "Entrega", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Esta ativo?", diff --git a/resources/assets/v1/src/locales/ro.json b/resources/assets/v1/src/locales/ro.json index 06bf7bad93..e4129a99ec 100644 --- a/resources/assets/v1/src/locales/ro.json +++ b/resources/assets/v1/src/locales/ro.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Aplic\u0103 regulile", "fire_webhooks_checkbox": "Webhook-uri de incendiu", "no_budget_pointer": "Se pare c\u0103 nu ave\u021bi \u00eenc\u0103 bugete. Ar trebui s\u0103 crea\u021bi c\u00e2teva pe pagina bugete<\/a>. Bugetele v\u0103 pot ajuta s\u0103 \u021bine\u021bi eviden\u021ba cheltuielilor.", - "no_bill_pointer": "Se pare c\u0103 nu ave\u021bi \u00eenc\u0103 facturi. Ar trebui s\u0103 crea\u021bi unele pe pagina facturi<\/a>. Facturile v\u0103 pot ajuta s\u0103 \u021bine\u021bi eviden\u021ba cheltuielilor.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Contul surs\u0103", "hidden_fields_preferences": "Pute\u021bi activa mai multe op\u021biuni de tranzac\u021bie \u00een preferin\u021bele<\/a> dvs.", "destination_account": "Contul de destina\u021bie", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Etichete", "no_budget": "(nici un buget)", - "no_bill": "(f\u0103r\u0103 factur\u0103)", + "no_bill": "(no subscription)", "category": "Categorie", "attachments": "Ata\u0219amente", "notes": "Noti\u021be", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Nu pute\u021bi edita contul de destina\u021bie al unei tranzac\u021bii de reconciliere.", "source_account_reconciliation": "Nu pute\u021bi edita contul surs\u0103 al unei tranzac\u021bii de reconciliere.", "budget": "Buget", - "bill": "Factur\u0103", + "bill": "Subscription", "you_create_withdrawal": "Creezi o retragere.", "you_create_transfer": "Creezi un transfer.", "you_create_deposit": "Creezi un depozit.", @@ -129,13 +129,23 @@ "logs": "Jurnale", "response": "R\u0103spuns", "visit_webhook_url": "Vizita\u0163i URL-ul webhook", - "reset_webhook_secret": "Resetare secret webhook" + "reset_webhook_secret": "Resetare secret webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Activ", "interest_date": "Data de interes", "title": "Titlu", + "date": "Dat\u0103", "book_date": "Rezerv\u0103 dat\u0103", "process_date": "Data proces\u0103rii", "due_date": "Data scadent\u0103", @@ -145,7 +155,10 @@ "internal_reference": "Referin\u021b\u0103 intern\u0103", "webhook_response": "R\u0103spuns", "webhook_trigger": "Declan\u0219ator", - "webhook_delivery": "Livrare" + "webhook_delivery": "Livrare", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Este activ?", diff --git a/resources/assets/v1/src/locales/ru.json b/resources/assets/v1/src/locales/ru.json index dab5f13086..5bd6948f95 100644 --- a/resources/assets/v1/src/locales/ru.json +++ b/resources/assets/v1/src/locales/ru.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "\u041f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "\u041f\u043e\u0445\u043e\u0436\u0435, \u0443 \u0432\u0430\u0441 \u043f\u043e\u043a\u0430 \u043d\u0435\u0442 \u0431\u044e\u0434\u0436\u0435\u0442\u043e\u0432. \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0445 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u0411\u044e\u0434\u0436\u0435\u0442\u044b<\/a>. \u0411\u044e\u0434\u0436\u0435\u0442\u044b \u043c\u043e\u0433\u0443\u0442 \u043f\u043e\u043c\u043e\u0447\u044c \u0432\u0430\u043c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u0440\u0430\u0441\u0445\u043e\u0434\u044b.", - "no_bill_pointer": "\u041f\u043e\u0445\u043e\u0436\u0435, \u0443 \u0432\u0430\u0441 \u043f\u043e\u043a\u0430 \u043d\u0435\u0442 \u0441\u0447\u0435\u0442\u043e\u0432 \u043d\u0430 \u043e\u043f\u043b\u0430\u0442\u0443. \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0445 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u0421\u0447\u0435\u0442\u0430 \u043d\u0430 \u043e\u043f\u043b\u0430\u0442\u0443<\/a>. \u0421\u0447\u0435\u0442\u0430 \u043d\u0430 \u043e\u043f\u043b\u0430\u0442\u0443 \u043c\u043e\u0433\u0443\u0442 \u043f\u043e\u043c\u043e\u0447\u044c \u0432\u0430\u043c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u0440\u0430\u0441\u0445\u043e\u0434\u044b.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\u0421\u0447\u0451\u0442-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a", "hidden_fields_preferences": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445<\/a>.", "destination_account": "\u0421\u0447\u0451\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "\u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u044d\u0442\u0430 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u0441\u0432\u0435\u0440\u0435\u043d\u0430, \u0432\u044b \u043d\u0435 \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0447\u0435\u0442\u0430, \u043d\u0438 \u0441\u0443\u043c\u043c\u0443(\u044b).", "tags": "\u041c\u0435\u0442\u043a\u0438", "no_budget": "(\u0432\u043d\u0435 \u0431\u044e\u0434\u0436\u0435\u0442\u0430)", - "no_bill": "(\u043d\u0435\u0442 \u0441\u0447\u0451\u0442\u0430 \u043d\u0430 \u043e\u043f\u043b\u0430\u0442\u0443)", + "no_bill": "(no subscription)", "category": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f", "attachments": "\u0412\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "notes": "\u0417\u0430\u043c\u0435\u0442\u043a\u0438", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0447\u0451\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0432\u0435\u0440\u044f\u0435\u043c\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438.", "source_account_reconciliation": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0447\u0451\u0442-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u043b\u044f \u0441\u0432\u0435\u0440\u044f\u0435\u043c\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438.", "budget": "\u0411\u044e\u0434\u0436\u0435\u0442", - "bill": "\u0421\u0447\u0451\u0442 \u043a \u043e\u043f\u043b\u0430\u0442\u0435", + "bill": "Subscription", "you_create_withdrawal": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u0440\u0430\u0441\u0445\u043e\u0434.", "you_create_transfer": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434.", "you_create_deposit": "\u0412\u044b \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0435 \u0434\u043e\u0445\u043e\u0434.", @@ -129,13 +129,23 @@ "logs": "\u041b\u043e\u0433\u0438", "response": "\u041e\u0442\u0432\u0435\u0442", "visit_webhook_url": "\u041f\u043e\u0441\u0435\u0442\u0438\u0442\u044c URL \u0432\u0435\u0431\u0445\u0443\u043a\u0430", - "reset_webhook_secret": "" + "reset_webhook_secret": "", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "\u0421\u0441\u044b\u043b\u043a\u0430", "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0439", "interest_date": "\u0414\u0430\u0442\u0430 \u043d\u0430\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u043d\u0442\u043e\u0432", "title": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a", + "date": "\u0414\u0430\u0442\u0430", "book_date": "\u0414\u0430\u0442\u0430 \u0431\u0440\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", "process_date": "\u0414\u0430\u0442\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438", "due_date": "\u0421\u0440\u043e\u043a \u043e\u043f\u043b\u0430\u0442\u044b", @@ -145,7 +155,10 @@ "internal_reference": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u0441\u0441\u044b\u043b\u043a\u0430", "webhook_response": "\u041e\u0442\u0432\u0435\u0442", "webhook_trigger": "\u0421\u043e\u0431\u044b\u0442\u0438\u044f", - "webhook_delivery": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430" + "webhook_delivery": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u0410\u043a\u0442\u0438\u0432\u0435\u043d?", diff --git a/resources/assets/v1/src/locales/sk.json b/resources/assets/v1/src/locales/sk.json index 61191157b4..81ce869c5b 100644 --- a/resources/assets/v1/src/locales/sk.json +++ b/resources/assets/v1/src/locales/sk.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Zd\u00e1 sa, \u017ee zatia\u013e nem\u00e1te \u017eiadne rozpo\u010dty. Na str\u00e1nke rozpo\u010dty<\/a> by ste si nejak\u00e9 mali vytvori\u0165. Rozpo\u010dty m\u00f4\u017eu pom\u00f4c\u0165 udr\u017ea\u0165 preh\u013ead vo v\u00fddavkoch.", - "no_bill_pointer": "Zd\u00e1 sa, \u017ee zatia\u013e nem\u00e1te \u017eiadne \u00fa\u010dty. Na str\u00e1nke \u00fa\u010dty<\/a> by ste mali nejak\u00e9 vytvori\u0165. \u00da\u010dty m\u00f4\u017eu pom\u00f4c\u0165 udr\u017ea\u0165 si preh\u013ead vo v\u00fddavkoch.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Zdrojov\u00fd \u00fa\u010det", "hidden_fields_preferences": "Viac mo\u017enost\u00ed transakci\u00ed m\u00f4\u017eete povoli\u0165 vo svojich nastaveniach<\/a>.", "destination_account": "Cie\u013eov\u00fd \u00fa\u010det", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "\u0160t\u00edtky", "no_budget": "(\u017eiadny rozpo\u010det)", - "no_bill": "(\u017eiadny \u00fa\u010det)", + "no_bill": "(no subscription)", "category": "Kateg\u00f3ria", "attachments": "Pr\u00edlohy", "notes": "Pozn\u00e1mky", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Nem\u00f4\u017eete upravi\u0165 cie\u013eov\u00fd \u00fa\u010det z\u00fa\u010dtovacej transakcie.", "source_account_reconciliation": "Nem\u00f4\u017eete upravi\u0165 zdrojov\u00fd \u00fa\u010det z\u00fa\u010dtovacej transakcie.", "budget": "Rozpo\u010det", - "bill": "\u00da\u010det", + "bill": "Subscription", "you_create_withdrawal": "Vytv\u00e1rate v\u00fdber.", "you_create_transfer": "Vytv\u00e1rate prevod.", "you_create_deposit": "Vytv\u00e1rate vklad.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Response", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Akt\u00edvne", "interest_date": "\u00darokov\u00fd d\u00e1tum", "title": "N\u00e1zov", + "date": "D\u00e1tum", "book_date": "D\u00e1tum rezerv\u00e1cie", "process_date": "D\u00e1tum spracovania", "due_date": "D\u00e1tum splatnosti", @@ -145,7 +155,10 @@ "internal_reference": "Intern\u00e1 referencia", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Akt\u00edvne?", diff --git a/resources/assets/v1/src/locales/sl.json b/resources/assets/v1/src/locales/sl.json index f854cb8d09..0f5be1c16f 100644 --- a/resources/assets/v1/src/locales/sl.json +++ b/resources/assets/v1/src/locales/sl.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Uporabite pravila", "fire_webhooks_checkbox": "Spro\u017eite Webhooke", "no_budget_pointer": "Zdi se, da \u0161e nimate prora\u010duna. Ustvarite jih nekaj na strani prora\u010duni<\/a>. Prora\u010duni vam lahko pomagajo spremljati stro\u0161ke.", - "no_bill_pointer": "Zdi se, da \u0161e nimate ra\u010dunov. Ustvarite jih na strani ra\u010duni<\/a>. Ra\u010duni vam lahko pomagajo spremljati stro\u0161ke.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Izvorni ra\u010dun", "hidden_fields_preferences": "Ve\u010d mo\u017enosti transakcije lahko omogo\u010dite v nastavitvah<\/a>.", "destination_account": "Ciljni ra\u010dun", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Ker je ta transakcija usklajena, ne boste mogli posodobiti ra\u010dunov niti zneskov.", "tags": "Oznake", "no_budget": "(brez prora\u010duna)", - "no_bill": "(ni ra\u010duna)", + "no_bill": "(no subscription)", "category": "Kategorija", "attachments": "Priloge", "notes": "Opombe", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Pri usklajevalni transakciji ni mo\u017eno urejati ciljnega ra\u010duna.", "source_account_reconciliation": "Pri usklajevalni transakciji ni mo\u017eno urejati izvornega ra\u010duna.", "budget": "Prora\u010dun", - "bill": "Ra\u010dun", + "bill": "Subscription", "you_create_withdrawal": "Ustvarjate odliv.", "you_create_transfer": "Ustvarjate prenos.", "you_create_deposit": "Ustvarja\u0161 priliv.", @@ -129,13 +129,23 @@ "logs": "Dnevniki", "response": "Odziv", "visit_webhook_url": "Obi\u0161\u010dite URL webhooka", - "reset_webhook_secret": "Ponastavi skrivno kodo webhooka" + "reset_webhook_secret": "Ponastavi skrivno kodo webhooka", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Aktivno", "interest_date": "Datum obresti", "title": "Naslov", + "date": "Datum", "book_date": "Datum knji\u017eenja", "process_date": "Datum obdelave", "due_date": "Datum zapadlosti", @@ -145,7 +155,10 @@ "internal_reference": "Notranji sklic", "webhook_response": "Odziv", "webhook_trigger": "Spro\u017eilec", - "webhook_delivery": "Dostava" + "webhook_delivery": "Dostava", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Aktiviran?", diff --git a/resources/assets/v1/src/locales/sv.json b/resources/assets/v1/src/locales/sv.json index 7bfa04fc1b..bb4dd88b7f 100644 --- a/resources/assets/v1/src/locales/sv.json +++ b/resources/assets/v1/src/locales/sv.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Du verkar inte ha n\u00e5gra budgetar \u00e4n. Du b\u00f6r skapa n\u00e5gra p\u00e5 budgetar<\/a>-sidan. Budgetar kan hj\u00e4lpa dig att h\u00e5lla reda p\u00e5 utgifter.", - "no_bill_pointer": "Du verkar inte ha n\u00e5gra r\u00e4kningar \u00e4nnu. Du b\u00f6r skapa n\u00e5gra p\u00e5 r\u00e4kningar<\/a>-sidan. R\u00e4kningar kan hj\u00e4lpa dig att h\u00e5lla reda p\u00e5 utgifter.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "K\u00e4llkonto", "hidden_fields_preferences": "Du kan aktivera fler transaktionsalternativ i dina inst\u00e4llningar<\/a>.", "destination_account": "Till konto", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Etiketter", "no_budget": "(ingen budget)", - "no_bill": "(ingen r\u00e4kning)", + "no_bill": "(no subscription)", "category": "Kategori", "attachments": "Bilagor", "notes": "Noteringar", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Du kan inte redigera destinationskontot f\u00f6r en avst\u00e4mningstransaktion.", "source_account_reconciliation": "Du kan inte redigera k\u00e4llkontot f\u00f6r en avst\u00e4mningstransaktion.", "budget": "Budget", - "bill": "Nota", + "bill": "Subscription", "you_create_withdrawal": "Du skapar ett uttag.", "you_create_transfer": "Du skapar en \u00f6verf\u00f6ring.", "you_create_deposit": "Du skapar en ins\u00e4ttning.", @@ -129,13 +129,23 @@ "logs": "Loggar", "response": "Svar", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "L\u00e4nk", "active": "Aktiv", "interest_date": "R\u00e4ntedatum", "title": "Titel", + "date": "Datum", "book_date": "Bokf\u00f6ringsdatum", "process_date": "Behandlingsdatum", "due_date": "F\u00f6rfallodatum", @@ -145,7 +155,10 @@ "internal_reference": "Intern referens", "webhook_response": "Response", "webhook_trigger": "Utl\u00f6sare", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u00c4r aktiv?", diff --git a/resources/assets/v1/src/locales/tr.json b/resources/assets/v1/src/locales/tr.json index c4fc71fa5c..768c6f0bac 100644 --- a/resources/assets/v1/src/locales/tr.json +++ b/resources/assets/v1/src/locales/tr.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "Hen\u00fcz b\u00fct\u00e7eniz yok gibi g\u00f6r\u00fcn\u00fcyor. b\u00fct\u00e7eler<\/a> sayfas\u0131nda biraz olu\u015fturmal\u0131s\u0131n\u0131z. B\u00fct\u00e7eler, giderleri takip etmenize yard\u0131mc\u0131 olabilir.", - "no_bill_pointer": "Hen\u00fcz faturan\u0131z yok gibi g\u00f6r\u00fcn\u00fcyor. faturalar<\/a> sayfas\u0131nda biraz olu\u015fturmal\u0131s\u0131n\u0131z. Faturalar, harcamalar\u0131 takip etmenize yard\u0131mc\u0131 olabilir.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Kaynak hesap", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "Hedef hesap", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Etiketler", "no_budget": "(b\u00fct\u00e7e yok)", - "no_bill": "(hay\u0131r bill)", + "no_bill": "(no subscription)", "category": "Kategori", "attachments": "Ekler", "notes": "Notlar", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "Bir mutabakat i\u015fleminin hedef hesab\u0131n\u0131 d\u00fczenleyemezsiniz.", "source_account_reconciliation": "Bir mutabakat i\u015fleminin kaynak hesab\u0131n\u0131 d\u00fczenleyemezsiniz.", "budget": "B\u00fct\u00e7e", - "bill": "Fatura", + "bill": "Subscription", "you_create_withdrawal": "You're creating a withdrawal.", "you_create_transfer": "You're creating a transfer.", "you_create_deposit": "You're creating a deposit.", @@ -129,13 +129,23 @@ "logs": "Logs", "response": "Response", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "Aktif", "interest_date": "Faiz tarihi", "title": "Ba\u015fl\u0131k", + "date": "Tarih", "book_date": "Kitap Tarihi", "process_date": "\u0130\u015flem tarihi", "due_date": "Biti\u015f Tarihi", @@ -145,7 +155,10 @@ "internal_reference": "Dahili referans", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "Aktif mi?", diff --git a/resources/assets/v1/src/locales/uk.json b/resources/assets/v1/src/locales/uk.json index e257a8f819..254e22f5b4 100644 --- a/resources/assets/v1/src/locales/uk.json +++ b/resources/assets/v1/src/locales/uk.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "\u0417\u0430\u0441\u0442\u043e\u0441\u0443\u0432\u0430\u0442\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430", "fire_webhooks_checkbox": "\u041f\u043e\u0436\u0435\u0436\u043d\u0456 \u0432\u0435\u0431\u0433\u0430\u043a\u0438", "no_budget_pointer": "\u0417\u0434\u0430\u0454\u0442\u044c\u0441\u044f, \u043d\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u043b\u0438 \u0436\u043e\u0434\u043d\u043e\u0433\u043e \u0431\u044e\u0434\u0436\u0435\u0442\u0443. \u0421\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u0446\u0456 \u0431\u044e\u0434\u0436\u0435\u0442\u0456\u0432<\/a>. \u0411\u044e\u0434\u0436\u0435\u0442\u0438 \u043c\u043e\u0436\u0443\u0442\u044c \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u0442\u0438 \u0432\u0430\u043c \u0441\u0442\u0435\u0436\u0438\u0442\u0438 \u0437\u0430 \u0432\u0438\u0442\u0440\u0430\u0442\u0430\u043c\u0438.", - "no_bill_pointer": "\u0423 \u0432\u0430\u0441, \u0437\u0434\u0430\u0454\u0442\u044c\u0441\u044f, \u0449\u0435 \u043d\u0435\u043c\u0430\u0454 \u0440\u0430\u0445\u0443\u043d\u043a\u0456\u0432 \u0434\u043e \u0441\u043f\u043b\u0430\u0442\u0438. \u0421\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u043a\u0456\u043b\u044c\u043a\u0430 \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u0446\u0456 \u0440\u0430\u0445\u0443\u043d\u043a\u0456\u0432<\/a>. \u0420\u0430\u0445\u0443\u043d\u043a\u0438 \u043c\u043e\u0436\u0443\u0442\u044c \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u0442\u0438 \u0432\u0430\u043c \u0441\u0442\u0435\u0436\u0438\u0442\u0438 \u0437\u0430 \u0432\u0438\u0442\u0440\u0430\u0442\u0430\u043c\u0438.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\u0412\u0438\u0445\u0456\u0434\u043d\u0438\u0439 \u0440\u0430\u0445\u0443\u043d\u043e\u043a", "hidden_fields_preferences": "\u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0431\u0456\u043b\u044c\u0448\u0435 \u043e\u043f\u0446\u0456\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0456\u0457 \u0443 \u0432\u0430\u0448\u043e\u043c\u0443 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f<\/a>.", "destination_account": "\u0420\u0430\u0445\u0443\u043d\u043e\u043a \u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "\u0427\u0435\u0440\u0435\u0437 \u0442\u0435, \u0449\u043e \u0446\u044f \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0456\u044f \u0431\u0443\u043b\u0430 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0430, \u0432\u0438 \u043d\u0435 \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u043d\u043e\u0432\u0438\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0437\u0430\u043f\u0438\u0441\u0438, \u0430 \u0442\u0430\u043a\u043e\u0436 \u0441\u0443\u043c\u0438(\u0457).", "tags": "\u0422\u0435\u0433\u0438", "no_budget": "(\u043f\u043e\u0437\u0430 \u0431\u044e\u0434\u0436\u0435\u0442\u043e\u043c)", - "no_bill": "(\u043d\u0435\u043c\u0430\u0454 \u0440\u0430\u0445\u0443\u043d\u043a\u0443)", + "no_bill": "(no subscription)", "category": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f", "attachments": "\u0412\u043a\u043b\u0430\u0434\u0435\u043d\u043d\u044f", "notes": "\u041f\u0440\u0438\u043c\u0456\u0442\u043a\u0438", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "\u0412\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0433\u043e\u0434\u0436\u0435\u043d\u043d\u044f, \u0440\u0430\u0445\u0443\u043d\u043a\u0443 \u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f.", "source_account_reconciliation": "\u0412\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0457 \u0437\u0432\u0456\u0440\u043a\u0438, \u0440\u0430\u0445\u0443\u043d\u043a\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430.", "budget": "\u0411\u044e\u0434\u0436\u0435\u0442", - "bill": "\u0420\u0430\u0445\u0443\u043d\u043e\u043a", + "bill": "Subscription", "you_create_withdrawal": "\u0412\u0438 \u0441\u0442\u0432\u043e\u0440\u044e\u0454\u0442\u0435 \u0432\u0456\u0434\u043a\u043b\u0438\u043a\u0430\u043d\u043d\u044f.", "you_create_transfer": "\u0412\u0438 \u0441\u0442\u0432\u043e\u0440\u044e\u0454\u0442\u0435 \u043f\u0435\u0440\u0435\u043a\u0430\u0437.", "you_create_deposit": "\u0412\u0438 \u0441\u0442\u0432\u043e\u0440\u044e\u0454\u0442\u0435 \u0434\u0435\u043f\u043e\u0437\u0438\u0442.", @@ -129,13 +129,23 @@ "logs": "\u0416\u0443\u0440\u043d\u0430\u043b\u0438", "response": "\u0412\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c", "visit_webhook_url": "\u0412\u0456\u0434\u0432\u0456\u0434\u0430\u0439\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u0432\u0435\u0431-\u0445\u0443\u043a\u0443", - "reset_webhook_secret": "\u0412\u0456\u0434\u043d\u043e\u0432\u0438\u0442\u0438 \u0441\u0456\u043a\u0440\u0435\u0442 \u0432\u0435\u0431-\u0445\u0443\u043a\u0430" + "reset_webhook_secret": "\u0412\u0456\u0434\u043d\u043e\u0432\u0438\u0442\u0438 \u0441\u0456\u043a\u0440\u0435\u0442 \u0432\u0435\u0431-\u0445\u0443\u043a\u0430", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430", "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u043e", "interest_date": "\u0414\u0430\u0442\u0430 \u043d\u0430\u0440\u0430\u0445\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0441\u043e\u0442\u043a\u0456\u0432", "title": "\u041d\u0430\u0437\u0432\u0430", + "date": "\u0414\u0430\u0442\u0430", "book_date": "\u0414\u0430\u0442\u0430 \u0431\u0440\u043e\u043d\u044e\u0432\u0430\u043d\u043d\u044f", "process_date": "\u0414\u0430\u0442\u0430 \u043e\u043f\u0440\u0430\u0446\u044e\u0432\u0430\u043d\u043d\u044f", "due_date": "\u0414\u0430\u0442\u0430 \u0437\u0430\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u044f", @@ -145,7 +155,10 @@ "internal_reference": "\u0412\u043d\u0443\u0442\u0440\u0456\u0448\u043d\u0454 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f", "webhook_response": "\u0412\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c", "webhook_trigger": "\u0422\u0440\u0438\u0433\u0435\u0440", - "webhook_delivery": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430" + "webhook_delivery": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u0427\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439?", diff --git a/resources/assets/v1/src/locales/vi.json b/resources/assets/v1/src/locales/vi.json index 41ba0ceae9..5e2e002e84 100644 --- a/resources/assets/v1/src/locales/vi.json +++ b/resources/assets/v1/src/locales/vi.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "D\u01b0\u1eddng nh\u01b0 b\u1ea1n ch\u01b0a c\u00f3 ng\u00e2n s\u00e1ch. B\u1ea1n n\u00ean t\u1ea1o v\u00e0i c\u00e1i t\u1ea1i trang ng\u00e2n s\u00e1ch<\/a>-. Ng\u00e2n s\u00e1ch c\u00f3 th\u1ec3 gi\u00fap b\u1ea1n theo d\u00f5i chi ti\u00eau.", - "no_bill_pointer": "D\u01b0\u1eddng nh\u01b0 b\u1ea1n ch\u01b0a c\u00f3 h\u00f3a \u0111\u01a1n. B\u1ea1n n\u00ean t\u1ea1o v\u00e0i c\u00e1i t\u1ea1i trang h\u00f3a \u0111\u01a1n<\/a>-. H\u00f3a \u0111\u01a1n c\u00f3 th\u1ec3 gi\u00fap b\u1ea1n theo d\u00f5i chi ti\u00eau.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "Ngu\u1ed3n t\u00e0i kho\u1ea3n", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "T\u00e0i kho\u1ea3n \u0111\u00edch", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "Nh\u00e3n", "no_budget": "(kh\u00f4ng c\u00f3 ng\u00e2n s\u00e1ch)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "Danh m\u1ee5c", "attachments": "T\u1ec7p \u0111\u00ednh k\u00e8m", "notes": "Ghi ch\u00fa", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "B\u1ea1n kh\u00f4ng th\u1ec3 ch\u1ec9nh s\u1eeda t\u00e0i kho\u1ea3n \u0111\u00edch c\u1ee7a giao d\u1ecbch \u0111\u1ed1i chi\u1ebfu.", "source_account_reconciliation": "B\u1ea1n kh\u00f4ng th\u1ec3 ch\u1ec9nh s\u1eeda t\u00e0i kho\u1ea3n ngu\u1ed3n c\u1ee7a giao d\u1ecbch \u0111\u1ed1i chi\u1ebfu.", "budget": "Ng\u00e2n s\u00e1ch", - "bill": "H\u00f3a \u0111\u01a1n", + "bill": "Subscription", "you_create_withdrawal": "B\u1ea1n \u0111ang t\u1ea1o m\u1ed9t r\u00fat ti\u1ec1n<\/strong>.", "you_create_transfer": "B\u1ea1n \u0111ang t\u1ea1o m\u1ed9t chuy\u1ec3n kho\u1ea3n<\/strong>.", "you_create_deposit": "B\u1ea1n \u0111ang t\u1ea1o m\u1ed9t ti\u1ec1n g\u1eedi<\/strong>.", @@ -129,13 +129,23 @@ "logs": "Nh\u1eadt k\u00fd", "response": "\u0110\u00e1p l\u1ea1i", "visit_webhook_url": "\u0110i \u0111\u1ebfn webhook URL", - "reset_webhook_secret": "C\u00e0i l\u1ea1i kh\u00f3a webhook" + "reset_webhook_secret": "C\u00e0i l\u1ea1i kh\u00f3a webhook", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "H\u00e0nh \u0111\u1ed9ng", "interest_date": "Ng\u00e0y l\u00e3i", "title": "Ti\u00eau \u0111\u1ec1", + "date": "Ng\u00e0y", "book_date": "Ng\u00e0y \u0111\u1eb7t s\u00e1ch", "process_date": "Ng\u00e0y x\u1eed l\u00fd", "due_date": "Ng\u00e0y \u0111\u00e1o h\u1ea1n", @@ -145,7 +155,10 @@ "internal_reference": "T\u00e0i li\u1ec7u tham kh\u1ea3o n\u1ed9i b\u1ed9", "webhook_response": "\u0110\u00e1p l\u1ea1i", "webhook_trigger": "K\u00edch ho\u1ea1t", - "webhook_delivery": "Ph\u00e2n ph\u1ed1i" + "webhook_delivery": "Ph\u00e2n ph\u1ed1i", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u0110ang ho\u1ea1t \u0111\u1ed9ng?", diff --git a/resources/assets/v1/src/locales/zh-cn.json b/resources/assets/v1/src/locales/zh-cn.json index 628b354443..55f1f1559b 100644 --- a/resources/assets/v1/src/locales/zh-cn.json +++ b/resources/assets/v1/src/locales/zh-cn.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "\u5e94\u7528\u89c4\u5219", "fire_webhooks_checkbox": "\u89e6\u53d1 webhook", "no_budget_pointer": "\u60a8\u8fd8\u6ca1\u6709\u9884\u7b97\uff0c\u60a8\u5e94\u8be5\u5728\u9884\u7b97\u9875\u9762<\/a>\u8fdb\u884c\u521b\u5efa\u3002\u9884\u7b97\u53ef\u4ee5\u5e2e\u52a9\u60a8\u8ffd\u8e2a\u652f\u51fa\u3002", - "no_bill_pointer": "\u60a8\u8fd8\u6ca1\u6709\u8d26\u5355\uff0c\u60a8\u5e94\u8be5\u5728\u8d26\u5355\u9875\u9762<\/a>\u8fdb\u884c\u521b\u5efa\u3002\u8d26\u5355\u53ef\u4ee5\u5e2e\u52a9\u60a8\u8ffd\u8e2a\u652f\u51fa\u3002", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\u6765\u6e90\u8d26\u6237", "hidden_fields_preferences": "\u60a8\u53ef\u4ee5\u5728\u504f\u597d\u8bbe\u5b9a<\/a>\u4e2d\u542f\u7528\u66f4\u591a\u4ea4\u6613\u9009\u9879\u3002", "destination_account": "\u76ee\u6807\u8d26\u6237", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "\u8fd9\u7b14\u4ea4\u6613\u5df2\u7ecf\u5bf9\u8d26\uff0c\u60a8\u65e0\u6cd5\u66f4\u65b0\u8d26\u6237\uff0c\u4e5f\u65e0\u6cd5\u66f4\u65b0\u91d1\u989d\u3002", "tags": "\u6807\u7b7e", "no_budget": "(\u65e0\u9884\u7b97)", - "no_bill": "(\u65e0\u8d26\u5355)", + "no_bill": "(no subscription)", "category": "\u5206\u7c7b", "attachments": "\u9644\u4ef6", "notes": "\u5907\u6ce8", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "\u60a8\u4e0d\u80fd\u7f16\u8f91\u5bf9\u8d26\u4ea4\u6613\u7684\u76ee\u6807\u8d26\u6237", "source_account_reconciliation": "\u60a8\u4e0d\u80fd\u7f16\u8f91\u5bf9\u8d26\u4ea4\u6613\u7684\u6765\u6e90\u8d26\u6237\u3002", "budget": "\u9884\u7b97", - "bill": "\u8d26\u5355", + "bill": "Subscription", "you_create_withdrawal": "\u60a8\u6b63\u5728\u521b\u5efa\u4e00\u7b14\u652f\u51fa", "you_create_transfer": "\u60a8\u6b63\u5728\u521b\u5efa\u4e00\u7b14\u8f6c\u8d26", "you_create_deposit": "\u60a8\u6b63\u5728\u521b\u5efa\u4e00\u7b14\u6536\u5165", @@ -129,13 +129,23 @@ "logs": "\u65e5\u5fd7", "response": "\u54cd\u5e94", "visit_webhook_url": "\u8bbf\u95ee webhook URL", - "reset_webhook_secret": "\u91cd\u7f6e webhook \u5bc6\u94a5" + "reset_webhook_secret": "\u91cd\u7f6e webhook \u5bc6\u94a5", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "\u7f51\u5740", "active": "\u542f\u7528", "interest_date": "\u5229\u606f\u65e5\u671f", "title": "\u6807\u9898", + "date": "\u65e5\u671f", "book_date": "\u767b\u8bb0\u65e5\u671f", "process_date": "\u5904\u7406\u65e5\u671f", "due_date": "\u5230\u671f\u65e5", @@ -145,7 +155,10 @@ "internal_reference": "\u5185\u90e8\u5f15\u7528", "webhook_response": "\u54cd\u5e94\u5185\u5bb9", "webhook_trigger": "\u89e6\u53d1\u6761\u4ef6", - "webhook_delivery": "\u53d1\u9001\u683c\u5f0f" + "webhook_delivery": "\u53d1\u9001\u683c\u5f0f", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u662f\u5426\u542f\u7528\uff1f", diff --git a/resources/assets/v1/src/locales/zh-tw.json b/resources/assets/v1/src/locales/zh-tw.json index 931d5a63e3..5dc911c6b5 100644 --- a/resources/assets/v1/src/locales/zh-tw.json +++ b/resources/assets/v1/src/locales/zh-tw.json @@ -21,7 +21,7 @@ "apply_rules_checkbox": "Apply rules", "fire_webhooks_checkbox": "Fire webhooks", "no_budget_pointer": "You seem to have no budgets yet. You should create some on the budgets<\/a>-page. Budgets can help you keep track of expenses.", - "no_bill_pointer": "You seem to have no bills yet. You should create some on the bills<\/a>-page. Bills can help you keep track of expenses.", + "no_bill_pointer": "You seem to have no subscription yet. You should create some on the subscription<\/a>-page. Subscriptions can help you keep track of expenses.", "source_account": "\u4f86\u6e90\u5e33\u6236", "hidden_fields_preferences": "You can enable more transaction options in your preferences<\/a>.", "destination_account": "\u76ee\u6a19\u5e33\u6236", @@ -36,7 +36,7 @@ "is_reconciled_fields_dropped": "Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).", "tags": "\u6a19\u7c64", "no_budget": "(\u7121\u9810\u7b97)", - "no_bill": "(no bill)", + "no_bill": "(no subscription)", "category": "\u5206\u985e", "attachments": "\u9644\u52a0\u6a94\u6848", "notes": "\u5099\u8a3b", @@ -52,7 +52,7 @@ "destination_account_reconciliation": "You can't edit the destination account of a reconciliation transaction.", "source_account_reconciliation": "You can't edit the source account of a reconciliation transaction.", "budget": "\u9810\u7b97", - "bill": "\u5e33\u55ae", + "bill": "Subscription", "you_create_withdrawal": "You're creating a withdrawal.", "you_create_transfer": "You're creating a transfer.", "you_create_deposit": "You're creating a deposit.", @@ -129,13 +129,23 @@ "logs": "\u7d00\u9304\u65e5\u8a8c", "response": "\u56de\u8986", "visit_webhook_url": "Visit webhook URL", - "reset_webhook_secret": "Reset webhook secret" + "reset_webhook_secret": "Reset webhook secret", + "header_exchange_rates": "Exchange rates", + "exchange_rates_intro": "Firefly III supports downloading and using exchange rates. Read more about this in the documentation<\/a>.", + "exchange_rates_from_to": "Between {from} and {to} (and the other way around)", + "exchange_rates_intro_rates": "Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate \"1\" will be used.", + "header_exchange_rates_rates": "Exchange rates", + "header_exchange_rates_table": "Table with exchange rates", + "help_rate_form": "On this day, how many {to} will you get for one {from}?", + "add_new_rate": "Add a new exchange rate", + "save_new_rate": "Save new rate" }, "form": { "url": "URL", "active": "\u555f\u7528", "interest_date": "\u5229\u7387\u65e5\u671f", "title": "\u6a19\u984c", + "date": "\u65e5\u671f", "book_date": "\u767b\u8a18\u65e5\u671f", "process_date": "\u8655\u7406\u65e5\u671f", "due_date": "\u5230\u671f\u65e5", @@ -145,7 +155,10 @@ "internal_reference": "\u5167\u90e8\u53c3\u8003", "webhook_response": "Response", "webhook_trigger": "Trigger", - "webhook_delivery": "Delivery" + "webhook_delivery": "Delivery", + "from_currency_to_currency": "{from} → {to}", + "to_currency_from_currency": "{to} → {from}", + "rate": "Rate" }, "list": { "active": "\u662f\u5426\u555f\u7528\uff1f", diff --git a/resources/assets/v1/webpack.mix.js b/resources/assets/v1/webpack.mix.js index bea3d693cf..04ce6319dc 100644 --- a/resources/assets/v1/webpack.mix.js +++ b/resources/assets/v1/webpack.mix.js @@ -46,3 +46,7 @@ mix.js('src/webhooks/index.js', 'build/webhooks').vue({version: 2}); mix.js('src/webhooks/create.js', 'build/webhooks').vue({version: 2}); mix.js('src/webhooks/edit.js', 'build/webhooks').vue({version: 2}); mix.js('src/webhooks/show.js', 'build/webhooks').vue({version: 2}).copy('build','../../../public/v1/js') + +// exchange rates +mix.js('src/exchange-rates/index.js', 'build/exchange-rates').vue({version: 2}); +mix.js('src/exchange-rates/rates.js', 'build/exchange-rates').vue({version: 2}); diff --git a/resources/assets/v2/package.json b/resources/assets/v2/package.json index 4b37c711f0..eaf1b74bf2 100644 --- a/resources/assets/v2/package.json +++ b/resources/assets/v2/package.json @@ -31,7 +31,7 @@ "chartjs-adapter-date-fns": "^3.0.0", "chartjs-chart-sankey": "^0.14.0", "date-fns": "^4.0.0", - "i18next": "^24.0.0", + "i18next": "^24.2.0", "i18next-chained-backend": "^4.6.2", "i18next-http-backend": "^3.0.1", "i18next-localstorage-backend": "^4.2.0", diff --git a/resources/lang/en_US/breadcrumbs.php b/resources/lang/en_US/breadcrumbs.php index 97e6bf9530..b06bf0f472 100644 --- a/resources/lang/en_US/breadcrumbs.php +++ b/resources/lang/en_US/breadcrumbs.php @@ -44,10 +44,10 @@ return [ 'accounts' => 'Accounts', 'changePassword' => 'Change your password', 'change_email' => 'Change your email address', - 'bills' => 'Bills', - 'newBill' => 'New bill', - 'edit_bill' => 'Edit bill ":name"', - 'delete_bill' => 'Delete bill ":name"', + 'bills' => 'Subscriptions', + 'newBill' => 'New subscription', + 'edit_bill' => 'Edit subscription ":name"', + 'delete_bill' => 'Delete subscription ":name"', 'reports' => 'Reports', 'search_result' => 'Search results for ":query"', 'withdrawal_list' => 'Expenses', @@ -86,4 +86,12 @@ return [ 'mfa_enableMFA' => 'Enable multi-factor authentication', 'mfa_backup_codes' => 'Backup codes', 'mfa_disableMFA' => 'Disable multi-factor authentication', + + // notifications + 'notification_index' => 'Owner notifications', + + // exchange rates + 'exchange_rates_index' => 'Exchange rates', + 'exchange_rates_rates' => 'Exchange rates between :from and :to (and the other way around)', + ]; diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index e805aaae61..dc892b2079 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -26,154 +26,168 @@ declare(strict_types=1); return [ // common items - 'greeting' => 'Hi there,', - 'closing' => 'Beep boop,', - 'signature' => 'The Firefly III Mail Robot', - 'footer_ps' => 'PS: This message was sent because a request from IP :ipAddress triggered it.', + 'greeting' => 'Hi there,', + 'closing' => 'Beep boop,', + 'signature' => 'The Firefly III Mail Robot', + 'footer_ps' => 'PS: This message was sent because a request from IP :ipAddress triggered it.', // admin test - 'admin_test_subject' => 'A test message from your Firefly III installation', - 'admin_test_body' => 'This is a test message from your Firefly III instance. It was sent to :email.', + 'admin_test_subject' => 'A test message from your Firefly III installation', + 'admin_test_body' => 'This is a test message from your Firefly III instance. It was sent to :email.', + 'admin_test_message' => 'This is a test message from your Firefly III instance over channel ":channel".', // Ignore this comment // invite - 'invitation_created_subject' => 'An invitation has been created', - 'invitation_created_body' => 'Admin user ":email" created a user invitation which can be used by whoever is behind email address ":invitee". The invite will be valid for 48hrs.', - 'invite_user_subject' => 'You\'ve been invited to create a Firefly III account.', - 'invitation_introduction' => 'You\'ve been invited to create a Firefly III account on **:host**. Firefly III is a personal, self-hosted, private personal finance manager. All the cool kids are using it.', - 'invitation_invited_by' => 'You\'ve been invited by ":admin" and this invitation was sent to ":invitee". That\'s you, right?', - 'invitation_url' => 'The invitation is valid for 48 hours and can be redeemed by surfing to [Firefly III](:url). Enjoy!', + 'invitation_created_subject' => 'An invitation has been created', + 'invitation_created_body' => 'Admin user ":email" created a user invitation which can be used by whoever is behind email address ":invitee". The invite will be valid for 48hrs.', + 'invite_user_subject' => 'You\'ve been invited to create a Firefly III account.', + 'invitation_introduction' => 'You\'ve been invited to create a Firefly III account on **:host**. Firefly III is a personal, self-hosted, private personal finance manager. All the cool kids are using it.', + 'invitation_invited_by' => 'You\'ve been invited by ":admin" and this invitation was sent to ":invitee". That\'s you, right?', + 'invitation_url' => 'The invitation is valid for 48 hours and can be redeemed by surfing to [Firefly III](:url). Enjoy!', // new IP - 'login_from_new_ip' => 'New login on Firefly III', - 'slack_login_from_new_ip' => 'New Firefly III login from IP :ip (:host)', - 'new_ip_body' => 'Firefly III detected a new login on your account from an unknown IP address. If you never logged in from the IP address below, or it has been more than six months ago, Firefly III will warn you.', - 'new_ip_warning' => 'If you recognize this IP address or the login, you can ignore this message. If you didn\'t login, of if you have no idea what this is about, verify your password security, change it, and log out all other sessions. To do this, go to your profile page. Of course you have 2FA enabled already, right? Stay safe!', - 'ip_address' => 'IP address', - 'host_name' => 'Host', - 'date_time' => 'Date + time', + 'login_from_new_ip' => 'New login on Firefly III', + 'slack_login_from_new_ip' => 'New Firefly III login from IP :ip (:host)', + 'new_ip_body' => 'Firefly III detected a new login on your account from an unknown IP address. If you never logged in from the IP address below, or it has been more than six months ago, Firefly III will warn you.', + 'new_ip_warning' => 'If you recognize this IP address or the login, you can ignore this message. If you didn\'t login, of if you have no idea what this is about, verify your password security, change it, and log out all other sessions. To do this, go to your profile page. Of course you have 2FA enabled already, right? Stay safe!', + 'ip_address' => 'IP address', + 'host_name' => 'Host', + 'date_time' => 'Date + time', + 'user_agent' => 'Browser', // access token created - 'access_token_created_subject' => 'A new access token was created', - 'access_token_created_body' => 'Somebody (hopefully you) just created a new Firefly III API Access Token for your user account.', - 'access_token_created_explanation' => 'With this token, they can access **all** of your financial records through the Firefly III API.', - 'access_token_created_revoke' => 'If this wasn\'t you, please revoke this token as soon as possible at :url', + 'access_token_created_subject' => 'A new access token was created', + 'access_token_created_body' => 'Somebody (hopefully you) just created a new Firefly III API Access Token for your user account.', + 'access_token_created_explanation' => 'With this token, they can access **all** of your financial records through the Firefly III API.', + 'access_token_created_revoke' => 'If this wasn\'t you, please revoke this token as soon as possible at :url', + + // unknown user login attempt + 'unknown_user_subject' => 'An unknown user tried to log in', + 'unknown_user_body' => 'An unknown user tried to log in to Firefly III. The email address they used was ":address".', + 'unknown_user_message' => 'The email address they used was ":address".', + + // known user login attempt + 'failed_login_subject' => 'Firefly III detected a failed login attempt', + 'failed_login_body' => 'Firefly III detected that somebody (you?) failed to login with your account ":email". Please verify that this was you.', + 'failed_login_message' => 'A failed login attempt on your Firefly III account ":email" was detected.', + 'failed_login_warning' => 'If you recognize this IP address or the login attempt, you can ignore this message. If you didn\'t try to login, of if you have no idea what this is about, verify your password security, change it, and log out all other sessions. To do this, go to your profile page. Of course you have 2FA enabled already, right? Stay safe!', // registered - 'registered_subject' => 'Welcome to Firefly III!', - 'registered_subject_admin' => 'A new user has registered', - 'admin_new_user_registered' => 'A new user has registered. User **:email** was given user ID #:id.', - 'registered_welcome' => 'Welcome to [Firefly III](:address). Your registration has made it, and this email is here to confirm it. Yay!', - 'registered_pw' => 'If you have forgotten your password already, please reset it using [the password reset tool](:address/password/reset).', - 'registered_help' => 'There is a help-icon in the top right corner of each page. If you need help, click it!', - 'registered_closing' => 'Enjoy!', - 'registered_firefly_iii_link' => 'Firefly III:', - 'registered_pw_reset_link' => 'Password reset:', - 'registered_doc_link' => 'Documentation:', + 'registered_subject' => 'Welcome to Firefly III!', + 'registered_subject_admin' => 'A new user has registered', + 'admin_new_user_registered' => 'A new user has registered. User **:email** was given user ID #:id.', + 'registered_welcome' => 'Welcome to [Firefly III](:address). Your registration has made it, and this email is here to confirm it. Yay!', + 'registered_pw' => 'If you have forgotten your password already, please reset it using [the password reset tool](:address/password/reset).', + 'registered_help' => 'There is a help-icon in the top right corner of each page. If you need help, click it!', + 'registered_closing' => 'Enjoy!', + 'registered_firefly_iii_link' => 'Firefly III:', + 'registered_pw_reset_link' => 'Password reset:', + 'registered_doc_link' => 'Documentation:', // Ignore this comment // new version - 'new_version_email_subject' => 'A new Firefly III version is available', + 'new_version_email_subject' => 'A new Firefly III version is available', // email change - 'email_change_subject' => 'Your Firefly III email address has changed', - 'email_change_body_to_new' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this message, please ignore and delete it.', - 'email_change_body_to_old' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this to happen, you **must** follow the "undo"-link below to protect your account!', - 'email_change_ignore' => 'If you initiated this change, you may safely ignore this message.', - 'email_change_old' => 'The old email address was: :email', - 'email_change_old_strong' => 'The old email address was: **:email**', - 'email_change_new' => 'The new email address is: :email', - 'email_change_new_strong' => 'The new email address is: **:email**', - 'email_change_instructions' => 'You cannot use Firefly III until you confirm this change. Please follow the link below to do so.', - 'email_change_undo_link' => 'To undo the change, follow this link:', + 'email_change_subject' => 'Your Firefly III email address has changed', + 'email_change_body_to_new' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this message, please ignore and delete it.', + 'email_change_body_to_old' => 'You or somebody with access to your Firefly III account has changed your email address. If you did not expect this to happen, you **must** follow the "undo"-link below to protect your account!', + 'email_change_ignore' => 'If you initiated this change, you may safely ignore this message.', + 'email_change_old' => 'The old email address was: :email', + 'email_change_old_strong' => 'The old email address was: **:email**', + 'email_change_new' => 'The new email address is: :email', + 'email_change_new_strong' => 'The new email address is: **:email**', + 'email_change_instructions' => 'You cannot use Firefly III until you confirm this change. Please follow the link below to do so.', + 'email_change_undo_link' => 'To undo the change, follow this link:', // OAuth token created - 'oauth_created_subject' => 'A new OAuth client has been created', - 'oauth_created_body' => 'Somebody (hopefully you) just created a new Firefly III API OAuth Client for your user account. It\'s labeled ":name" and has callback URL `:url`.', - 'oauth_created_explanation' => 'With this client, they can access **all** of your financial records through the Firefly III API.', - 'oauth_created_undo' => 'If this wasn\'t you, please revoke this client as soon as possible at `:url`', + 'oauth_created_subject' => 'A new OAuth client has been created', + 'oauth_created_body' => 'Somebody (hopefully you) just created a new Firefly III API OAuth Client for your user account. It\'s labeled ":name" and has callback URL `:url`.', + 'oauth_created_explanation' => 'With this client, they can access **all** of your financial records through the Firefly III API.', + 'oauth_created_undo' => 'If this wasn\'t you, please revoke this client as soon as possible at `:url`', // reset password - 'reset_pw_subject' => 'Your password reset request', - 'reset_pw_instructions' => 'Somebody tried to reset your password. If it was you, please follow the link below to do so.', - 'reset_pw_warning' => '**PLEASE** verify that the link actually goes to the Firefly III you expect it to go!', + 'reset_pw_subject' => 'Your password reset request', + 'reset_pw_message' => 'You have received password reset instructions in your email. If this was you, please follow the instructions.', + 'reset_pw_instructions' => 'Somebody tried to reset your password. If it was you, please follow the link below to do so.', + 'reset_pw_warning' => '**PLEASE** verify that the link actually goes to the Firefly III you expect it to go!', // error - 'error_subject' => 'Caught an error in Firefly III', - 'error_intro' => 'Firefly III v:version ran into an error: :errorMessage.', - 'error_type' => 'The error was of type ":class".', - 'error_timestamp' => 'The error occurred on/at: :time.', - 'error_location' => 'This error occurred in file ":file" on line :line with code :code.', - 'error_user' => 'The error was encountered by user #:id, :email.', - 'error_no_user' => 'There was no user logged in for this error or no user was detected.', - 'error_ip' => 'The IP address related to this error is: :ip', - 'error_url' => 'URL is: :url', - 'error_user_agent' => 'User agent: :userAgent', - 'error_stacktrace' => 'The full stacktrace is below. If you think this is a bug in Firefly III, you can forward this message to james@firefly-iii.org. This can help fix the bug you just encountered.', - 'error_github_html' => 'If you prefer, you can also open a new issue on GitHub.', - 'error_github_text' => 'If you prefer, you can also open a new issue on https://github.com/firefly-iii/firefly-iii/issues.', - 'error_stacktrace_below' => 'The full stacktrace is below:', - 'error_headers' => 'The following headers may also be relevant:', - 'error_post' => 'This was submitted by the user:', + 'error_subject' => 'Caught an error in Firefly III', + 'error_intro' => 'Firefly III v:version ran into an error: :errorMessage.', + 'error_type' => 'The error was of type ":class".', + 'error_timestamp' => 'The error occurred on/at: :time.', + 'error_location' => 'This error occurred in file ":file" on line :line with code :code.', + 'error_user' => 'The error was encountered by user #:id, :email.', + 'error_no_user' => 'There was no user logged in for this error or no user was detected.', + 'error_ip' => 'The IP address related to this error is: :ip', + 'error_url' => 'URL is: :url', + 'error_user_agent' => 'User agent: :userAgent', + 'error_stacktrace' => 'The full stacktrace is below. If you think this is a bug in Firefly III, you can forward this message to james@firefly-iii.org. This can help fix the bug you just encountered.', + 'error_github_html' => 'If you prefer, you can also open a new issue on GitHub.', + 'error_github_text' => 'If you prefer, you can also open a new issue on https://github.com/firefly-iii/firefly-iii/issues.', + 'error_stacktrace_below' => 'The full stacktrace is below:', + 'error_headers' => 'The following headers may also be relevant:', + 'error_post' => 'This was submitted by the user:', // Ignore this comment // report new journals - 'new_journals_subject' => 'Firefly III has created a new transaction|Firefly III has created :count new transactions', - 'new_journals_header' => 'Firefly III has created a transaction for you. You can find it in your Firefly III installation:|Firefly III has created :count transactions for you. You can find them in your Firefly III installation:', + 'new_journals_subject' => 'Firefly III has created a new transaction|Firefly III has created :count new transactions', + 'new_journals_header' => 'Firefly III has created a transaction for you. You can find it in your Firefly III installation:|Firefly III has created :count transactions for you. You can find them in your Firefly III installation:', // bill warning - 'bill_warning_subject_end_date' => 'Your bill ":name" is due to end in :diff days', - 'bill_warning_subject_now_end_date' => 'Your bill ":name" is due to end TODAY', - 'bill_warning_subject_extension_date' => 'Your bill ":name" is due to be extended or cancelled in :diff days', - 'bill_warning_subject_now_extension_date' => 'Your bill ":name" is due to be extended or cancelled TODAY', - 'bill_warning_end_date' => 'Your bill **":name"** is due to end on :date. This moment will pass in about **:diff days**.', - 'bill_warning_extension_date' => 'Your bill **":name"** is due to be extended or cancelled on :date. This moment will pass in about **:diff days**.', - 'bill_warning_end_date_zero' => 'Your bill **":name"** is due to end on :date. This moment will pass **TODAY!**', - 'bill_warning_extension_date_zero' => 'Your bill **":name"** is due to be extended or cancelled on :date. This moment will pass **TODAY!**', - 'bill_warning_please_action' => 'Please take the appropriate action.', + 'bill_warning_subject_end_date' => 'Your subscription ":name" is due to end in :diff days', + 'bill_warning_subject_now_end_date' => 'Your subscription ":name" is due to end TODAY', + 'bill_warning_subject_extension_date' => 'Your subscription ":name" is due to be extended or cancelled in :diff days', + 'bill_warning_subject_now_extension_date' => 'Your subscription ":name" is due to be extended or cancelled TODAY', + 'bill_warning_end_date' => 'Your subscription **":name"** is due to end on :date. This moment will pass in about **:diff days**.', + 'bill_warning_extension_date' => 'Your subscription **":name"** is due to be extended or cancelled on :date. This moment will pass in about **:diff days**.', + 'bill_warning_end_date_zero' => 'Your subscription **":name"** is due to end on :date. This moment will pass **TODAY!**', + 'bill_warning_extension_date_zero' => 'Your subscription **":name"** is due to be extended or cancelled on :date. This moment will pass **TODAY!**', + 'bill_warning_please_action' => 'Please take the appropriate action.', // user has enabled MFA - 'enabled_mfa_subject' => 'You have enabled multi-factor authentication', - 'enabled_mfa_slack' => 'You (:email) have enabled multi-factor authentication. Is this not correct? Check your settings!', - 'have_enabled_mfa' => 'You have enabled multi-factor authentication on your Firefly III account ":email". This means that you will need to use an authenticator app to log in from now on.', - 'enabled_mfa_warning' => 'If you did not enable this, please contact your administrator immediately or check out the Firefly III documentation.', + 'enabled_mfa_subject' => 'You have enabled multi-factor authentication', + 'enabled_mfa_slack' => 'You (:email) have enabled multi-factor authentication. Is this not correct? Check your settings!', + 'have_enabled_mfa' => 'You have enabled multi-factor authentication on your Firefly III account ":email". This means that you will need to use an authenticator app to log in from now on.', + 'enabled_mfa_warning' => 'If you did not enable this, please contact your administrator immediately or check out the Firefly III documentation.', - 'disabled_mfa_subject' => 'You have disabled multi-factor authentication!', - 'disabled_mfa_slack' => 'You (:email) have disabled multi-factor authentication. Is this not correct? Check your settings!', - 'have_disabled_mfa' => 'You have disabled multi-factor authentication on your Firefly III account ":email".', - 'disabled_mfa_warning' => 'If you did not disable this, please contact your administrator immediately or check out the Firefly III documentation.', + 'disabled_mfa_subject' => 'You have disabled multi-factor authentication!', + 'disabled_mfa_slack' => 'You (:email) have disabled multi-factor authentication. Is this not correct? Check your settings!', + 'have_disabled_mfa' => 'You have disabled multi-factor authentication on your Firefly III account ":email".', + 'disabled_mfa_warning' => 'If you did not disable this, please contact your administrator immediately or check out the Firefly III documentation.', - 'new_backup_codes_subject' => 'You have generated new back-up codes', - 'new_backup_codes_slack' => 'You (:email) have generated new back-up codes. These can be used to login to Firefly III. Is this not correct? Check your settings!', - 'new_backup_codes_intro' => 'You (:email) have generated new back-up codes. These can be used to login to Firefly III if you lose access to your authenticator app.', - 'new_backup_codes_warning' => 'Please store these codes in a safe place. If you lose them, you will not be able to log in to Firefly III. If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.', + 'new_backup_codes_subject' => 'You have generated new back-up codes', + 'new_backup_codes_slack' => 'You (:email) have generated new back-up codes. These can be used to login to Firefly III. Is this not correct? Check your settings!', + 'new_backup_codes_intro' => 'You (:email) have generated new back-up codes. These can be used to login to Firefly III if you lose access to your authenticator app.', + 'new_backup_codes_warning' => 'Please store these codes in a safe place. If you lose them, you will not be able to log in to Firefly III. If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.', - 'used_backup_code_subject' => 'You have used a back-up code to login', - 'used_backup_code_slack' => 'You (:email) have used a back-up code to login', + 'used_backup_code_subject' => 'You have used a back-up code to login', + 'used_backup_code_slack' => 'You (:email) have used a back-up code to login', - 'used_backup_code_intro' => 'You (:email) have used a back-up code to login to Firefly III. You now have one less back-up code to login with. Please remove it from your list.', - 'used_backup_code_warning' => 'If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.', + 'used_backup_code_intro' => 'You (:email) have used a back-up code to login to Firefly III. You now have one less back-up code to login with. Please remove it from your list.', + 'used_backup_code_warning' => 'If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.', // few left: - 'mfa_few_backups_left_subject' => 'You have only :count backup code(s) left!', - 'mfa_few_backups_left_slack' => 'You (:email) have only :count backup code(s) left!', - 'few_backup_codes_intro' => 'You (:email) have used most of your backup codes, and now have only :count left. Please generate new ones as soon as possible.', - 'few_backup_codes_warning' => 'Without backup codes, you cannot recover your MFA login if you lose access to your code generator.', + 'mfa_few_backups_left_subject' => 'You have only :count backup code(s) left!', + 'mfa_few_backups_left_slack' => 'You (:email) have only :count backup code(s) left!', + 'few_backup_codes_intro' => 'You (:email) have used most of your backup codes, and now have only :count left. Please generate new ones as soon as possible.', + 'few_backup_codes_warning' => 'Without backup codes, you cannot recover your MFA login if you lose access to your code generator.', // NO left: - 'mfa_no_backups_left_subject' => 'You have NO backup codes left!', - 'mfa_no_backups_left_slack' => 'You (:email) NO backup codes left!', - 'no_backup_codes_intro' => 'You (:email) have used ALL of your backup codes. Please generate new ones as soon as possible.', - 'no_backup_codes_warning' => 'Without backup codes, you cannot recover your MFA login if you lose access to your code generator.', + 'mfa_no_backups_left_subject' => 'You have NO backup codes left!', + 'mfa_no_backups_left_slack' => 'You (:email) NO backup codes left!', + 'no_backup_codes_intro' => 'You (:email) have used ALL of your backup codes. Please generate new ones as soon as possible.', + 'no_backup_codes_warning' => 'Without backup codes, you cannot recover your MFA login if you lose access to your code generator.', // many failed MFA attempts - 'mfa_many_failed_subject' => 'You have tried and failed to use multi-factor authentication :count times now!', - 'mfa_many_failed_slack' => 'You (:email) have tried and failed to use multi-factor authentication :count times now. Is this not correct? Check your settings!', - 'mfa_many_failed_attempts_intro' => 'You (:email) have tried :count times to use a multi-factor authentication code, but these login attempts have failed. Are you sure you are using the right MFA code? Are you sure the time on the server is correct?', - 'mfa_many_failed_attempts_warning' => 'If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.', + 'mfa_many_failed_subject' => 'You have tried and failed to use multi-factor authentication :count times now!', + 'mfa_many_failed_slack' => 'You (:email) have tried and failed to use multi-factor authentication :count times now. Is this not correct? Check your settings!', + 'mfa_many_failed_attempts_intro' => 'You (:email) have tried :count times to use a multi-factor authentication code, but these login attempts have failed. Are you sure you are using the right MFA code? Are you sure the time on the server is correct?', + 'mfa_many_failed_attempts_warning' => 'If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.', ]; // Ignore this comment diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index e472b7484a..bd38b2404d 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -26,2744 +26,2868 @@ declare(strict_types=1); return [ // general stuff: - 'stored_in_tz' => 'stored in ":timezone"', - 'displayed_in_tz' => 'displayed in ":timezone"', - 'close' => 'Close', - 'actions' => 'Actions', - 'edit' => 'Edit', - 'delete' => 'Delete', - 'split' => 'Split', - 'single_split' => 'Split', - 'clone' => 'Clone', - 'clone_and_edit' => 'Clone and edit', - 'confirm_action' => 'Confirm action', - 'last_seven_days' => 'Last seven days', - 'last_thirty_days' => 'Last thirty days', - 'last_180_days' => 'Last 180 days', - 'month_to_date' => 'Month to date', - 'year_to_date' => 'Year to date', - 'YTD' => 'YTD', - 'welcome_back' => 'What\'s playing?', - 'main_dashboard_page_title' => 'Home', - 'everything' => 'Everything', - 'today' => 'today', - 'customRange' => 'Custom range', - 'date_range' => 'Date range', - 'apply' => 'Apply', - 'select_date' => 'Select date..', - 'cancel' => 'Cancel', - 'from' => 'From', - 'to' => 'To', - 'structure' => 'Structure', - 'help_translating' => 'This help text is not yet available in your language. Will you help translate?', - 'showEverything' => 'Show everything', - 'never' => 'Never', - 'no_results_for_empty_search' => 'Your search was empty, so nothing was found.', - 'removed_amount' => 'Removed :amount', - 'added_amount' => 'Added :amount', - 'asset_account_role_help' => 'Any extra options resulting from your choice can be set later.', - 'Opening balance' => 'Opening balance', - 'create_new_stuff' => 'Create new stuff', - 'new_withdrawal' => 'New withdrawal', - 'create_new_transaction' => 'Create a new transaction', - 'sidebar_frontpage_create' => 'Create', - 'new_transaction' => 'New transaction', - 'no_rules_for_bill' => 'This bill has no rules associated to it.', - 'go_to_asset_accounts' => 'View your asset accounts', - 'go_to_budgets' => 'Go to your budgets', - 'go_to_withdrawals' => 'Go to your withdrawals', - 'clones_journal_x' => 'This transaction is a clone of ":description" (#:id)', - 'go_to_categories' => 'Go to your categories', - 'go_to_bills' => 'Go to your bills', - 'go_to_expense_accounts' => 'See your expense accounts', - 'go_to_revenue_accounts' => 'See your revenue accounts', - 'go_to_piggies' => 'Go to your piggy banks', - 'new_deposit' => 'New deposit', - 'new_transfer' => 'New transfer', - 'new_transfers' => 'New transfer', - 'new_asset_account' => 'New asset account', - 'new_expense_account' => 'New expense account', - 'new_revenue_account' => 'New revenue account', - 'new_liabilities_account' => 'New liability', - 'new_budget' => 'New budget', - 'new_bill' => 'New bill', - 'block_account_logout' => 'You have been logged out. Blocked accounts cannot use this site. Did you register with a valid email address?', - 'flash_success' => 'Success!', - 'flash_info' => 'Message', - 'flash_warning' => 'Warning!', - 'flash_error' => 'Error!', - 'flash_danger' => 'Danger!', - 'flash_info_multiple' => 'There is one message|There are :count messages', - 'flash_error_multiple' => 'There is one error|There are :count errors', - 'net_worth' => 'Net worth', - 'help_for_this_page' => 'Help for this page', - 'help_for_this_page_body' => 'You can find more information about this page in the documentation.', - 'two_factor_welcome' => 'Hello!', - 'two_factor_enter_code' => 'To continue, please enter your two factor authentication code. Your application can generate it for you.', - 'two_factor_code_here' => 'Enter code here', - 'two_factor_title' => 'Two factor authentication', - 'authenticate' => 'Authenticate', - 'two_factor_forgot_title' => 'Lost two factor authentication', - 'two_factor_forgot' => 'I forgot my two-factor thing.', - 'two_factor_lost_header' => 'Lost your two factor authentication?', - 'two_factor_lost_intro' => 'If you lost your backup codes as well, you have bad luck. This is not something you can fix from the web interface. You have two choices.', - 'two_factor_lost_fix_self' => 'If you run your own instance of Firefly III, read this entry in the FAQ for instructions.', - 'two_factor_lost_fix_owner' => 'Otherwise, email the site owner, :site_owner and ask them to reset your two factor authentication.', - 'mfa_backup_code' => 'You have used a backup code to login to Firefly III. It can\'t be used again, so cross it from your list.', - 'pref_two_factor_new_backup_codes' => 'Get new backup codes', - 'pref_two_factor_backup_code_count' => 'You have :count valid backup code.|You have :count valid backup codes.', - '2fa_i_have_them' => 'I stored them!', - 'warning_much_data' => ':days days of data may take a while to load.', - 'registered' => 'You have registered successfully!', - 'Default asset account' => 'Default asset account', - 'no_budget_pointer' => 'You seem to have no budgets yet. You should create some on the budgets-page. Budgets can help you keep track of expenses.', - 'no_bill_pointer' => 'You seem to have no bills yet. You should create some on the bills-page. Bills can help you keep track of expenses.', - 'Savings account' => 'Savings account', - 'Credit card' => 'Credit card', - 'source_accounts' => 'Source account|Source accounts', - 'destination_accounts' => 'Destination account|Destination accounts', - 'user_id_is' => 'Your user id is :user', - 'field_supports_markdown' => 'This field supports Markdown.', - 'need_more_help' => 'If you need more help using Firefly III, please open a ticket on Github.', - 'reenable_intro_text' => 'You can also re-enable the introduction guidance.', - 'intro_boxes_after_refresh' => 'The introduction boxes will reappear when you refresh the page.', - 'show_all_no_filter' => 'Show all transactions without grouping them by date.', - 'expenses_by_category' => 'Expenses by category', - 'expenses_by_budget' => 'Expenses by budget', - 'income_by_category' => 'Income by category', - 'expenses_by_asset_account' => 'Expenses by asset account', - 'expenses_by_expense_account' => 'Expenses by expense account', - 'cannot_redirect_to_account' => 'Firefly III cannot redirect you to the correct page. Apologies.', - 'sum_of_expenses' => 'Sum of expenses', - 'sum_of_income' => 'Sum of income', - 'liabilities' => 'Liabilities', - 'spent_in_specific_budget' => 'Spent in budget ":budget"', - 'spent_in_specific_double' => 'Spent in account ":account"', - 'earned_in_specific_double' => 'Earned in account ":account"', - 'source_account' => 'Source account', - 'source_account_reconciliation' => 'You can\'t edit the source account of a reconciliation transaction.', - 'destination_account' => 'Destination account', - 'destination_account_reconciliation' => 'You can\'t edit the destination account of a reconciliation transaction.', - 'sum_of_expenses_in_budget' => 'Spent total in budget ":budget"', - 'left_in_budget_limit' => 'Left to spend according to budgeting', - 'current_period' => 'Current period', - 'show_the_current_period_and_overview' => 'Show the current period and overview', - 'pref_languages_locale' => 'For a language other than English to work properly, your operating system must be equipped with the correct locale-information. If these are not present, currency data, dates and amounts may be formatted wrong.', - 'budget_in_period' => 'All transactions for budget ":name" between :start and :end in :currency', - 'chart_budget_in_period' => 'Chart for all transactions for budget ":name" between :start and :end in :currency', - 'chart_budget_in_period_only_currency' => 'The amount you budgeted was in :currency, so this chart will only show transactions in :currency.', - 'chart_account_in_period' => 'Chart for all transactions for account ":name" (:balance) between :start and :end', - 'chart_category_in_period' => 'Chart for all transactions for category ":name" between :start and :end', - 'chart_category_all' => 'Chart for all transactions for category ":name"', - 'clone_withdrawal' => 'Clone this withdrawal', - 'clone_deposit' => 'Clone this deposit', - 'clone_transfer' => 'Clone this transfer', - 'multi_select_no_selection' => 'None selected', - 'multi_select_select_all' => 'Select all', - 'multi_select_n_selected' => 'selected', - 'multi_select_all_selected' => 'All selected', - 'multi_select_filter_placeholder' => 'Find..', - 'intro_next_label' => 'Next', - 'intro_prev_label' => 'Previous', - 'intro_skip_label' => 'Skip', - 'intro_done_label' => 'Done', - 'between_dates_breadcrumb' => 'Between :start and :end', - 'all_journals_without_budget' => 'All transactions without a budget', - 'journals_without_budget' => 'Transactions without a budget', - 'all_journals_without_category' => 'All transactions without a category', - 'journals_without_category' => 'Transactions without a category', - 'all_journals_for_account' => 'All transactions for account :name', - 'chart_all_journals_for_account' => 'Chart of all transactions for account :name', - 'journals_in_period_for_account' => 'All transactions for account :name between :start and :end', - 'journals_in_period_for_account_js' => 'All transactions for account {title} between {start} and {end}', - 'transferred' => 'Transferred', - 'all_withdrawal' => 'All expenses', - 'all_transactions' => 'All transactions', - 'title_withdrawal_between' => 'All expenses between :start and :end', - 'all_deposit' => 'All revenue', - 'title_deposit_between' => 'All revenue between :start and :end', - 'all_transfers' => 'All transfers', - 'title_transfers_between' => 'All transfers between :start and :end', - 'all_transfer' => 'All transfers', - 'all_journals_for_tag' => 'All transactions for tag ":tag"', - 'title_transfer_between' => 'All transfers between :start and :end', - 'all_journals_for_category' => 'All transactions for category :name', - 'all_journals_for_budget' => 'All transactions for budget :name', - 'chart_all_journals_for_budget' => 'Chart of all transactions for budget :name', - 'journals_in_period_for_category' => 'All transactions for category :name between :start and :end', - 'journals_in_period_for_tag' => 'All transactions for tag :tag between :start and :end', - 'not_available_demo_user' => 'The feature you try to access is not available to demo users.', - 'exchange_rate_instructions' => 'Asset account "@name" only accepts transactions in @native_currency. If you wish to use @foreign_currency instead, make sure that the amount in @native_currency is known as well:', - 'transfer_exchange_rate_instructions' => 'Source asset account "@source_name" only accepts transactions in @source_currency. Destination asset account "@dest_name" only accepts transactions in @dest_currency. You must provide the transferred amount correctly in both currencies.', - 'transaction_data' => 'Transaction data', - 'invalid_server_configuration' => 'Invalid server configuration', - 'invalid_locale_settings' => 'Firefly III is unable to format monetary amounts because your server is missing the required packages. There are instructions how to do this.', - 'quickswitch' => 'Quickswitch', - 'sign_in_to_start' => 'Sign in to start your session', - 'sign_in' => 'Sign in', - 'register_new_account' => 'Register a new account', - 'forgot_my_password' => 'I forgot my password', - 'problems_with_input' => 'There were some problems with your input.', - 'reset_password' => 'Reset your password', - 'button_reset_password' => 'Reset password', - 'reset_button' => 'Reset', - 'want_to_login' => 'I want to login', - 'login_page_title' => 'Login to Firefly III', - 'register_page_title' => 'Register at Firefly III', - 'forgot_pw_page_title' => 'Forgot your password for Firefly III', - 'reset_pw_page_title' => 'Reset your password for Firefly III', - 'cannot_reset_demo_user' => 'You cannot reset the password of the demo user.', - 'no_att_demo_user' => 'The demo user can\'t upload attachments.', - 'button_register' => 'Register', - 'authorization' => 'Authorization', - 'active_bills_only' => 'active bills only', - 'active_bills_only_total' => 'all active bills', - 'active_exp_bills_only' => 'active and expected bills only', - 'active_exp_bills_only_total' => 'all active expected bills only', - 'per_period_sum_1D' => 'Expected daily costs', - 'per_period_sum_1W' => 'Expected weekly costs', - 'per_period_sum_1M' => 'Expected monthly costs', - 'per_period_sum_3M' => 'Expected quarterly costs', - 'per_period_sum_6M' => 'Expected half-yearly costs', - 'per_period_sum_1Y' => 'Expected yearly costs', - 'average_per_bill' => 'average per bill', - 'expected_total' => 'expected total', - 'reconciliation_account_name' => ':name reconciliation (:currency)', - 'saved' => 'Saved', - 'advanced_options' => 'Advanced options', - 'advanced_options_explain' => 'Some pages in Firefly III have advanced options hidden behind this button. This page doesn\'t have anything fancy here, but do check out the others!', - 'here_be_dragons' => 'Hic sunt dracones', + 'stored_in_tz' => 'stored in ":timezone"', + 'displayed_in_tz' => 'displayed in ":timezone"', + 'close' => 'Close', + 'actions' => 'Actions', + 'edit' => 'Edit', + 'delete' => 'Delete', + 'split' => 'Split', + 'single_split' => 'Split', + 'clone' => 'Clone', + 'clone_and_edit' => 'Clone and edit', + 'confirm_action' => 'Confirm action', + 'last_seven_days' => 'Last seven days', + 'last_thirty_days' => 'Last thirty days', + 'last_180_days' => 'Last 180 days', + 'month_to_date' => 'Month to date', + 'year_to_date' => 'Year to date', + 'YTD' => 'YTD', + 'welcome_back' => 'What\'s playing?', + 'main_dashboard_page_title' => 'Home', + 'everything' => 'Everything', + 'today' => 'today', + 'customRange' => 'Custom range', + 'date_range' => 'Date range', + 'apply' => 'Apply', + 'select_date' => 'Select date..', + 'cancel' => 'Cancel', + 'from' => 'From', + 'to' => 'To', + 'structure' => 'Structure', + 'help_translating' => 'This help text is not yet available in your language. Will you help translate?', + 'showEverything' => 'Show everything', + 'never' => 'Never', + 'no_results_for_empty_search' => 'Your search was empty, so nothing was found.', + 'removed_amount' => 'Removed :amount', + 'added_amount' => 'Added :amount', + 'asset_account_role_help' => 'Any extra options resulting from your choice can be set later.', + 'Opening balance' => 'Opening balance', + 'create_new_stuff' => 'Create new stuff', + 'new_withdrawal' => 'New withdrawal', + 'create_new_transaction' => 'Create a new transaction', + 'sidebar_frontpage_create' => 'Create', + 'new_transaction' => 'New transaction', + 'no_rules_for_bill' => 'This subscription has no rules associated to it.', + 'go_to_asset_accounts' => 'View your asset accounts', + 'go_to_budgets' => 'Go to your budgets', + 'go_to_withdrawals' => 'Go to your withdrawals', + 'clones_journal_x' => 'This transaction is a clone of ":description" (#:id)', + 'go_to_categories' => 'Go to your categories', + 'go_to_bills' => 'Go to your subscriptions', + 'go_to_expense_accounts' => 'See your expense accounts', + 'go_to_revenue_accounts' => 'See your revenue accounts', + 'go_to_piggies' => 'Go to your piggy banks', + 'new_deposit' => 'New deposit', + 'new_transfer' => 'New transfer', + 'new_transfers' => 'New transfer', + 'new_asset_account' => 'New asset account', + 'new_expense_account' => 'New expense account', + 'new_revenue_account' => 'New revenue account', + 'new_liabilities_account' => 'New liability', + 'new_budget' => 'New budget', + 'new_bill' => 'New subscription', + 'block_account_logout' => 'You have been logged out. Blocked accounts cannot use this site. Did you register with a valid email address?', + 'flash_success' => 'Success!', + 'flash_info' => 'Message', + 'flash_warning' => 'Warning!', + 'flash_error' => 'Error!', + 'flash_danger' => 'Danger!', + 'flash_info_multiple' => 'There is one message|There are :count messages', + 'flash_error_multiple' => 'There is one error|There are :count errors', + 'net_worth' => 'Net worth', + 'help_for_this_page' => 'Help for this page', + 'help_for_this_page_body' => 'You can find more information about this page in the documentation.', + 'two_factor_welcome' => 'Hello!', + 'two_factor_enter_code' => 'To continue, please enter your two factor authentication code. Your application can generate it for you.', + 'two_factor_code_here' => 'Enter code here', + 'two_factor_title' => 'Two factor authentication', + 'authenticate' => 'Authenticate', + 'two_factor_forgot_title' => 'Lost two factor authentication', + 'two_factor_forgot' => 'I forgot my two-factor thing.', + 'two_factor_lost_header' => 'Lost your two factor authentication?', + 'two_factor_lost_intro' => 'If you lost your backup codes as well, you have bad luck. This is not something you can fix from the web interface. You have two choices.', + 'two_factor_lost_fix_self' => 'If you run your own instance of Firefly III, read this entry in the FAQ for instructions.', + 'two_factor_lost_fix_owner' => 'Otherwise, email the site owner, :site_owner and ask them to reset your two factor authentication.', + 'mfa_backup_code' => 'You have used a backup code to login to Firefly III. It can\'t be used again, so cross it from your list.', + 'pref_two_factor_new_backup_codes' => 'Get new backup codes', + 'pref_two_factor_backup_code_count' => 'You have :count valid backup code.|You have :count valid backup codes.', + '2fa_i_have_them' => 'I stored them!', + 'warning_much_data' => ':days days of data may take a while to load.', + 'registered' => 'You have registered successfully!', + 'Default asset account' => 'Default asset account', + 'no_budget_pointer' => 'You seem to have no budgets yet. You should create some on the budgets-page. Budgets can help you keep track of expenses.', + 'no_bill_pointer' => 'You seem to have no subscription yet. You should create some on the subscription-page. Subscriptions can help you keep track of expenses.', + 'Savings account' => 'Savings account', + 'Credit card' => 'Credit card', + 'source_accounts' => 'Source account|Source accounts', + 'destination_accounts' => 'Destination account|Destination accounts', + 'user_id_is' => 'Your user id is :user', + 'field_supports_markdown' => 'This field supports Markdown.', + 'need_more_help' => 'If you need more help using Firefly III, please open a ticket on Github.', + 'reenable_intro_text' => 'You can also re-enable the introduction guidance.', + 'intro_boxes_after_refresh' => 'The introduction boxes will reappear when you refresh the page.', + 'show_all_no_filter' => 'Show all transactions without grouping them by date.', + 'expenses_by_category' => 'Expenses by category', + 'expenses_by_budget' => 'Expenses by budget', + 'income_by_category' => 'Income by category', + 'expenses_by_asset_account' => 'Expenses by asset account', + 'expenses_by_expense_account' => 'Expenses by expense account', + 'cannot_redirect_to_account' => 'Firefly III cannot redirect you to the correct page. Apologies.', + 'sum_of_expenses' => 'Sum of expenses', + 'sum_of_income' => 'Sum of income', + 'liabilities' => 'Liabilities', + 'spent_in_specific_budget' => 'Spent in budget ":budget"', + 'spent_in_specific_double' => 'Spent in account ":account"', + 'earned_in_specific_double' => 'Earned in account ":account"', + 'source_account' => 'Source account', + 'source_account_reconciliation' => 'You can\'t edit the source account of a reconciliation transaction.', + 'destination_account' => 'Destination account', + 'destination_account_reconciliation' => 'You can\'t edit the destination account of a reconciliation transaction.', + 'sum_of_expenses_in_budget' => 'Spent total in budget ":budget"', + 'left_in_budget_limit' => 'Left to spend according to budgeting', + 'current_period' => 'Current period', + 'show_the_current_period_and_overview' => 'Show the current period and overview', + 'pref_languages_locale' => 'For a language other than English to work properly, your operating system must be equipped with the correct locale-information. If these are not present, currency data, dates and amounts may be formatted wrong.', + 'budget_in_period' => 'All transactions for budget ":name" between :start and :end in :currency', + 'chart_budget_in_period' => 'Chart for all transactions for budget ":name" between :start and :end in :currency', + 'chart_budget_in_period_only_currency' => 'The amount you budgeted was in :currency, so this chart will only show transactions in :currency.', + 'chart_account_in_period' => 'Chart for all transactions for account ":name" (:balance) between :start and :end', + 'chart_category_in_period' => 'Chart for all transactions for category ":name" between :start and :end', + 'chart_category_all' => 'Chart for all transactions for category ":name"', + 'clone_withdrawal' => 'Clone this withdrawal', + 'clone_deposit' => 'Clone this deposit', + 'clone_transfer' => 'Clone this transfer', + 'multi_select_no_selection' => 'None selected', + 'multi_select_select_all' => 'Select all', + 'multi_select_n_selected' => 'selected', + 'multi_select_all_selected' => 'All selected', + 'multi_select_filter_placeholder' => 'Find..', + 'intro_next_label' => 'Next', + 'intro_prev_label' => 'Previous', + 'intro_skip_label' => 'Skip', + 'intro_done_label' => 'Done', + 'between_dates_breadcrumb' => 'Between :start and :end', + 'all_journals_without_budget' => 'All transactions without a budget', + 'journals_without_budget' => 'Transactions without a budget', + 'all_journals_without_category' => 'All transactions without a category', + 'journals_without_category' => 'Transactions without a category', + 'all_journals_for_account' => 'All transactions for account :name', + 'chart_all_journals_for_account' => 'Chart of all transactions for account :name', + 'journals_in_period_for_account' => 'All transactions for account :name between :start and :end', + 'journals_in_period_for_account_js' => 'All transactions for account {title} between {start} and {end}', + 'transferred' => 'Transferred', + 'all_withdrawal' => 'All expenses', + 'all_transactions' => 'All transactions', + 'title_withdrawal_between' => 'All expenses between :start and :end', + 'all_deposit' => 'All revenue', + 'title_deposit_between' => 'All revenue between :start and :end', + 'all_transfers' => 'All transfers', + 'title_transfers_between' => 'All transfers between :start and :end', + 'all_transfer' => 'All transfers', + 'all_journals_for_tag' => 'All transactions for tag ":tag"', + 'title_transfer_between' => 'All transfers between :start and :end', + 'all_journals_for_category' => 'All transactions for category :name', + 'all_journals_for_budget' => 'All transactions for budget :name', + 'chart_all_journals_for_budget' => 'Chart of all transactions for budget :name', + 'journals_in_period_for_category' => 'All transactions for category :name between :start and :end', + 'journals_in_period_for_tag' => 'All transactions for tag :tag between :start and :end', + 'not_available_demo_user' => 'The feature you try to access is not available to demo users.', + 'exchange_rate_instructions' => 'Asset account "@name" only accepts transactions in @native_currency. If you wish to use @foreign_currency instead, make sure that the amount in @native_currency is known as well:', + 'transfer_exchange_rate_instructions' => 'Source asset account "@source_name" only accepts transactions in @source_currency. Destination asset account "@dest_name" only accepts transactions in @dest_currency. You must provide the transferred amount correctly in both currencies.', + 'transaction_data' => 'Transaction data', + 'invalid_server_configuration' => 'Invalid server configuration', + 'invalid_locale_settings' => 'Firefly III is unable to format monetary amounts because your server is missing the required packages. There are instructions how to do this.', + 'quickswitch' => 'Quickswitch', + 'sign_in_to_start' => 'Sign in to start your session', + 'sign_in' => 'Sign in', + 'register_new_account' => 'Register a new account', + 'forgot_my_password' => 'I forgot my password', + 'problems_with_input' => 'There were some problems with your input.', + 'reset_password' => 'Reset your password', + 'button_reset_password' => 'Reset password', + 'reset_button' => 'Reset', + 'want_to_login' => 'I want to login', + 'login_page_title' => 'Login to Firefly III', + 'register_page_title' => 'Register at Firefly III', + 'forgot_pw_page_title' => 'Forgot your password for Firefly III', + 'reset_pw_page_title' => 'Reset your password for Firefly III', + 'cannot_reset_demo_user' => 'You cannot reset the password of the demo user.', + 'no_att_demo_user' => 'The demo user can\'t upload attachments.', + 'button_register' => 'Register', + 'authorization' => 'Authorization', + 'active_bills_only' => 'active subscription only', + 'active_bills_only_total' => 'all active subscription', + 'active_exp_bills_only' => 'active and expected subscription only', + 'active_exp_bills_only_total' => 'all active expected subscription only', + 'per_period_sum_1D' => 'Expected daily costs', + 'per_period_sum_1W' => 'Expected weekly costs', + 'per_period_sum_1M' => 'Expected monthly costs', + 'per_period_sum_3M' => 'Expected quarterly costs', + 'per_period_sum_6M' => 'Expected half-yearly costs', + 'per_period_sum_1Y' => 'Expected yearly costs', + 'average_per_bill' => 'average per subscription', + 'expected_total' => 'expected total', + 'reconciliation_account_name' => ':name reconciliation (:currency)', + 'saved' => 'Saved', + 'advanced_options' => 'Advanced options', + 'advanced_options_explain' => 'Some pages in Firefly III have advanced options hidden behind this button. This page doesn\'t have anything fancy here, but do check out the others!', + 'here_be_dragons' => 'Hic sunt dracones', // Webhooks - 'webhooks' => 'Webhooks', - 'webhooks_breadcrumb' => 'Webhooks', - 'webhooks_menu_disabled' => 'disabled', - 'no_webhook_messages' => 'There are no webhook messages', - 'webhook_trigger_STORE_TRANSACTION' => 'After transaction creation', - 'webhook_trigger_UPDATE_TRANSACTION' => 'After transaction update', - 'webhook_trigger_DESTROY_TRANSACTION' => 'After transaction delete', - 'webhook_response_TRANSACTIONS' => 'Transaction details', - 'webhook_response_ACCOUNTS' => 'Account details', - 'webhook_response_none_NONE' => 'No details', - 'webhook_delivery_JSON' => 'JSON', - 'inspect' => 'Inspect', - 'create_new_webhook' => 'Create new webhook', - 'webhooks_create_breadcrumb' => 'Create new webhook', - 'webhook_trigger_form_help' => 'Indicate on what event the webhook will trigger', - 'webhook_response_form_help' => 'Indicate what the webhook must submit to the URL.', - 'webhook_delivery_form_help' => 'Which format the webhook must deliver data in.', - 'webhook_active_form_help' => 'The webhook must be active or it won\'t be called.', - 'stored_new_webhook' => 'Stored new webhook ":title"', - 'delete_webhook' => 'Delete webhook', - 'deleted_webhook' => 'Deleted webhook ":title"', - 'edit_webhook' => 'Edit webhook ":title"', - 'updated_webhook' => 'Updated webhook ":title"', - 'edit_webhook_js' => 'Edit webhook "{title}"', - 'show_webhook' => 'Webhook ":title"', - 'webhook_was_triggered' => 'The webhook was triggered on the indicated transaction. Please wait for results to appear.', - 'webhook_messages' => 'Webhook message', - 'view_message' => 'View message', - 'view_attempts' => 'View failed attempts', - 'message_content_title' => 'Webhook message content', - 'message_content_help' => 'This is the content of the message that was sent (or tried) using this webhook.', - 'attempt_content_title' => 'Webhook attempts', - 'attempt_content_help' => 'These are all the unsuccessful attempts of this webhook message to submit to the configured URL. After some time, Firefly III will stop trying.', - 'no_attempts' => 'There are no unsuccessful attempts. That\'s a good thing!', - 'webhook_attempt_at' => 'Attempt at {moment}', - 'logs' => 'Logs', - 'response' => 'Response', - 'visit_webhook_url' => 'Visit webhook URL', - 'reset_webhook_secret' => 'Reset webhook secret', - 'webhook_stored_link' => 'Webhook #{ID} ("{title}") has been stored.', - 'webhook_updated_link' => 'Webhook #{ID} ("{title}") has been updated.', + 'webhooks' => 'Webhooks', + 'webhooks_breadcrumb' => 'Webhooks', + 'webhooks_menu_disabled' => 'disabled', + 'no_webhook_messages' => 'There are no webhook messages', + 'webhook_trigger_STORE_TRANSACTION' => 'After transaction creation', + 'webhook_trigger_UPDATE_TRANSACTION' => 'After transaction update', + 'webhook_trigger_DESTROY_TRANSACTION' => 'After transaction delete', + 'webhook_response_TRANSACTIONS' => 'Transaction details', + 'webhook_response_ACCOUNTS' => 'Account details', + 'webhook_response_none_NONE' => 'No details', + 'webhook_delivery_JSON' => 'JSON', + 'inspect' => 'Inspect', + 'create_new_webhook' => 'Create new webhook', + 'webhooks_create_breadcrumb' => 'Create new webhook', + 'webhook_trigger_form_help' => 'Indicate on what event the webhook will trigger', + 'webhook_response_form_help' => 'Indicate what the webhook must submit to the URL.', + 'webhook_delivery_form_help' => 'Which format the webhook must deliver data in.', + 'webhook_active_form_help' => 'The webhook must be active or it won\'t be called.', + 'stored_new_webhook' => 'Stored new webhook ":title"', + 'delete_webhook' => 'Delete webhook', + 'deleted_webhook' => 'Deleted webhook ":title"', + 'edit_webhook' => 'Edit webhook ":title"', + 'updated_webhook' => 'Updated webhook ":title"', + 'edit_webhook_js' => 'Edit webhook "{title}"', + 'show_webhook' => 'Webhook ":title"', + 'webhook_was_triggered' => 'The webhook was triggered on the indicated transaction. Please wait for results to appear.', + 'webhook_messages' => 'Webhook message', + 'view_message' => 'View message', + 'view_attempts' => 'View failed attempts', + 'message_content_title' => 'Webhook message content', + 'message_content_help' => 'This is the content of the message that was sent (or tried) using this webhook.', + 'attempt_content_title' => 'Webhook attempts', + 'attempt_content_help' => 'These are all the unsuccessful attempts of this webhook message to submit to the configured URL. After some time, Firefly III will stop trying.', + 'no_attempts' => 'There are no unsuccessful attempts. That\'s a good thing!', + 'webhook_attempt_at' => 'Attempt at {moment}', + 'logs' => 'Logs', + 'response' => 'Response', + 'visit_webhook_url' => 'Visit webhook URL', + 'reset_webhook_secret' => 'Reset webhook secret', + 'webhook_stored_link' => 'Webhook #{ID} ("{title}") has been stored.', + 'webhook_updated_link' => 'Webhook #{ID} ("{title}") has been updated.', // API access - 'authorization_request' => 'Firefly III v:version Authorization Request', - 'authorization_request_intro' => 'Application ":client" is requesting permission to access your financial administration. Would you like to authorize :client to access these records?', - 'authorization_request_site' => 'You will be redirected to :url which will then be able to access your Firefly III data.', - 'authorization_request_invalid' => 'This access request is invalid. Please never follow this link again.', - 'scopes_will_be_able' => 'This application will be able to:', - 'button_authorize' => 'Authorize', - 'none_in_select_list' => '(none)', - 'no_piggy_bank' => '(no piggy bank)', - 'name_in_currency' => ':name in :currency', - 'paid_in_currency' => 'Paid in :currency', - 'unpaid_in_currency' => 'Unpaid in :currency', - 'is_alpha_warning' => 'You are running an ALPHA version. Be wary of bugs and issues.', - 'is_beta_warning' => 'You are running an BETA version. Be wary of bugs and issues.', - 'all_destination_accounts' => 'Destination accounts', - 'all_source_accounts' => 'Source accounts', - 'back_to_index' => 'Back to the index', - 'cant_logout_guard' => 'Firefly III can\'t log you out.', - 'internal_reference' => 'Internal reference', + 'authorization_request' => 'Firefly III v:version Authorization Request', + 'authorization_request_intro' => 'Application ":client" is requesting permission to access your financial administration. Would you like to authorize :client to access these records?', + 'authorization_request_site' => 'You will be redirected to :url which will then be able to access your Firefly III data.', + 'authorization_request_invalid' => 'This access request is invalid. Please never follow this link again.', + 'scopes_will_be_able' => 'This application will be able to:', + 'button_authorize' => 'Authorize', + 'none_in_select_list' => '(none)', + 'no_piggy_bank' => '(no piggy bank)', + 'name_in_currency' => ':name in :currency', + 'paid_in_currency' => 'Paid in :currency', + 'unpaid_in_currency' => 'Unpaid in :currency', + 'is_alpha_warning' => 'You are running an ALPHA version. Be wary of bugs and issues.', + 'is_beta_warning' => 'You are running an BETA version. Be wary of bugs and issues.', + 'all_destination_accounts' => 'Destination accounts', + 'all_source_accounts' => 'Source accounts', + 'back_to_index' => 'Back to the index', + 'cant_logout_guard' => 'Firefly III can\'t log you out.', + 'internal_reference' => 'Internal reference', // check for updates: - 'update_check_title' => 'Check for updates', - 'admin_update_check_title' => 'Automatically check for update', - 'admin_update_check_explain' => 'Firefly III can check for updates automatically. When you enable this setting, it will contact the Firefly III update server to see if a new version of Firefly III is available. When it is, you will get a notification. You can test this notification using the button on the right. Please indicate below if you want Firefly III to check for updates.', - 'check_for_updates_permission' => 'Firefly III can check for updates, but it needs your permission to do so. Please go to the administration to indicate if you would like this feature to be enabled.', - 'updates_ask_me_later' => 'Ask me later', - 'updates_do_not_check' => 'Do not check for updates', - 'updates_enable_check' => 'Enable the check for updates', - 'admin_update_check_now_title' => 'Check for updates now', - 'admin_update_check_now_explain' => 'If you press the button, Firefly III will see if your current version is the latest.', - 'check_for_updates_button' => 'Check now!', - 'update_new_version_alert' => 'A new version of Firefly III is available. You are running :your_version, the latest version is :new_version which was released on :date.', - 'update_version_beta' => 'This version is a BETA version. You may run into issues.', - 'update_version_alpha' => 'This version is a ALPHA version. You may run into issues.', - 'update_current_dev_older' => 'You are running development release ":version", which is older than the latest release :new_version. Please update!', - 'update_current_dev_newer' => 'You are running development release ":version", which is newer than the latest release :new_version.', - 'update_current_version_alert' => 'You are running :version, which is the latest available release.', - 'update_newer_version_alert' => 'You are running :your_version, which is newer than the latest release, :new_version.', - 'update_check_error' => 'An error occurred while checking for updates: :error', - 'unknown_error' => 'Unknown error. Sorry about that.', - 'disabled_but_check' => 'You disabled update checking. So don\'t forget to check for updates yourself every now and then. Thank you!', - 'admin_update_channel_title' => 'Update channel', - 'admin_update_channel_explain' => 'Firefly III has three update "channels" which determine how ahead of the curve you are in terms of features, enhancements and bugs. Use the "beta" channel if you\'re adventurous and the "alpha" when you like to live life dangerously.', - 'update_channel_stable' => 'Stable. Everything should work as expected.', - 'update_channel_beta' => 'Beta. New features but things may be broken.', - 'update_channel_alpha' => 'Alpha. We throw stuff in, and use whatever sticks.', + 'update_check_title' => 'Check for updates', + 'admin_update_check_title' => 'Automatically check for update', + 'admin_update_check_explain' => 'Firefly III can check for updates automatically. When you enable this setting, it will contact the Firefly III update server to see if a new version of Firefly III is available. When it is, you will get a notification. You can test this notification using the button on the right. Please indicate below if you want Firefly III to check for updates.', + 'check_for_updates_permission' => 'Firefly III can check for updates, but it needs your permission to do so. Please go to the administration to indicate if you would like this feature to be enabled.', + 'updates_ask_me_later' => 'Ask me later', + 'updates_do_not_check' => 'Do not check for updates', + 'updates_enable_check' => 'Enable the check for updates', + 'admin_update_check_now_title' => 'Check for updates now', + 'admin_update_check_now_explain' => 'If you press the button, Firefly III will see if your current version is the latest.', + 'check_for_updates_button' => 'Check now!', + 'update_new_version_alert' => 'A new version of Firefly III is available. You are running :your_version, the latest version is :new_version which was released on :date.', + 'update_version_beta' => 'This version is a BETA version. You may run into issues.', + 'update_version_alpha' => 'This version is a ALPHA version. You may run into issues.', + 'update_current_dev_older' => 'You are running development release ":version", which is older than the latest release :new_version. Please update!', + 'update_current_dev_newer' => 'You are running development release ":version", which is newer than the latest release :new_version.', + 'update_current_version_alert' => 'You are running :version, which is the latest available release.', + 'update_newer_version_alert' => 'You are running :your_version, which is newer than the latest release, :new_version.', + 'update_check_error' => 'An error occurred while checking for updates: :error', + 'unknown_error' => 'Unknown error. Sorry about that.', + 'disabled_but_check' => 'You disabled update checking. So don\'t forget to check for updates yourself every now and then. Thank you!', + 'admin_update_channel_title' => 'Update channel', + 'admin_update_channel_explain' => 'Firefly III has three update "channels" which determine how ahead of the curve you are in terms of features, enhancements and bugs. Use the "beta" channel if you\'re adventurous and the "alpha" when you like to live life dangerously.', + 'update_channel_stable' => 'Stable. Everything should work as expected.', + 'update_channel_beta' => 'Beta. New features but things may be broken.', + 'update_channel_alpha' => 'Alpha. We throw stuff in, and use whatever sticks.', // search - 'search' => 'Search', - 'search_query' => 'Query', - 'search_found_transactions' => 'Firefly III found :count transaction in :time seconds.|Firefly III found :count transactions in :time seconds.', - 'search_found_more_transactions' => 'Firefly III found more than :count transactions in :time seconds.', - 'search_for_query' => 'Firefly III is searching for transactions with all of these words in them: :query', - 'invalid_operators_list' => 'These search parameters are not valid and have been ignored.', + 'search' => 'Search', + 'search_query' => 'Query', + 'search_found_transactions' => 'Firefly III found :count transaction in :time seconds.|Firefly III found :count transactions in :time seconds.', + 'search_found_more_transactions' => 'Firefly III found more than :count transactions in :time seconds.', + 'search_for_query' => 'Firefly III is searching for transactions with all of these words in them: :query', + 'invalid_operators_list' => 'These search parameters are not valid and have been ignored.', // old // Ignore this comment - 'search_modifier_date_on' => 'Transaction date is ":value"', - 'search_modifier_not_date_on' => 'Transaction date is not ":value"', - 'search_modifier_reconciled' => 'Transaction is reconciled', - 'search_modifier_not_reconciled' => 'Transaction is not reconciled', - 'search_modifier_id' => 'Transaction ID is ":value"', - 'search_modifier_not_id' => 'Transaction ID is not ":value"', - 'search_modifier_date_before' => 'Transaction date is before or on ":value"', - 'search_modifier_date_after' => 'Transaction date is after or on ":value"', - 'search_modifier_external_id_is' => 'External ID is ":value"', - 'search_modifier_not_external_id_is' => 'External ID is not ":value"', - 'search_modifier_no_external_url' => 'The transaction has no external URL', - 'search_modifier_no_external_id' => 'The transaction has no external ID', - 'search_modifier_not_any_external_url' => 'The transaction has no external URL', - 'search_modifier_not_any_external_id' => 'The transaction has no external ID', - 'search_modifier_any_external_url' => 'The transaction must have a (any) external URL', - 'search_modifier_any_external_id' => 'The transaction must have a (any) external ID', - 'search_modifier_not_no_external_url' => 'The transaction must have a (any) external URL', - 'search_modifier_not_no_external_id' => 'The transaction must have a (any) external ID', - 'search_modifier_internal_reference_is' => 'Internal reference is ":value"', - 'search_modifier_not_internal_reference_is' => 'Internal reference is not ":value"', - 'search_modifier_description_starts' => 'Description starts with ":value"', - 'search_modifier_not_description_starts' => 'Description does not start with ":value"', - 'search_modifier_description_ends' => 'Description ends on ":value"', - 'search_modifier_not_description_ends' => 'Description does not end on ":value"', - 'search_modifier_description_contains' => 'Description contains ":value"', - 'search_modifier_not_description_contains' => 'Description does not contain ":value"', - 'search_modifier_description_is' => 'Description is exactly ":value"', - 'search_modifier_not_description_is' => 'Description is exactly not ":value"', - 'search_modifier_currency_is' => 'Transaction (foreign) currency is ":value"', - 'search_modifier_not_currency_is' => 'Transaction (foreign) currency is not ":value"', - 'search_modifier_foreign_currency_is' => 'Transaction foreign currency is ":value"', - 'search_modifier_not_foreign_currency_is' => 'Transaction foreign currency is not ":value"', - 'search_modifier_has_attachments' => 'The transaction must have an attachment', - 'search_modifier_has_no_category' => 'The transaction must have no category', - 'search_modifier_not_has_no_category' => 'The transaction must have a (any) category', - 'search_modifier_not_has_any_category' => 'The transaction must have no category', - 'search_modifier_has_any_category' => 'The transaction must have a (any) category', - 'search_modifier_has_no_budget' => 'The transaction must have no budget', - 'search_modifier_not_has_any_budget' => 'The transaction must have no budget', - 'search_modifier_has_any_budget' => 'The transaction must have a (any) budget', - 'search_modifier_not_has_no_budget' => 'The transaction must have a (any) budget', - 'search_modifier_has_no_bill' => 'The transaction must have no bill', - 'search_modifier_not_has_no_bill' => 'The transaction must have a (any) bill', - 'search_modifier_has_any_bill' => 'The transaction must have a (any) bill', - 'search_modifier_not_has_any_bill' => 'The transaction must have no bill', - 'search_modifier_has_no_tag' => 'The transaction must have no tags', - 'search_modifier_not_has_any_tag' => 'The transaction must have no tags', - 'search_modifier_not_has_no_tag' => 'The transaction must have a (any) tag', - 'search_modifier_has_any_tag' => 'The transaction must have a (any) tag', - 'search_modifier_notes_contains' => 'The transaction notes contain ":value"', - 'search_modifier_not_notes_contains' => 'The transaction notes do not contain ":value"', - 'search_modifier_notes_starts' => 'The transaction notes start with ":value"', - 'search_modifier_not_notes_starts' => 'The transaction notes do not start with ":value"', - 'search_modifier_notes_ends' => 'The transaction notes end with ":value"', - 'search_modifier_not_notes_ends' => 'The transaction notes do not end with ":value"', - 'search_modifier_notes_is' => 'The transaction notes are exactly ":value"', - 'search_modifier_not_notes_is' => 'The transaction notes are exactly not ":value"', - 'search_modifier_no_notes' => 'The transaction has no notes', - 'search_modifier_not_no_notes' => 'The transaction must have notes', - 'search_modifier_any_notes' => 'The transaction must have notes', - 'search_modifier_not_any_notes' => 'The transaction has no notes', - 'search_modifier_amount_is' => 'Amount is exactly :value', - 'search_modifier_not_amount_is' => 'Amount is not :value', - 'search_modifier_amount_less' => 'Amount is less than or equal to :value', - 'search_modifier_not_amount_more' => 'Amount is less than or equal to :value', - 'search_modifier_amount_more' => 'Amount is more than or equal to :value', - 'search_modifier_not_amount_less' => 'Amount is more than or equal to :value', - 'search_modifier_source_account_is' => 'Source account name is exactly ":value"', - 'search_modifier_not_source_account_is' => 'Source account name is not ":value"', - 'search_modifier_source_account_contains' => 'Source account name contains ":value"', - 'search_modifier_not_source_account_contains' => 'Source account name does not contain ":value"', - 'search_modifier_source_account_starts' => 'Source account name starts with ":value"', - 'search_modifier_not_source_account_starts' => 'Source account name does not start with ":value"', - 'search_modifier_source_account_ends' => 'Source account name ends with ":value"', - 'search_modifier_not_source_account_ends' => 'Source account name does not end with ":value"', - 'search_modifier_source_account_id' => 'Source account ID is :value', - 'search_modifier_not_source_account_id' => 'Source account ID is not :value', - 'search_modifier_source_account_nr_is' => 'Source account number (IBAN) is ":value"', - 'search_modifier_not_source_account_nr_is' => 'Source account number (IBAN) is not ":value"', - 'search_modifier_source_account_nr_contains' => 'Source account number (IBAN) contains ":value"', - 'search_modifier_not_source_account_nr_contains' => 'Source account number (IBAN) does not contain ":value"', - 'search_modifier_source_account_nr_starts' => 'Source account number (IBAN) starts with ":value"', - 'search_modifier_not_source_account_nr_starts' => 'Source account number (IBAN) does not start with ":value"', - 'search_modifier_source_account_nr_ends' => 'Source account number (IBAN) ends on ":value"', - 'search_modifier_not_source_account_nr_ends' => 'Source account number (IBAN) does not end on ":value"', - 'search_modifier_destination_account_is' => 'Destination account name is exactly ":value"', - 'search_modifier_not_destination_account_is' => 'Destination account name is not ":value"', - 'search_modifier_destination_account_contains' => 'Destination account name contains ":value"', - 'search_modifier_not_destination_account_contains' => 'Destination account name does not contain ":value"', - 'search_modifier_destination_account_starts' => 'Destination account name starts with ":value"', - 'search_modifier_not_destination_account_starts' => 'Destination account name does not start with ":value"', - 'search_modifier_destination_account_ends' => 'Destination account name ends on ":value"', - 'search_modifier_not_destination_account_ends' => 'Destination account name does not end on ":value"', - 'search_modifier_destination_account_id' => 'Destination account ID is :value', - 'search_modifier_not_destination_account_id' => 'Destination account ID is not :value', - 'search_modifier_destination_is_cash' => 'Destination account is the "(cash)" account', - 'search_modifier_not_destination_is_cash' => 'Destination account is not the "(cash)" account', - 'search_modifier_source_is_cash' => 'Source account is the "(cash)" account', - 'search_modifier_not_source_is_cash' => 'Source account is not the "(cash)" account', - 'search_modifier_destination_account_nr_is' => 'Destination account number (IBAN) is ":value"', - 'search_modifier_not_destination_account_nr_is' => 'Destination account number (IBAN) is ":value"', - 'search_modifier_destination_account_nr_contains' => 'Destination account number (IBAN) contains ":value"', - 'search_modifier_not_destination_account_nr_contains' => 'Destination account number (IBAN) does not contain ":value"', - 'search_modifier_destination_account_nr_starts' => 'Destination account number (IBAN) starts with ":value"', - 'search_modifier_not_destination_account_nr_starts' => 'Destination account number (IBAN) does not start with ":value"', - 'search_modifier_destination_account_nr_ends' => 'Destination account number (IBAN) ends with ":value"', - 'search_modifier_not_destination_account_nr_ends' => 'Destination account number (IBAN) does not end with ":value"', - 'search_modifier_account_id' => 'Source or destination account ID\'s is/are: :value', - 'search_modifier_not_account_id' => 'Source or destination account ID\'s is/are not: :value', - 'search_modifier_category_is' => 'Category is ":value"', - 'search_modifier_not_category_is' => 'Category is not ":value"', - 'search_modifier_budget_is' => 'Budget is ":value"', - 'search_modifier_not_budget_is' => 'Budget is not ":value"', - 'search_modifier_bill_is' => 'Bill is ":value"', - 'search_modifier_not_bill_is' => 'Bill is not ":value"', - 'search_modifier_transaction_type' => 'Transaction type is ":value"', - 'search_modifier_not_transaction_type' => 'Transaction type is not ":value"', - 'search_modifier_tag_is' => 'Tag is ":value"', - 'search_modifier_tag_contains' => 'Tag contains ":value"', - 'search_modifier_not_tag_contains' => 'Tag does not contain ":value"', - 'search_modifier_tag_ends' => 'Tag ends with ":value"', - 'search_modifier_tag_starts' => 'Tag starts with ":value"', - 'search_modifier_not_tag_is' => 'No tag is ":value"', - 'search_modifier_date_on_year' => 'Transaction is in year ":value"', - 'search_modifier_not_date_on_year' => 'Transaction is not in year ":value"', - 'search_modifier_date_on_month' => 'Transaction is in month ":value"', - 'search_modifier_not_date_on_month' => 'Transaction is not in month ":value"', - 'search_modifier_date_on_day' => 'Transaction is on day of month ":value"', - 'search_modifier_not_date_on_day' => 'Transaction is not on day of month ":value"', - 'search_modifier_date_before_year' => 'Transaction is before or in year ":value"', - 'search_modifier_date_before_month' => 'Transaction is before or in month ":value"', - 'search_modifier_date_before_day' => 'Transaction is before or on day of month ":value"', - 'search_modifier_date_after_year' => 'Transaction is in or after year ":value"', - 'search_modifier_date_after_month' => 'Transaction is in or after month ":value"', - 'search_modifier_date_after_day' => 'Transaction is after or on day of month ":value"', + 'search_modifier_date_on' => 'Transaction date is ":value"', + 'search_modifier_not_date_on' => 'Transaction date is not ":value"', + 'search_modifier_reconciled' => 'Transaction is reconciled', + 'search_modifier_not_reconciled' => 'Transaction is not reconciled', + 'search_modifier_id' => 'Transaction ID is ":value"', + 'search_modifier_not_id' => 'Transaction ID is not ":value"', + 'search_modifier_date_before' => 'Transaction date is before or on ":value"', + 'search_modifier_date_after' => 'Transaction date is after or on ":value"', + 'search_modifier_external_id_is' => 'External ID is ":value"', + 'search_modifier_not_external_id_is' => 'External ID is not ":value"', + 'search_modifier_no_external_url' => 'The transaction has no external URL', + 'search_modifier_no_external_id' => 'The transaction has no external ID', + 'search_modifier_not_any_external_url' => 'The transaction has no external URL', + 'search_modifier_not_any_external_id' => 'The transaction has no external ID', + 'search_modifier_any_external_url' => 'The transaction must have a (any) external URL', + 'search_modifier_any_external_id' => 'The transaction must have a (any) external ID', + 'search_modifier_not_no_external_url' => 'The transaction must have a (any) external URL', + 'search_modifier_not_no_external_id' => 'The transaction must have a (any) external ID', + 'search_modifier_internal_reference_is' => 'Internal reference is ":value"', + 'search_modifier_not_internal_reference_is' => 'Internal reference is not ":value"', + 'search_modifier_description_starts' => 'Description starts with ":value"', + 'search_modifier_not_description_starts' => 'Description does not start with ":value"', + 'search_modifier_description_ends' => 'Description ends on ":value"', + 'search_modifier_not_description_ends' => 'Description does not end on ":value"', + 'search_modifier_description_contains' => 'Description contains ":value"', + 'search_modifier_not_description_contains' => 'Description does not contain ":value"', + 'search_modifier_description_is' => 'Description is exactly ":value"', + 'search_modifier_not_description_is' => 'Description is exactly not ":value"', + 'search_modifier_currency_is' => 'Transaction (foreign) currency is ":value"', + 'search_modifier_not_currency_is' => 'Transaction (foreign) currency is not ":value"', + 'search_modifier_foreign_currency_is' => 'Transaction foreign currency is ":value"', + 'search_modifier_not_foreign_currency_is' => 'Transaction foreign currency is not ":value"', + 'search_modifier_has_attachments' => 'The transaction must have an attachment', + 'search_modifier_has_no_category' => 'The transaction must have no category', + 'search_modifier_not_has_no_category' => 'The transaction must have a (any) category', + 'search_modifier_not_has_any_category' => 'The transaction must have no category', + 'search_modifier_has_any_category' => 'The transaction must have a (any) category', + 'search_modifier_has_no_budget' => 'The transaction must have no budget', + 'search_modifier_not_has_any_budget' => 'The transaction must have no budget', + 'search_modifier_has_any_budget' => 'The transaction must have a (any) budget', + 'search_modifier_not_has_no_budget' => 'The transaction must have a (any) budget', + 'search_modifier_has_no_bill' => 'The transaction must have no subscription', + 'search_modifier_not_has_no_bill' => 'The transaction must have a (any) subscription', + 'search_modifier_has_any_bill' => 'The transaction must have a (any) subscription', + 'search_modifier_not_has_any_bill' => 'The transaction must have no subscription', + 'search_modifier_has_no_tag' => 'The transaction must have no tags', + 'search_modifier_not_has_any_tag' => 'The transaction must have no tags', + 'search_modifier_not_has_no_tag' => 'The transaction must have a (any) tag', + 'search_modifier_has_any_tag' => 'The transaction must have a (any) tag', + 'search_modifier_notes_contains' => 'The transaction notes contain ":value"', + 'search_modifier_not_notes_contains' => 'The transaction notes do not contain ":value"', + 'search_modifier_notes_starts' => 'The transaction notes start with ":value"', + 'search_modifier_not_notes_starts' => 'The transaction notes do not start with ":value"', + 'search_modifier_notes_ends' => 'The transaction notes end with ":value"', + 'search_modifier_not_notes_ends' => 'The transaction notes do not end with ":value"', + 'search_modifier_notes_is' => 'The transaction notes are exactly ":value"', + 'search_modifier_not_notes_is' => 'The transaction notes are exactly not ":value"', + 'search_modifier_no_notes' => 'The transaction has no notes', + 'search_modifier_not_no_notes' => 'The transaction must have notes', + 'search_modifier_any_notes' => 'The transaction must have notes', + 'search_modifier_not_any_notes' => 'The transaction has no notes', + 'search_modifier_amount_is' => 'Amount is exactly :value', + 'search_modifier_not_amount_is' => 'Amount is not :value', + 'search_modifier_amount_less' => 'Amount is less than or equal to :value', + 'search_modifier_not_amount_more' => 'Amount is less than or equal to :value', + 'search_modifier_amount_more' => 'Amount is more than or equal to :value', + 'search_modifier_not_amount_less' => 'Amount is more than or equal to :value', + 'search_modifier_source_account_is' => 'Source account name is exactly ":value"', + 'search_modifier_not_source_account_is' => 'Source account name is not ":value"', + 'search_modifier_source_account_contains' => 'Source account name contains ":value"', + 'search_modifier_not_source_account_contains' => 'Source account name does not contain ":value"', + 'search_modifier_source_account_starts' => 'Source account name starts with ":value"', + 'search_modifier_not_source_account_starts' => 'Source account name does not start with ":value"', + 'search_modifier_source_account_ends' => 'Source account name ends with ":value"', + 'search_modifier_not_source_account_ends' => 'Source account name does not end with ":value"', + 'search_modifier_source_account_id' => 'Source account ID is :value', + 'search_modifier_not_source_account_id' => 'Source account ID is not :value', + 'search_modifier_source_account_nr_is' => 'Source account number (IBAN) is ":value"', + 'search_modifier_not_source_account_nr_is' => 'Source account number (IBAN) is not ":value"', + 'search_modifier_source_account_nr_contains' => 'Source account number (IBAN) contains ":value"', + 'search_modifier_not_source_account_nr_contains' => 'Source account number (IBAN) does not contain ":value"', + 'search_modifier_source_account_nr_starts' => 'Source account number (IBAN) starts with ":value"', + 'search_modifier_not_source_account_nr_starts' => 'Source account number (IBAN) does not start with ":value"', + 'search_modifier_source_account_nr_ends' => 'Source account number (IBAN) ends on ":value"', + 'search_modifier_not_source_account_nr_ends' => 'Source account number (IBAN) does not end on ":value"', + 'search_modifier_destination_account_is' => 'Destination account name is exactly ":value"', + 'search_modifier_not_destination_account_is' => 'Destination account name is not ":value"', + 'search_modifier_destination_account_contains' => 'Destination account name contains ":value"', + 'search_modifier_not_destination_account_contains' => 'Destination account name does not contain ":value"', + 'search_modifier_destination_account_starts' => 'Destination account name starts with ":value"', + 'search_modifier_not_destination_account_starts' => 'Destination account name does not start with ":value"', + 'search_modifier_destination_account_ends' => 'Destination account name ends on ":value"', + 'search_modifier_not_destination_account_ends' => 'Destination account name does not end on ":value"', + 'search_modifier_destination_account_id' => 'Destination account ID is :value', + 'search_modifier_not_destination_account_id' => 'Destination account ID is not :value', + 'search_modifier_destination_is_cash' => 'Destination account is the "(cash)" account', + 'search_modifier_not_destination_is_cash' => 'Destination account is not the "(cash)" account', + 'search_modifier_source_is_cash' => 'Source account is the "(cash)" account', + 'search_modifier_not_source_is_cash' => 'Source account is not the "(cash)" account', + 'search_modifier_destination_account_nr_is' => 'Destination account number (IBAN) is ":value"', + 'search_modifier_not_destination_account_nr_is' => 'Destination account number (IBAN) is ":value"', + 'search_modifier_destination_account_nr_contains' => 'Destination account number (IBAN) contains ":value"', + 'search_modifier_not_destination_account_nr_contains' => 'Destination account number (IBAN) does not contain ":value"', + 'search_modifier_destination_account_nr_starts' => 'Destination account number (IBAN) starts with ":value"', + 'search_modifier_not_destination_account_nr_starts' => 'Destination account number (IBAN) does not start with ":value"', + 'search_modifier_destination_account_nr_ends' => 'Destination account number (IBAN) ends with ":value"', + 'search_modifier_not_destination_account_nr_ends' => 'Destination account number (IBAN) does not end with ":value"', + 'search_modifier_account_id' => 'Source or destination account ID\'s is/are: :value', + 'search_modifier_not_account_id' => 'Source or destination account ID\'s is/are not: :value', + 'search_modifier_category_is' => 'Category is ":value"', + 'search_modifier_not_category_is' => 'Category is not ":value"', + 'search_modifier_budget_is' => 'Budget is ":value"', + 'search_modifier_not_budget_is' => 'Budget is not ":value"', + 'search_modifier_bill_is' => 'Subscription is ":value"', + 'search_modifier_not_bill_is' => 'Subscription is not ":value"', + 'search_modifier_transaction_type' => 'Transaction type is ":value"', + 'search_modifier_not_transaction_type' => 'Transaction type is not ":value"', + 'search_modifier_tag_is' => 'Tag is ":value"', + 'search_modifier_tag_contains' => 'Tag contains ":value"', + 'search_modifier_not_tag_contains' => 'Tag does not contain ":value"', + 'search_modifier_tag_ends' => 'Tag ends with ":value"', + 'search_modifier_tag_starts' => 'Tag starts with ":value"', + 'search_modifier_not_tag_is' => 'No tag is ":value"', + 'search_modifier_date_on_year' => 'Transaction is in year ":value"', + 'search_modifier_not_date_on_year' => 'Transaction is not in year ":value"', + 'search_modifier_date_on_month' => 'Transaction is in month ":value"', + 'search_modifier_not_date_on_month' => 'Transaction is not in month ":value"', + 'search_modifier_date_on_day' => 'Transaction is on day of month ":value"', + 'search_modifier_not_date_on_day' => 'Transaction is not on day of month ":value"', + 'search_modifier_date_before_year' => 'Transaction is before or in year ":value"', + 'search_modifier_date_before_month' => 'Transaction is before or in month ":value"', + 'search_modifier_date_before_day' => 'Transaction is before or on day of month ":value"', + 'search_modifier_date_after_year' => 'Transaction is in or after year ":value"', + 'search_modifier_date_after_month' => 'Transaction is in or after month ":value"', + 'search_modifier_date_after_day' => 'Transaction is after or on day of month ":value"', // new - 'search_modifier_tag_is_not' => 'No tag is ":value"', - 'search_modifier_not_tag_is_not' => 'Tag is ":value"', - 'search_modifier_account_is' => 'Either account is ":value"', - 'search_modifier_not_account_is' => 'Neither account is ":value"', - 'search_modifier_account_contains' => 'Either account contains ":value"', - 'search_modifier_not_account_contains' => 'Neither account contains ":value"', - 'search_modifier_account_ends' => 'Either account ends with ":value"', - 'search_modifier_not_account_ends' => 'Neither account ends with ":value"', - 'search_modifier_account_starts' => 'Either account starts with ":value"', - 'search_modifier_not_account_starts' => 'Neither account starts with ":value"', - 'search_modifier_account_nr_is' => 'Either account number / IBAN is ":value"', - 'search_modifier_not_account_nr_is' => 'Neither account number / IBAN is ":value"', - 'search_modifier_account_nr_contains' => 'Either account number / IBAN contains ":value"', - 'search_modifier_not_account_nr_contains' => 'Neither account number / IBAN contains ":value"', - 'search_modifier_account_nr_ends' => 'Either account number / IBAN ends with ":value"', - 'search_modifier_not_account_nr_ends' => 'Neither account number / IBAN ends with ":value"', - 'search_modifier_account_nr_starts' => 'Either account number / IBAN starts with ":value"', - 'search_modifier_not_account_nr_starts' => 'Neither account number / IBAN starts with ":value"', - 'search_modifier_category_contains' => 'Category contains ":value"', - 'search_modifier_not_category_contains' => 'Category does not contain ":value"', - 'search_modifier_category_ends' => 'Category ends on ":value"', - 'search_modifier_not_category_ends' => 'Category does not end on ":value"', - 'search_modifier_category_starts' => 'Category starts with ":value"', - 'search_modifier_not_category_starts' => 'Category does not start with ":value"', - 'search_modifier_budget_contains' => 'Budget contains ":value"', - 'search_modifier_not_budget_contains' => 'Budget does not contain ":value"', - 'search_modifier_budget_ends' => 'Budget ends with ":value"', - 'search_modifier_not_budget_ends' => 'Budget does not end on ":value"', - 'search_modifier_budget_starts' => 'Budget starts with ":value"', - 'search_modifier_not_budget_starts' => 'Budget does not start with ":value"', - 'search_modifier_bill_contains' => 'Bill contains ":value"', - 'search_modifier_not_bill_contains' => 'Bill does not contain ":value"', - 'search_modifier_bill_ends' => 'Bill ends with ":value"', - 'search_modifier_not_bill_ends' => 'Bill does not end on ":value"', - 'search_modifier_bill_starts' => 'Bill starts with ":value"', - 'search_modifier_not_bill_starts' => 'Bill does not start with ":value"', - 'search_modifier_external_id_contains' => 'External ID contains ":value"', - 'search_modifier_not_external_id_contains' => 'External ID does not contain ":value"', - 'search_modifier_external_id_ends' => 'External ID ends with ":value"', - 'search_modifier_not_external_id_ends' => 'External ID does not end with ":value"', - 'search_modifier_external_id_starts' => 'External ID starts with ":value"', - 'search_modifier_not_external_id_starts' => 'External ID does not start with ":value"', - 'search_modifier_internal_reference_contains' => 'Internal reference contains ":value"', - 'search_modifier_not_internal_reference_contains' => 'Internal reference does not contain ":value"', - 'search_modifier_internal_reference_ends' => 'Internal reference ends with ":value"', - 'search_modifier_internal_reference_starts' => 'Internal reference starts with ":value"', - 'search_modifier_not_internal_reference_ends' => 'Internal reference does not end with ":value"', - 'search_modifier_not_internal_reference_starts' => 'Internal reference does not start with ":value"', - 'search_modifier_external_url_is' => 'External URL is ":value"', - 'search_modifier_not_external_url_is' => 'External URL is not ":value"', - 'search_modifier_external_url_contains' => 'External URL contains ":value"', - 'search_modifier_not_external_url_contains' => 'External URL does not contain ":value"', - 'search_modifier_external_url_ends' => 'External URL ends with ":value"', - 'search_modifier_not_external_url_ends' => 'External URL does not end with ":value"', - 'search_modifier_external_url_starts' => 'External URL starts with ":value"', - 'search_modifier_not_external_url_starts' => 'External URL does not start with ":value"', - 'search_modifier_has_no_attachments' => 'Transaction has no attachments', - 'search_modifier_not_has_no_attachments' => 'Transaction has attachments', - 'search_modifier_not_has_attachments' => 'Transaction has no attachments', - 'search_modifier_account_is_cash' => 'Either account is the "(cash)" account.', - 'search_modifier_not_account_is_cash' => 'Neither account is the "(cash)" account.', - 'search_modifier_journal_id' => 'The journal ID is ":value"', - 'search_modifier_not_journal_id' => 'The journal ID is not ":value"', - 'search_modifier_recurrence_id' => 'The recurring transaction ID is ":value"', - 'search_modifier_not_recurrence_id' => 'The recurring transaction ID is not ":value"', - 'search_modifier_foreign_amount_is' => 'The foreign amount is ":value"', - 'search_modifier_not_foreign_amount_is' => 'The foreign amount is not ":value"', - 'search_modifier_foreign_amount_less' => 'The foreign amount is less than ":value"', - 'search_modifier_not_foreign_amount_more' => 'The foreign amount is less than ":value"', - 'search_modifier_not_foreign_amount_less' => 'The foreign amount is more than ":value"', - 'search_modifier_foreign_amount_more' => 'The foreign amount is more than ":value"', - 'search_modifier_exists' => 'Transaction exists (any transaction)', - 'search_modifier_not_exists' => 'Transaction does not exist (no transaction)', + 'search_modifier_tag_is_not' => 'No tag is ":value"', + 'search_modifier_not_tag_is_not' => 'Tag is ":value"', + 'search_modifier_account_is' => 'Either account is ":value"', + 'search_modifier_not_account_is' => 'Neither account is ":value"', + 'search_modifier_account_contains' => 'Either account contains ":value"', + 'search_modifier_not_account_contains' => 'Neither account contains ":value"', + 'search_modifier_account_ends' => 'Either account ends with ":value"', + 'search_modifier_not_account_ends' => 'Neither account ends with ":value"', + 'search_modifier_account_starts' => 'Either account starts with ":value"', + 'search_modifier_not_account_starts' => 'Neither account starts with ":value"', + 'search_modifier_account_nr_is' => 'Either account number / IBAN is ":value"', + 'search_modifier_not_account_nr_is' => 'Neither account number / IBAN is ":value"', + 'search_modifier_account_nr_contains' => 'Either account number / IBAN contains ":value"', + 'search_modifier_not_account_nr_contains' => 'Neither account number / IBAN contains ":value"', + 'search_modifier_account_nr_ends' => 'Either account number / IBAN ends with ":value"', + 'search_modifier_not_account_nr_ends' => 'Neither account number / IBAN ends with ":value"', + 'search_modifier_account_nr_starts' => 'Either account number / IBAN starts with ":value"', + 'search_modifier_not_account_nr_starts' => 'Neither account number / IBAN starts with ":value"', + 'search_modifier_category_contains' => 'Category contains ":value"', + 'search_modifier_not_category_contains' => 'Category does not contain ":value"', + 'search_modifier_category_ends' => 'Category ends on ":value"', + 'search_modifier_not_category_ends' => 'Category does not end on ":value"', + 'search_modifier_category_starts' => 'Category starts with ":value"', + 'search_modifier_not_category_starts' => 'Category does not start with ":value"', + 'search_modifier_budget_contains' => 'Budget contains ":value"', + 'search_modifier_not_budget_contains' => 'Budget does not contain ":value"', + 'search_modifier_budget_ends' => 'Budget ends with ":value"', + 'search_modifier_not_budget_ends' => 'Budget does not end on ":value"', + 'search_modifier_budget_starts' => 'Budget starts with ":value"', + 'search_modifier_not_budget_starts' => 'Budget does not start with ":value"', + 'search_modifier_bill_contains' => 'Subscription contains ":value"', + 'search_modifier_not_bill_contains' => 'Subscription does not contain ":value"', + 'search_modifier_bill_ends' => 'Subscription ends with ":value"', + 'search_modifier_not_bill_ends' => 'Subscription does not end on ":value"', + 'search_modifier_bill_starts' => 'Subscription starts with ":value"', + 'search_modifier_not_bill_starts' => 'Subscription does not start with ":value"', + 'search_modifier_external_id_contains' => 'External ID contains ":value"', + 'search_modifier_not_external_id_contains' => 'External ID does not contain ":value"', + 'search_modifier_external_id_ends' => 'External ID ends with ":value"', + 'search_modifier_not_external_id_ends' => 'External ID does not end with ":value"', + 'search_modifier_external_id_starts' => 'External ID starts with ":value"', + 'search_modifier_not_external_id_starts' => 'External ID does not start with ":value"', + 'search_modifier_internal_reference_contains' => 'Internal reference contains ":value"', + 'search_modifier_not_internal_reference_contains' => 'Internal reference does not contain ":value"', + 'search_modifier_internal_reference_ends' => 'Internal reference ends with ":value"', + 'search_modifier_internal_reference_starts' => 'Internal reference starts with ":value"', + 'search_modifier_not_internal_reference_ends' => 'Internal reference does not end with ":value"', + 'search_modifier_not_internal_reference_starts' => 'Internal reference does not start with ":value"', + 'search_modifier_external_url_is' => 'External URL is ":value"', + 'search_modifier_not_external_url_is' => 'External URL is not ":value"', + 'search_modifier_external_url_contains' => 'External URL contains ":value"', + 'search_modifier_not_external_url_contains' => 'External URL does not contain ":value"', + 'search_modifier_external_url_ends' => 'External URL ends with ":value"', + 'search_modifier_not_external_url_ends' => 'External URL does not end with ":value"', + 'search_modifier_external_url_starts' => 'External URL starts with ":value"', + 'search_modifier_not_external_url_starts' => 'External URL does not start with ":value"', + 'search_modifier_has_no_attachments' => 'Transaction has no attachments', + 'search_modifier_not_has_no_attachments' => 'Transaction has attachments', + 'search_modifier_not_has_attachments' => 'Transaction has no attachments', + 'search_modifier_account_is_cash' => 'Either account is the "(cash)" account.', + 'search_modifier_not_account_is_cash' => 'Neither account is the "(cash)" account.', + 'search_modifier_journal_id' => 'The journal ID is ":value"', + 'search_modifier_not_journal_id' => 'The journal ID is not ":value"', + 'search_modifier_recurrence_id' => 'The recurring transaction ID is ":value"', + 'search_modifier_not_recurrence_id' => 'The recurring transaction ID is not ":value"', + 'search_modifier_foreign_amount_is' => 'The foreign amount is ":value"', + 'search_modifier_not_foreign_amount_is' => 'The foreign amount is not ":value"', + 'search_modifier_foreign_amount_less' => 'The foreign amount is less than ":value"', + 'search_modifier_not_foreign_amount_more' => 'The foreign amount is less than ":value"', + 'search_modifier_not_foreign_amount_less' => 'The foreign amount is more than ":value"', + 'search_modifier_foreign_amount_more' => 'The foreign amount is more than ":value"', + 'search_modifier_exists' => 'Transaction exists (any transaction)', + 'search_modifier_not_exists' => 'Transaction does not exist (no transaction)', // date fields - 'search_modifier_interest_date_on' => 'Transaction interest date is ":value"', - 'search_modifier_not_interest_date_on' => 'Transaction interest date is not ":value"', - 'search_modifier_interest_date_on_year' => 'Transaction interest date is in year ":value"', - 'search_modifier_not_interest_date_on_year' => 'Transaction interest date is not in year ":value"', - 'search_modifier_interest_date_on_month' => 'Transaction interest date is in month ":value"', - 'search_modifier_not_interest_date_on_month' => 'Transaction interest date is not in month ":value"', - 'search_modifier_interest_date_on_day' => 'Transaction interest date is on day of month ":value"', - 'search_modifier_not_interest_date_on_day' => 'Transaction interest date is not on day of month ":value"', - 'search_modifier_interest_date_before_year' => 'Transaction interest date is before or in year ":value"', - 'search_modifier_interest_date_before_month' => 'Transaction interest date is before or in month ":value"', - 'search_modifier_interest_date_before_day' => 'Transaction interest date is before or on day of month ":value"', - 'search_modifier_interest_date_after_year' => 'Transaction interest date is after or in year ":value"', - 'search_modifier_interest_date_after_month' => 'Transaction interest date is after or in month ":value"', - 'search_modifier_interest_date_after_day' => 'Transaction interest date is after or on day of month ":value"', - 'search_modifier_book_date_on_year' => 'Transaction book date is in year ":value"', - 'search_modifier_book_date_on_month' => 'Transaction book date is in month ":value"', - 'search_modifier_book_date_on_day' => 'Transaction book date is on day of month ":value"', - 'search_modifier_not_book_date_on_year' => 'Transaction book date is not in year ":value"', - 'search_modifier_not_book_date_on_month' => 'Transaction book date is not in month ":value"', - 'search_modifier_not_book_date_on_day' => 'Transaction book date is not on day of month ":value"', - 'search_modifier_book_date_before_year' => 'Transaction book date is before or in year ":value"', - 'search_modifier_book_date_before_month' => 'Transaction book date is before or in month ":value"', - 'search_modifier_book_date_before_day' => 'Transaction book date is before or on day of month ":value"', - 'search_modifier_book_date_after_year' => 'Transaction book date is after or in year ":value"', - 'search_modifier_book_date_after_month' => 'Transaction book date is after or in month ":value"', - 'search_modifier_book_date_after_day' => 'Transaction book date is after or on day of month ":value"', - 'search_modifier_process_date_on_year' => 'Transaction process date is in year ":value"', - 'search_modifier_process_date_on_month' => 'Transaction process date is in month ":value"', - 'search_modifier_process_date_on_day' => 'Transaction process date is on day of month ":value"', - 'search_modifier_not_process_date_on_year' => 'Transaction process date is not in year ":value"', - 'search_modifier_not_process_date_on_month' => 'Transaction process date is not in month ":value"', - 'search_modifier_not_process_date_on_day' => 'Transaction process date is not on day of month ":value"', - 'search_modifier_process_date_before_year' => 'Transaction process date is before or in year ":value"', - 'search_modifier_process_date_before_month' => 'Transaction process date is before or in month ":value"', - 'search_modifier_process_date_before_day' => 'Transaction process date is before or on day of month ":value"', - 'search_modifier_process_date_after_year' => 'Transaction process date is after or in year ":value"', - 'search_modifier_process_date_after_month' => 'Transaction process date is after or in month ":value"', - 'search_modifier_process_date_after_day' => 'Transaction process date is after or on day of month ":value"', - 'search_modifier_due_date_on_year' => 'Transaction due date is in year ":value"', - 'search_modifier_due_date_on_month' => 'Transaction due date is in month ":value"', - 'search_modifier_due_date_on_day' => 'Transaction due date is on day of month ":value"', - 'search_modifier_not_due_date_on_year' => 'Transaction due date is not in year ":value"', - 'search_modifier_not_due_date_on_month' => 'Transaction due date is not in month ":value"', - 'search_modifier_not_due_date_on_day' => 'Transaction due date is not on day of month ":value"', - 'search_modifier_due_date_before_year' => 'Transaction due date is before or in year ":value"', - 'search_modifier_due_date_before_month' => 'Transaction due date is before or in month ":value"', - 'search_modifier_due_date_before_day' => 'Transaction due date is before or on day of month ":value"', - 'search_modifier_due_date_after_year' => 'Transaction due date is after or in year ":value"', - 'search_modifier_due_date_after_month' => 'Transaction due date is after or in month ":value"', - 'search_modifier_due_date_after_day' => 'Transaction due date is after or on day of month ":value"', - 'search_modifier_payment_date_on_year' => 'Transaction payment date is in year ":value"', - 'search_modifier_payment_date_on_month' => 'Transaction payment date is in month ":value"', - 'search_modifier_payment_date_on_day' => 'Transaction payment date is on day of month ":value"', - 'search_modifier_not_payment_date_on_year' => 'Transaction payment date is not in year ":value"', - 'search_modifier_not_payment_date_on_month' => 'Transaction payment date is not in month ":value"', - 'search_modifier_not_payment_date_on_day' => 'Transaction payment date is not on day of month ":value"', - 'search_modifier_payment_date_before_year' => 'Transaction payment date is before or in year ":value"', - 'search_modifier_payment_date_before_month' => 'Transaction payment date is before or in month ":value"', - 'search_modifier_payment_date_before_day' => 'Transaction payment date is before or on day of month ":value"', - 'search_modifier_payment_date_after_year' => 'Transaction payment date is after or in year ":value"', - 'search_modifier_payment_date_after_month' => 'Transaction payment date is after or in month ":value"', - 'search_modifier_payment_date_after_day' => 'Transaction payment date is after or on day of month ":value"', - 'search_modifier_invoice_date_on_year' => 'Transaction invoice date is in year ":value"', - 'search_modifier_invoice_date_on_month' => 'Transaction invoice date is in month ":value"', - 'search_modifier_invoice_date_on_day' => 'Transaction invoice date is on day of month ":value"', - 'search_modifier_not_invoice_date_on_year' => 'Transaction invoice date is not in year ":value"', - 'search_modifier_not_invoice_date_on_month' => 'Transaction invoice date is not in month ":value"', - 'search_modifier_not_invoice_date_on_day' => 'Transaction invoice date is not on day of month ":value"', - 'search_modifier_invoice_date_before_year' => 'Transaction invoice date is before or in year ":value"', - 'search_modifier_invoice_date_before_month' => 'Transaction invoice date is before or in month ":value"', - 'search_modifier_invoice_date_before_day' => 'Transaction invoice date is before or on day of month ":value"', - 'search_modifier_invoice_date_after_year' => 'Transaction invoice date is after or in year ":value"', - 'search_modifier_invoice_date_after_month' => 'Transaction invoice date is after or in month ":value"', - 'search_modifier_invoice_date_after_day' => 'Transaction invoice date is after or on day of month ":value"', + 'search_modifier_interest_date_on' => 'Transaction interest date is ":value"', + 'search_modifier_not_interest_date_on' => 'Transaction interest date is not ":value"', + 'search_modifier_interest_date_on_year' => 'Transaction interest date is in year ":value"', + 'search_modifier_not_interest_date_on_year' => 'Transaction interest date is not in year ":value"', + 'search_modifier_interest_date_on_month' => 'Transaction interest date is in month ":value"', + 'search_modifier_not_interest_date_on_month' => 'Transaction interest date is not in month ":value"', + 'search_modifier_interest_date_on_day' => 'Transaction interest date is on day of month ":value"', + 'search_modifier_not_interest_date_on_day' => 'Transaction interest date is not on day of month ":value"', + 'search_modifier_interest_date_before_year' => 'Transaction interest date is before or in year ":value"', + 'search_modifier_interest_date_before_month' => 'Transaction interest date is before or in month ":value"', + 'search_modifier_interest_date_before_day' => 'Transaction interest date is before or on day of month ":value"', + 'search_modifier_interest_date_after_year' => 'Transaction interest date is after or in year ":value"', + 'search_modifier_interest_date_after_month' => 'Transaction interest date is after or in month ":value"', + 'search_modifier_interest_date_after_day' => 'Transaction interest date is after or on day of month ":value"', + 'search_modifier_book_date_on_year' => 'Transaction book date is in year ":value"', + 'search_modifier_book_date_on_month' => 'Transaction book date is in month ":value"', + 'search_modifier_book_date_on_day' => 'Transaction book date is on day of month ":value"', + 'search_modifier_not_book_date_on_year' => 'Transaction book date is not in year ":value"', + 'search_modifier_not_book_date_on_month' => 'Transaction book date is not in month ":value"', + 'search_modifier_not_book_date_on_day' => 'Transaction book date is not on day of month ":value"', + 'search_modifier_book_date_before_year' => 'Transaction book date is before or in year ":value"', + 'search_modifier_book_date_before_month' => 'Transaction book date is before or in month ":value"', + 'search_modifier_book_date_before_day' => 'Transaction book date is before or on day of month ":value"', + 'search_modifier_book_date_after_year' => 'Transaction book date is after or in year ":value"', + 'search_modifier_book_date_after_month' => 'Transaction book date is after or in month ":value"', + 'search_modifier_book_date_after_day' => 'Transaction book date is after or on day of month ":value"', + 'search_modifier_process_date_on_year' => 'Transaction process date is in year ":value"', + 'search_modifier_process_date_on_month' => 'Transaction process date is in month ":value"', + 'search_modifier_process_date_on_day' => 'Transaction process date is on day of month ":value"', + 'search_modifier_not_process_date_on_year' => 'Transaction process date is not in year ":value"', + 'search_modifier_not_process_date_on_month' => 'Transaction process date is not in month ":value"', + 'search_modifier_not_process_date_on_day' => 'Transaction process date is not on day of month ":value"', + 'search_modifier_process_date_before_year' => 'Transaction process date is before or in year ":value"', + 'search_modifier_process_date_before_month' => 'Transaction process date is before or in month ":value"', + 'search_modifier_process_date_before_day' => 'Transaction process date is before or on day of month ":value"', + 'search_modifier_process_date_after_year' => 'Transaction process date is after or in year ":value"', + 'search_modifier_process_date_after_month' => 'Transaction process date is after or in month ":value"', + 'search_modifier_process_date_after_day' => 'Transaction process date is after or on day of month ":value"', + 'search_modifier_due_date_on_year' => 'Transaction due date is in year ":value"', + 'search_modifier_due_date_on_month' => 'Transaction due date is in month ":value"', + 'search_modifier_due_date_on_day' => 'Transaction due date is on day of month ":value"', + 'search_modifier_not_due_date_on_year' => 'Transaction due date is not in year ":value"', + 'search_modifier_not_due_date_on_month' => 'Transaction due date is not in month ":value"', + 'search_modifier_not_due_date_on_day' => 'Transaction due date is not on day of month ":value"', + 'search_modifier_due_date_before_year' => 'Transaction due date is before or in year ":value"', + 'search_modifier_due_date_before_month' => 'Transaction due date is before or in month ":value"', + 'search_modifier_due_date_before_day' => 'Transaction due date is before or on day of month ":value"', + 'search_modifier_due_date_after_year' => 'Transaction due date is after or in year ":value"', + 'search_modifier_due_date_after_month' => 'Transaction due date is after or in month ":value"', + 'search_modifier_due_date_after_day' => 'Transaction due date is after or on day of month ":value"', + 'search_modifier_payment_date_on_year' => 'Transaction payment date is in year ":value"', + 'search_modifier_payment_date_on_month' => 'Transaction payment date is in month ":value"', + 'search_modifier_payment_date_on_day' => 'Transaction payment date is on day of month ":value"', + 'search_modifier_not_payment_date_on_year' => 'Transaction payment date is not in year ":value"', + 'search_modifier_not_payment_date_on_month' => 'Transaction payment date is not in month ":value"', + 'search_modifier_not_payment_date_on_day' => 'Transaction payment date is not on day of month ":value"', + 'search_modifier_payment_date_before_year' => 'Transaction payment date is before or in year ":value"', + 'search_modifier_payment_date_before_month' => 'Transaction payment date is before or in month ":value"', + 'search_modifier_payment_date_before_day' => 'Transaction payment date is before or on day of month ":value"', + 'search_modifier_payment_date_after_year' => 'Transaction payment date is after or in year ":value"', + 'search_modifier_payment_date_after_month' => 'Transaction payment date is after or in month ":value"', + 'search_modifier_payment_date_after_day' => 'Transaction payment date is after or on day of month ":value"', + 'search_modifier_invoice_date_on_year' => 'Transaction invoice date is in year ":value"', + 'search_modifier_invoice_date_on_month' => 'Transaction invoice date is in month ":value"', + 'search_modifier_invoice_date_on_day' => 'Transaction invoice date is on day of month ":value"', + 'search_modifier_not_invoice_date_on_year' => 'Transaction invoice date is not in year ":value"', + 'search_modifier_not_invoice_date_on_month' => 'Transaction invoice date is not in month ":value"', + 'search_modifier_not_invoice_date_on_day' => 'Transaction invoice date is not on day of month ":value"', + 'search_modifier_invoice_date_before_year' => 'Transaction invoice date is before or in year ":value"', + 'search_modifier_invoice_date_before_month' => 'Transaction invoice date is before or in month ":value"', + 'search_modifier_invoice_date_before_day' => 'Transaction invoice date is before or on day of month ":value"', + 'search_modifier_invoice_date_after_year' => 'Transaction invoice date is after or in year ":value"', + 'search_modifier_invoice_date_after_month' => 'Transaction invoice date is after or in month ":value"', + 'search_modifier_invoice_date_after_day' => 'Transaction invoice date is after or on day of month ":value"', // other dates - 'search_modifier_updated_at_on_year' => 'Transaction was last updated in year ":value"', - 'search_modifier_updated_at_on_month' => 'Transaction was last updated in month ":value"', - 'search_modifier_updated_at_on_day' => 'Transaction was last updated on day of month ":value"', - 'search_modifier_not_updated_at_on_year' => 'Transaction was not last updated in year ":value"', - 'search_modifier_not_updated_at_on_month' => 'Transaction was not last updated in month ":value"', - 'search_modifier_not_updated_at_on_day' => 'Transaction was not last updated on day of month ":value"', - 'search_modifier_updated_at_before_year' => 'Transaction was last updated in or before year ":value"', - 'search_modifier_updated_at_before_month' => 'Transaction was last updated in or before month ":value"', - 'search_modifier_updated_at_before_day' => 'Transaction was last updated on or before day of month ":value"', - 'search_modifier_updated_at_after_year' => 'Transaction was last updated in or after year ":value"', - 'search_modifier_updated_at_after_month' => 'Transaction was last updated in or after month ":value"', - 'search_modifier_updated_at_after_day' => 'Transaction was last updated on or after day of month ":value"', - 'search_modifier_created_at_on_year' => 'Transaction was created in year ":value"', - 'search_modifier_created_at_on_month' => 'Transaction was created in month ":value"', - 'search_modifier_created_at_on_day' => 'Transaction was created on day of month ":value"', - 'search_modifier_not_created_at_on_year' => 'Transaction was not created in year ":value"', - 'search_modifier_not_created_at_on_month' => 'Transaction was not created in month ":value"', - 'search_modifier_not_created_at_on_day' => 'Transaction was not created on day of month ":value"', - 'search_modifier_created_at_before_year' => 'Transaction was created in or before year ":value"', - 'search_modifier_created_at_before_month' => 'Transaction was created in or before month ":value"', - 'search_modifier_created_at_before_day' => 'Transaction was created on or before day of month ":value"', - 'search_modifier_created_at_after_year' => 'Transaction was created in or after year ":value"', - 'search_modifier_created_at_after_month' => 'Transaction was created in or after month ":value"', - 'search_modifier_created_at_after_day' => 'Transaction was created on or after day of month ":value"', - 'search_modifier_interest_date_before' => 'Transaction interest date is on or before ":value"', - 'search_modifier_interest_date_after' => 'Transaction interest date is on or after ":value"', - 'search_modifier_book_date_on' => 'Transaction book date is on ":value"', - 'search_modifier_not_book_date_on' => 'Transaction book date is not on ":value"', - 'search_modifier_book_date_before' => 'Transaction book date is on or before ":value"', - 'search_modifier_book_date_after' => 'Transaction book date is on or after ":value"', - 'search_modifier_process_date_on' => 'Transaction process date is on ":value"', - 'search_modifier_not_process_date_on' => 'Transaction process date is not on ":value"', - 'search_modifier_process_date_before' => 'Transaction process date is on or before ":value"', - 'search_modifier_process_date_after' => 'Transaction process date is on or after ":value"', - 'search_modifier_due_date_on' => 'Transaction due date is on ":value"', - 'search_modifier_not_due_date_on' => 'Transaction due date is not on ":value"', - 'search_modifier_due_date_before' => 'Transaction due date is on or before ":value"', - 'search_modifier_due_date_after' => 'Transaction due date is on or after ":value"', - 'search_modifier_payment_date_on' => 'Transaction payment date is on ":value"', - 'search_modifier_not_payment_date_on' => 'Transaction payment date is not on ":value"', - 'search_modifier_payment_date_before' => 'Transaction payment date is on or before ":value"', - 'search_modifier_payment_date_after' => 'Transaction payment date is on or after ":value"', - 'search_modifier_invoice_date_on' => 'Transaction invoice date is on ":value"', - 'search_modifier_not_invoice_date_on' => 'Transaction invoice date is not on ":value"', - 'search_modifier_invoice_date_before' => 'Transaction invoice date is on or before ":value"', - 'search_modifier_invoice_date_after' => 'Transaction invoice date is on or after ":value"', - 'search_modifier_created_at_on' => 'Transaction was created on ":value"', - 'search_modifier_not_created_at_on' => 'Transaction was not created on ":value"', - 'search_modifier_created_at_before' => 'Transaction was created on or before ":value"', - 'search_modifier_created_at_after' => 'Transaction was created on or after ":value"', - 'search_modifier_updated_at_on' => 'Transaction was updated on ":value"', - 'search_modifier_not_updated_at_on' => 'Transaction was not updated on ":value"', - 'search_modifier_updated_at_before' => 'Transaction was updated on or before ":value"', - 'search_modifier_updated_at_after' => 'Transaction was updated on or after ":value"', + 'search_modifier_updated_at_on_year' => 'Transaction was last updated in year ":value"', + 'search_modifier_updated_at_on_month' => 'Transaction was last updated in month ":value"', + 'search_modifier_updated_at_on_day' => 'Transaction was last updated on day of month ":value"', + 'search_modifier_not_updated_at_on_year' => 'Transaction was not last updated in year ":value"', + 'search_modifier_not_updated_at_on_month' => 'Transaction was not last updated in month ":value"', + 'search_modifier_not_updated_at_on_day' => 'Transaction was not last updated on day of month ":value"', + 'search_modifier_updated_at_before_year' => 'Transaction was last updated in or before year ":value"', + 'search_modifier_updated_at_before_month' => 'Transaction was last updated in or before month ":value"', + 'search_modifier_updated_at_before_day' => 'Transaction was last updated on or before day of month ":value"', + 'search_modifier_updated_at_after_year' => 'Transaction was last updated in or after year ":value"', + 'search_modifier_updated_at_after_month' => 'Transaction was last updated in or after month ":value"', + 'search_modifier_updated_at_after_day' => 'Transaction was last updated on or after day of month ":value"', + 'search_modifier_created_at_on_year' => 'Transaction was created in year ":value"', + 'search_modifier_created_at_on_month' => 'Transaction was created in month ":value"', + 'search_modifier_created_at_on_day' => 'Transaction was created on day of month ":value"', + 'search_modifier_not_created_at_on_year' => 'Transaction was not created in year ":value"', + 'search_modifier_not_created_at_on_month' => 'Transaction was not created in month ":value"', + 'search_modifier_not_created_at_on_day' => 'Transaction was not created on day of month ":value"', + 'search_modifier_created_at_before_year' => 'Transaction was created in or before year ":value"', + 'search_modifier_created_at_before_month' => 'Transaction was created in or before month ":value"', + 'search_modifier_created_at_before_day' => 'Transaction was created on or before day of month ":value"', + 'search_modifier_created_at_after_year' => 'Transaction was created in or after year ":value"', + 'search_modifier_created_at_after_month' => 'Transaction was created in or after month ":value"', + 'search_modifier_created_at_after_day' => 'Transaction was created on or after day of month ":value"', + 'search_modifier_interest_date_before' => 'Transaction interest date is on or before ":value"', + 'search_modifier_interest_date_after' => 'Transaction interest date is on or after ":value"', + 'search_modifier_book_date_on' => 'Transaction book date is on ":value"', + 'search_modifier_not_book_date_on' => 'Transaction book date is not on ":value"', + 'search_modifier_book_date_before' => 'Transaction book date is on or before ":value"', + 'search_modifier_book_date_after' => 'Transaction book date is on or after ":value"', + 'search_modifier_process_date_on' => 'Transaction process date is on ":value"', + 'search_modifier_not_process_date_on' => 'Transaction process date is not on ":value"', + 'search_modifier_process_date_before' => 'Transaction process date is on or before ":value"', + 'search_modifier_process_date_after' => 'Transaction process date is on or after ":value"', + 'search_modifier_due_date_on' => 'Transaction due date is on ":value"', + 'search_modifier_not_due_date_on' => 'Transaction due date is not on ":value"', + 'search_modifier_due_date_before' => 'Transaction due date is on or before ":value"', + 'search_modifier_due_date_after' => 'Transaction due date is on or after ":value"', + 'search_modifier_payment_date_on' => 'Transaction payment date is on ":value"', + 'search_modifier_not_payment_date_on' => 'Transaction payment date is not on ":value"', + 'search_modifier_payment_date_before' => 'Transaction payment date is on or before ":value"', + 'search_modifier_payment_date_after' => 'Transaction payment date is on or after ":value"', + 'search_modifier_invoice_date_on' => 'Transaction invoice date is on ":value"', + 'search_modifier_not_invoice_date_on' => 'Transaction invoice date is not on ":value"', + 'search_modifier_invoice_date_before' => 'Transaction invoice date is on or before ":value"', + 'search_modifier_invoice_date_after' => 'Transaction invoice date is on or after ":value"', + 'search_modifier_created_at_on' => 'Transaction was created on ":value"', + 'search_modifier_not_created_at_on' => 'Transaction was not created on ":value"', + 'search_modifier_created_at_before' => 'Transaction was created on or before ":value"', + 'search_modifier_created_at_after' => 'Transaction was created on or after ":value"', + 'search_modifier_updated_at_on' => 'Transaction was updated on ":value"', + 'search_modifier_not_updated_at_on' => 'Transaction was not updated on ":value"', + 'search_modifier_updated_at_before' => 'Transaction was updated on or before ":value"', + 'search_modifier_updated_at_after' => 'Transaction was updated on or after ":value"', + + 'search_modifier_attachment_name_is' => 'Any attachment\'s name is ":value"', + 'search_modifier_attachment_name_contains' => 'Any attachment\'s name contains ":value"', + 'search_modifier_attachment_name_starts' => 'Any attachment\'s name starts with ":value"', + 'search_modifier_attachment_name_ends' => 'Any attachment\'s name ends with ":value"', + 'search_modifier_attachment_notes_are' => 'Any attachment\'s notes are ":value"', + 'search_modifier_attachment_notes_contains' => 'Any attachment\'s notes contain ":value"', + 'search_modifier_attachment_notes_starts' => 'Any attachment\'s notes start with ":value"', + 'search_modifier_attachment_notes_ends' => 'Any attachment\'s notes end with ":value"', + 'search_modifier_not_attachment_name_is' => 'Any attachment\'s name is not ":value"', + 'search_modifier_not_attachment_name_contains' => 'Any attachment\'s name does not contain ":value"', + 'search_modifier_not_attachment_name_starts' => 'Any attachment\'s name does not start with ":value"', + 'search_modifier_not_attachment_name_ends' => 'Any attachment\'s name does not end with ":value"', + 'search_modifier_not_attachment_notes_are' => 'Any attachment\'s notes are not ":value"', + 'search_modifier_not_attachment_notes_contains' => 'Any attachment\'s notes do not contain ":value"', + 'search_modifier_not_attachment_notes_starts' => 'Any attachment\'s notes start with ":value"', + 'search_modifier_not_attachment_notes_ends' => 'Any attachment\'s notes do not end with ":value"', + 'search_modifier_sepa_ct_is' => 'SEPA CT is ":value"', + 'update_rule_from_query' => 'Update rule ":rule" from search query', + 'create_rule_from_query' => 'Create new rule from search query', + 'rule_from_search_words' => 'The rule engine has a hard time handling ":string". The suggested rule that fits your search query may give different results. Please verify the rule triggers carefully.', + + // more new stuff + 'search_modifier_destination_balance_is' => 'Destination account balance is exactly ":value"', + 'search_modifier_not_destination_balance_is' => 'Destination account balance is NOT exactly ":value"', + 'search_modifier_source_balance_is' => 'Source account balance is exactly ":value"', + 'search_modifier_not_source_balance_is' => 'Source account balance is NOT exactly ":value"', + + 'search_modifier_destination_balance_lte' => 'Destination account balance is less than or equal to ":value"', + 'search_modifier_not_destination_balance_lte' => 'Destination account balance is more than ":value"', + 'search_modifier_source_balance_lte' => 'Source account balance is less than or equal to ":value"', + 'search_modifier_not_source_balance_lte' => 'Source account balance is more than ":value"', + + 'search_modifier_destination_balance_lt' => 'Destination account balance is less than ":value"', + 'search_modifier_not_destination_balance_lt' => 'Destination account balance is more than or equal to ":value"', + 'search_modifier_source_balance_lt' => 'Source account balance is less than ":value"', + 'search_modifier_not_source_balance_lt' => 'Source account balance is more than or equal to ":value"', + + 'search_modifier_destination_balance_gte' => 'Destination account balance is more than or equal to ":value"', + 'search_modifier_not_destination_balance_gte' => 'Destination account balance is less than ":value"', + 'search_modifier_source_balance_gte' => 'Source account balance is more than or equal to ":value"', + 'search_modifier_not_source_balance_gte' => 'Source account balance is less than ":value"', + + 'search_modifier_destination_balance_gt' => 'Destination account balance is more than ":value"', + 'search_modifier_not_destination_balance_gt' => 'Destination account balance is less than or equal to ":value"', + 'search_modifier_source_balance_gt' => 'Source account balance is more than ":value"', + 'search_modifier_not_source_balance_gt' => 'Source account balance is less than or equal to ":value"', - 'search_modifier_attachment_name_is' => 'Any attachment\'s name is ":value"', - 'search_modifier_attachment_name_contains' => 'Any attachment\'s name contains ":value"', - 'search_modifier_attachment_name_starts' => 'Any attachment\'s name starts with ":value"', - 'search_modifier_attachment_name_ends' => 'Any attachment\'s name ends with ":value"', - 'search_modifier_attachment_notes_are' => 'Any attachment\'s notes are ":value"', - 'search_modifier_attachment_notes_contains' => 'Any attachment\'s notes contain ":value"', - 'search_modifier_attachment_notes_starts' => 'Any attachment\'s notes start with ":value"', - 'search_modifier_attachment_notes_ends' => 'Any attachment\'s notes end with ":value"', - 'search_modifier_not_attachment_name_is' => 'Any attachment\'s name is not ":value"', - 'search_modifier_not_attachment_name_contains' => 'Any attachment\'s name does not contain ":value"', - 'search_modifier_not_attachment_name_starts' => 'Any attachment\'s name does not start with ":value"', - 'search_modifier_not_attachment_name_ends' => 'Any attachment\'s name does not end with ":value"', - 'search_modifier_not_attachment_notes_are' => 'Any attachment\'s notes are not ":value"', - 'search_modifier_not_attachment_notes_contains' => 'Any attachment\'s notes do not contain ":value"', - 'search_modifier_not_attachment_notes_starts' => 'Any attachment\'s notes start with ":value"', - 'search_modifier_not_attachment_notes_ends' => 'Any attachment\'s notes do not end with ":value"', - 'search_modifier_sepa_ct_is' => 'SEPA CT is ":value"', - 'update_rule_from_query' => 'Update rule ":rule" from search query', - 'create_rule_from_query' => 'Create new rule from search query', - 'rule_from_search_words' => 'The rule engine has a hard time handling ":string". The suggested rule that fits your search query may give different results. Please verify the rule triggers carefully.', // Ignore this comment // END - 'modifiers_applies_are' => 'The following modifiers are applied to the search as well:', - 'general_search_error' => 'An error occurred while searching. Please check the log files for more information.', - 'search_box' => 'Search', - 'search_box_intro' => 'Welcome to the search function of Firefly III. Enter your search query in the box. Make sure you check out the help file because the search is pretty advanced.', - 'search_error' => 'Error while searching', - 'search_searching' => 'Searching ...', - 'search_results' => 'Search results', + 'modifiers_applies_are' => 'The following modifiers are applied to the search as well:', + 'general_search_error' => 'An error occurred while searching. Please check the log files for more information.', + 'search_box' => 'Search', + 'search_box_intro' => 'Welcome to the search function of Firefly III. Enter your search query in the box. Make sure you check out the help file because the search is pretty advanced.', + 'search_error' => 'Error while searching', + 'search_searching' => 'Searching ...', + 'search_results' => 'Search results', // repeat frequencies: - 'repeat_freq_yearly' => 'yearly', - 'repeat_freq_half-year' => 'every half-year', - 'repeat_freq_quarterly' => 'quarterly', - 'repeat_freq_monthly' => 'monthly', - 'repeat_freq_weekly' => 'weekly', - 'repeat_freq_daily' => 'daily', - 'daily' => 'daily', - 'weekly' => 'weekly', - 'quarterly' => 'quarterly', - 'half-year' => 'every half year', - 'yearly' => 'yearly', + 'repeat_freq_yearly' => 'yearly', + 'repeat_freq_half-year' => 'every half-year', + 'repeat_freq_quarterly' => 'quarterly', + 'repeat_freq_monthly' => 'monthly', + 'repeat_freq_weekly' => 'weekly', + 'repeat_freq_daily' => 'daily', + 'daily' => 'daily', + 'weekly' => 'weekly', + 'quarterly' => 'quarterly', + 'half-year' => 'every half year', + 'yearly' => 'yearly', // rules - 'is_not_rule_trigger' => 'Not', - 'cannot_fire_inactive_rules' => 'You cannot execute inactive rules.', - 'show_triggers' => 'Show triggers', - 'show_actions' => 'Show actions', - 'rules' => 'Rules', - 'rule_name' => 'Name of rule', - 'rule_triggers' => 'Rule triggers when', - 'rule_actions' => 'Rule will', - 'new_rule' => 'New rule', - 'new_rule_group' => 'New rule group', - 'rule_priority_up' => 'Give rule more priority', - 'rule_priority_down' => 'Give rule less priority', - 'make_new_rule_group' => 'Make new rule group', - 'store_new_rule_group' => 'Store new rule group', - 'created_new_rule_group' => 'New rule group ":title" stored!', - 'updated_rule_group' => 'Successfully updated rule group ":title".', - 'edit_rule_group' => 'Edit rule group ":title"', - 'duplicate_rule' => 'Duplicate rule ":title"', - 'rule_copy_of' => 'Copy of ":title"', - 'duplicated_rule' => 'Duplicated rule ":title" into ":newTitle"', - 'delete_rule_group' => 'Delete rule group ":title"', - 'deleted_rule_group' => 'Deleted rule group ":title"', - 'update_rule_group' => 'Update rule group', - 'no_rules_in_group' => 'There are no rules in this group', - 'move_rule_group_up' => 'Move rule group up', - 'move_rule_group_down' => 'Move rule group down', - 'save_rules_by_moving' => 'Save this rule by moving it to another rule group:|Save these rules by moving them to another rule group:', - 'make_new_rule' => 'Make a new rule in rule group ":title"', - 'make_new_rule_no_group' => 'Make a new rule', - 'instructions_rule_from_bill' => 'In order to match transactions to your new bill ":name", Firefly III can create a rule that will automatically be checked against any transactions you store. Please verify the details below and store the rule to have Firefly III automatically match transactions to your new bill.', - 'instructions_rule_from_journal' => 'Create a rule based on one of your transactions. Complement or submit the form below.', - 'rule_is_strict' => 'strict rule', - 'rule_is_not_strict' => 'non-strict rule', - 'rule_help_stop_processing' => 'When you check this box, later rules in this group will not be executed if this particular rule is executed.', - 'rule_help_strict' => 'In strict rules ALL triggers must fire for the action(s) to be executed. In non-strict rules, ANY trigger is enough for the action(s) to be executed.', - 'rule_help_active' => 'Inactive rules will never fire.', - 'stored_new_rule' => 'Stored new rule with title ":title"', - 'deleted_rule' => 'Deleted rule with title ":title"', - 'store_new_rule' => 'Store new rule', - 'updated_rule' => 'Updated rule with title ":title"', - 'default_rule_group_name' => 'Default rules', - 'default_rule_group_description' => 'All your rules not in a particular group.', - 'trigger' => 'Trigger', - 'trigger_value' => 'Trigger on value', - 'stop_processing_other_triggers' => 'Stop processing other triggers', - 'add_rule_trigger' => 'Add new trigger', - 'action' => 'Action', - 'action_value' => 'Action value', - 'stop_executing_other_actions' => 'Stop executing other actions', - 'add_rule_action' => 'Add new action', - 'edit_rule' => 'Edit rule ":title"', - 'delete_rule' => 'Delete rule ":title"', - 'update_rule' => 'Update rule', - 'test_rule_triggers' => 'See matching transactions', - 'warning_no_matching_transactions' => 'No matching transactions found.', - 'warning_no_valid_triggers' => 'No valid triggers provided.', - 'apply_rule_selection' => 'Apply rule ":title" to a selection of your transactions', - 'apply_rule_selection_intro' => 'Rules like ":title" are normally only applied to new or updated transactions, but you can tell Firefly III to run it on a selection of your existing transactions. This can be useful when you have updated a rule and you need the changes to be applied to all of your other transactions.', - 'include_transactions_from_accounts' => 'Include transactions from these accounts', - 'include' => 'Include?', - 'applied_rule_selection' => '{0} No transactions in your selection were changed by rule ":title".|[1] One transaction in your selection was changed by rule ":title".|[2,*] :count transactions in your selection were changed by rule ":title".', - 'execute' => 'Execute', - 'apply_rule_group_selection' => 'Apply rule group ":title" to a selection of your transactions', - 'apply_rule_group_selection_intro' => 'Rule groups like ":title" are normally only applied to new or updated transactions, but you can tell Firefly III to run all the rules in this group on a selection of your existing transactions. This can be useful when you have updated a group of rules and you need the changes to be applied to all of your other transactions.', - 'applied_rule_group_selection' => 'Rule group ":title" has been applied to your selection.', + 'is_not_rule_trigger' => 'Not', + 'cannot_fire_inactive_rules' => 'You cannot execute inactive rules.', + 'show_triggers' => 'Show triggers', + 'show_actions' => 'Show actions', + 'rules' => 'Rules', + 'rule_name' => 'Name of rule', + 'rule_triggers' => 'Rule triggers when', + 'rule_actions' => 'Rule will', + 'new_rule' => 'New rule', + 'new_rule_group' => 'New rule group', + 'rule_priority_up' => 'Give rule more priority', + 'rule_priority_down' => 'Give rule less priority', + 'make_new_rule_group' => 'Make new rule group', + 'store_new_rule_group' => 'Store new rule group', + 'created_new_rule_group' => 'New rule group ":title" stored!', + 'updated_rule_group' => 'Successfully updated rule group ":title".', + 'edit_rule_group' => 'Edit rule group ":title"', + 'duplicate_rule' => 'Duplicate rule ":title"', + 'rule_copy_of' => 'Copy of ":title"', + 'duplicated_rule' => 'Duplicated rule ":title" into ":newTitle"', + 'delete_rule_group' => 'Delete rule group ":title"', + 'deleted_rule_group' => 'Deleted rule group ":title"', + 'update_rule_group' => 'Update rule group', + 'no_rules_in_group' => 'There are no rules in this group', + 'move_rule_group_up' => 'Move rule group up', + 'move_rule_group_down' => 'Move rule group down', + 'save_rules_by_moving' => 'Save this rule by moving it to another rule group:|Save these rules by moving them to another rule group:', + 'make_new_rule' => 'Make a new rule in rule group ":title"', + 'make_new_rule_no_group' => 'Make a new rule', + 'instructions_rule_from_bill' => 'In order to match transactions to your new subscription ":name", Firefly III can create a rule that will automatically be checked against any transactions you store. Please verify the details below and store the rule to have Firefly III automatically match transactions to your new bill.', + 'instructions_rule_from_journal' => 'Create a rule based on one of your transactions. Complement or submit the form below.', + 'rule_is_strict' => 'strict rule', + 'rule_is_not_strict' => 'non-strict rule', + 'rule_help_stop_processing' => 'When you check this box, later rules in this group will not be executed if this particular rule is executed.', + 'rule_help_strict' => 'In strict rules ALL triggers must fire for the action(s) to be executed. In non-strict rules, ANY trigger is enough for the action(s) to be executed.', + 'rule_help_active' => 'Inactive rules will never fire.', + 'stored_new_rule' => 'Stored new rule with title ":title"', + 'deleted_rule' => 'Deleted rule with title ":title"', + 'store_new_rule' => 'Store new rule', + 'updated_rule' => 'Updated rule with title ":title"', + 'default_rule_group_name' => 'Default rules', + 'default_rule_group_description' => 'All your rules not in a particular group.', + 'trigger' => 'Trigger', + 'trigger_value' => 'Trigger on value', + 'stop_processing_other_triggers' => 'Stop processing other triggers', + 'add_rule_trigger' => 'Add new trigger', + 'action' => 'Action', + 'action_value' => 'Action value', + 'stop_executing_other_actions' => 'Stop executing other actions', + 'add_rule_action' => 'Add new action', + 'edit_rule' => 'Edit rule ":title"', + 'delete_rule' => 'Delete rule ":title"', + 'update_rule' => 'Update rule', + 'test_rule_triggers' => 'See matching transactions', + 'warning_no_matching_transactions' => 'No matching transactions found.', + 'warning_no_valid_triggers' => 'No valid triggers provided.', + 'apply_rule_selection' => 'Apply rule ":title" to a selection of your transactions', + 'apply_rule_selection_intro' => 'Rules like ":title" are normally only applied to new or updated transactions, but you can tell Firefly III to run it on a selection of your existing transactions. This can be useful when you have updated a rule and you need the changes to be applied to all of your other transactions.', + 'include_transactions_from_accounts' => 'Include transactions from these accounts', + 'include' => 'Include?', + 'applied_rule_selection' => '{0} No transactions in your selection were changed by rule ":title".|[1] One transaction in your selection was changed by rule ":title".|[2,*] :count transactions in your selection were changed by rule ":title".', + 'execute' => 'Execute', + 'apply_rule_group_selection' => 'Apply rule group ":title" to a selection of your transactions', + 'apply_rule_group_selection_intro' => 'Rule groups like ":title" are normally only applied to new or updated transactions, but you can tell Firefly III to run all the rules in this group on a selection of your existing transactions. This can be useful when you have updated a group of rules and you need the changes to be applied to all of your other transactions.', + 'applied_rule_group_selection' => 'Rule group ":title" has been applied to your selection.', // actions and triggers - 'rule_trigger_store_journal' => 'When a transaction is created', - 'rule_trigger_update_journal' => 'When a transaction is updated', - 'rule_trigger_user_action' => 'User action is ":trigger_value"', + 'rule_trigger_store_journal' => 'When a transaction is created', + 'rule_trigger_update_journal' => 'When a transaction is updated', + 'rule_trigger_manual' => 'Only when user-activated', + 'rule_trigger_user_action' => 'User action is ":trigger_value"', // OLD values (remove non-doubles later): - 'rule_trigger_source_account_starts_choice' => 'Source account name starts with..', - 'rule_trigger_source_account_starts' => 'Source account name starts with ":trigger_value"', - 'rule_trigger_source_account_ends_choice' => 'Source account name ends with..', - 'rule_trigger_source_account_ends' => 'Source account name ends with ":trigger_value"', - 'rule_trigger_source_account_is_choice' => 'Source account name is..', - 'rule_trigger_source_account_is' => 'Source account name is ":trigger_value"', - 'rule_trigger_source_account_contains_choice' => 'Source account name contains..', - 'rule_trigger_source_account_contains' => 'Source account name contains ":trigger_value"', - 'rule_trigger_account_id_choice' => 'Either account ID is exactly..', - 'rule_trigger_account_id' => 'Either account ID is exactly :trigger_value', - 'rule_trigger_source_account_id_choice' => 'Source account ID is exactly..', - 'rule_trigger_source_account_id' => 'Source account ID is exactly :trigger_value', - 'rule_trigger_destination_account_id_choice' => 'Destination account ID is exactly..', - 'rule_trigger_destination_account_id' => 'Destination account ID is exactly :trigger_value', - 'rule_trigger_account_is_cash_choice' => 'Either account is cash', - 'rule_trigger_account_is_cash' => 'Either account is cash', - 'rule_trigger_source_is_cash_choice' => 'Source account is (cash) account', - 'rule_trigger_source_is_cash' => 'Source account is (cash) account', - 'rule_trigger_destination_is_cash_choice' => 'Destination account is (cash) account', - 'rule_trigger_destination_is_cash' => 'Destination account is (cash) account', - 'rule_trigger_source_account_nr_starts_choice' => 'Source account number / IBAN starts with..', - 'rule_trigger_source_account_nr_starts' => 'Source account number / IBAN starts with ":trigger_value"', - 'rule_trigger_source_account_nr_ends_choice' => 'Source account number / IBAN ends with..', - 'rule_trigger_source_account_nr_ends' => 'Source account number / IBAN ends with ":trigger_value"', - 'rule_trigger_source_account_nr_is_choice' => 'Source account number / IBAN is..', - 'rule_trigger_source_account_nr_is' => 'Source account number / IBAN is ":trigger_value"', - 'rule_trigger_source_account_nr_contains_choice' => 'Source account number / IBAN contains..', - 'rule_trigger_source_account_nr_contains' => 'Source account number / IBAN contains ":trigger_value"', - 'rule_trigger_destination_account_starts_choice' => 'Destination account name starts with..', - 'rule_trigger_destination_account_starts' => 'Destination account name starts with ":trigger_value"', - 'rule_trigger_destination_account_ends_choice' => 'Destination account name ends with..', - 'rule_trigger_destination_account_ends' => 'Destination account name ends with ":trigger_value"', - 'rule_trigger_destination_account_is_choice' => 'Destination account name is..', - 'rule_trigger_destination_account_is' => 'Destination account name is ":trigger_value"', - 'rule_trigger_destination_account_contains_choice' => 'Destination account name contains..', - 'rule_trigger_destination_account_contains' => 'Destination account name contains ":trigger_value"', - 'rule_trigger_destination_account_nr_starts_choice' => 'Destination account number / IBAN starts with..', - 'rule_trigger_destination_account_nr_starts' => 'Destination account number / IBAN starts with ":trigger_value"', - 'rule_trigger_destination_account_nr_ends_choice' => 'Destination account number / IBAN ends with..', - 'rule_trigger_destination_account_nr_ends' => 'Destination account number / IBAN ends with ":trigger_value"', - 'rule_trigger_destination_account_nr_is_choice' => 'Destination account number / IBAN is..', - 'rule_trigger_destination_account_nr_is' => 'Destination account number / IBAN is ":trigger_value"', - 'rule_trigger_destination_account_nr_contains_choice' => 'Destination account number / IBAN contains..', - 'rule_trigger_destination_account_nr_contains' => 'Destination account number / IBAN contains ":trigger_value"', - 'rule_trigger_transaction_type_choice' => 'Transaction is of type..', - 'rule_trigger_transaction_type' => 'Transaction is of type ":trigger_value"', - 'rule_trigger_category_is_choice' => 'Category is..', - 'rule_trigger_category_is' => 'Category is ":trigger_value"', - 'rule_trigger_amount_less_choice' => 'Amount is less than or equal to ..', - 'rule_trigger_amount_less' => 'Amount is less than or equal to :trigger_value', - 'rule_trigger_amount_is_choice' => 'Amount is..', - 'rule_trigger_amount_is' => 'Amount is :trigger_value', - 'rule_trigger_amount_more_choice' => 'Amount is more than or equal to..', - 'rule_trigger_amount_more' => 'Amount is more than or equal to :trigger_value', - 'rule_trigger_description_starts_choice' => 'Description starts with..', - 'rule_trigger_description_starts' => 'Description starts with ":trigger_value"', - 'rule_trigger_description_ends_choice' => 'Description ends with..', - 'rule_trigger_description_ends' => 'Description ends with ":trigger_value"', - 'rule_trigger_description_contains_choice' => 'Description contains..', - 'rule_trigger_description_contains' => 'Description contains ":trigger_value"', - 'rule_trigger_description_is_choice' => 'Description is..', - 'rule_trigger_description_is' => 'Description is ":trigger_value"', - 'rule_trigger_date_on_choice' => 'Transaction date is..', - 'rule_trigger_date_on' => 'Transaction date is ":trigger_value"', - 'rule_trigger_date_before_choice' => 'Transaction date is before..', - 'rule_trigger_date_before' => 'Transaction date is before ":trigger_value"', - 'rule_trigger_date_after_choice' => 'Transaction date is after..', - 'rule_trigger_date_after' => 'Transaction date is after ":trigger_value"', - 'rule_trigger_created_at_on_choice' => 'Transaction was made on..', - 'rule_trigger_created_at_on' => 'Transaction was made on ":trigger_value"', - 'rule_trigger_updated_at_on_choice' => 'Transaction was last edited on..', - 'rule_trigger_updated_at_on' => 'Transaction was last edited on ":trigger_value"', - 'rule_trigger_budget_is_choice' => 'Budget is..', - 'rule_trigger_budget_is' => 'Budget is ":trigger_value"', - 'rule_trigger_tag_is_choice' => 'Any tag is..', - 'rule_trigger_tag_is' => 'Any tag is ":trigger_value"', - 'rule_trigger_tag_contains_choice' => 'Any tag contains..', - 'rule_trigger_tag_contains' => 'Any tag contains ":trigger_value"', - 'rule_trigger_tag_ends_choice' => 'Any tag ends with..', - 'rule_trigger_tag_ends' => 'Any tag ends with ":trigger_value"', - 'rule_trigger_tag_starts_choice' => 'Any tag starts with..', - 'rule_trigger_tag_starts' => 'Any tag starts with ":trigger_value"', - 'rule_trigger_currency_is_choice' => 'Transaction currency is..', - 'rule_trigger_currency_is' => 'Transaction currency is ":trigger_value"', - 'rule_trigger_foreign_currency_is_choice' => 'Transaction foreign currency is..', - 'rule_trigger_foreign_currency_is' => 'Transaction foreign currency is ":trigger_value"', - 'rule_trigger_has_attachments_choice' => 'Has any attachments', - 'rule_trigger_has_attachments' => 'Has any attachment(s)', - 'rule_trigger_has_no_category_choice' => 'Has no category', - 'rule_trigger_has_no_category' => 'Transaction has no category', - 'rule_trigger_has_any_category_choice' => 'Has a (any) category', - 'rule_trigger_has_any_category' => 'Transaction has a (any) category', - 'rule_trigger_has_no_budget_choice' => 'Has no budget', - 'rule_trigger_has_no_budget' => 'Transaction has no budget', - 'rule_trigger_has_any_budget_choice' => 'Has a (any) budget', - 'rule_trigger_has_any_budget' => 'Transaction has a (any) budget', - 'rule_trigger_has_no_bill_choice' => 'Has no bill', - 'rule_trigger_has_no_bill' => 'Transaction has no bill', - 'rule_trigger_has_any_bill_choice' => 'Has a (any) bill', - 'rule_trigger_has_any_bill' => 'Transaction has a (any) bill', - 'rule_trigger_has_no_tag_choice' => 'Has no tag(s)', - 'rule_trigger_has_no_tag' => 'Transaction has no tag(s)', - 'rule_trigger_has_any_tag_choice' => 'Has one or more (any) tags', - 'rule_trigger_has_any_tag' => 'Transaction has one or more (any) tags', - 'rule_trigger_any_notes_choice' => 'Has (any) notes', - 'rule_trigger_any_notes' => 'Transaction has (any) notes', - 'rule_trigger_no_notes_choice' => 'Has no notes', - 'rule_trigger_no_notes' => 'Transaction has no notes', - 'rule_trigger_notes_is_choice' => 'Notes are..', - 'rule_trigger_notes_is' => 'Notes are ":trigger_value"', - 'rule_trigger_notes_contains_choice' => 'Notes contain..', - 'rule_trigger_notes_contains' => 'Notes contain ":trigger_value"', - 'rule_trigger_notes_starts_choice' => 'Notes start with..', - 'rule_trigger_notes_starts' => 'Notes start with ":trigger_value"', - 'rule_trigger_notes_ends_choice' => 'Notes end with..', - 'rule_trigger_notes_ends' => 'Notes end with ":trigger_value"', - 'rule_trigger_bill_is_choice' => 'Bill is..', - 'rule_trigger_bill_is' => 'Bill is ":trigger_value"', - 'rule_trigger_external_id_is_choice' => 'External ID is..', - 'rule_trigger_external_id_is' => 'External ID is ":trigger_value"', - 'rule_trigger_internal_reference_is_choice' => 'Internal reference is..', - 'rule_trigger_internal_reference_is' => 'Internal reference is ":trigger_value"', - 'rule_trigger_journal_id_choice' => 'Transaction journal ID is..', - 'rule_trigger_journal_id' => 'Transaction journal ID is ":trigger_value"', - 'rule_trigger_any_external_url' => 'Transaction has an (any) external URL', - 'rule_trigger_any_external_url_choice' => 'Transaction has an (any) external URL', - 'rule_trigger_any_external_id' => 'Transaction has an (any) external ID', - 'rule_trigger_any_external_id_choice' => 'Transaction has an (any) external ID', - 'rule_trigger_no_external_url_choice' => 'Transaction has no external URL', - 'rule_trigger_no_external_url' => 'Transaction has no external URL', - 'rule_trigger_no_external_id_choice' => 'Transaction has no external ID', - 'rule_trigger_no_external_id' => 'Transaction has no external ID', - 'rule_trigger_id_choice' => 'Transaction ID is..', - 'rule_trigger_id' => 'Transaction ID is ":trigger_value"', - 'rule_trigger_sepa_ct_is_choice' => 'SEPA CT is..', - 'rule_trigger_sepa_ct_is' => 'SEPA CT is ":trigger_value"', + 'rule_trigger_source_account_starts_choice' => 'Source account name starts with..', + 'rule_trigger_source_account_starts' => 'Source account name starts with ":trigger_value"', + 'rule_trigger_source_account_ends_choice' => 'Source account name ends with..', + 'rule_trigger_source_account_ends' => 'Source account name ends with ":trigger_value"', + 'rule_trigger_source_account_is_choice' => 'Source account name is..', + 'rule_trigger_source_account_is' => 'Source account name is ":trigger_value"', + 'rule_trigger_source_account_contains_choice' => 'Source account name contains..', + 'rule_trigger_source_account_contains' => 'Source account name contains ":trigger_value"', + 'rule_trigger_account_id_choice' => 'Either account ID is exactly..', + 'rule_trigger_account_id' => 'Either account ID is exactly :trigger_value', + 'rule_trigger_source_account_id_choice' => 'Source account ID is exactly..', + 'rule_trigger_source_account_id' => 'Source account ID is exactly :trigger_value', + 'rule_trigger_destination_account_id_choice' => 'Destination account ID is exactly..', + 'rule_trigger_destination_account_id' => 'Destination account ID is exactly :trigger_value', + 'rule_trigger_account_is_cash_choice' => 'Either account is cash', + 'rule_trigger_account_is_cash' => 'Either account is cash', + 'rule_trigger_source_is_cash_choice' => 'Source account is (cash) account', + 'rule_trigger_source_is_cash' => 'Source account is (cash) account', + 'rule_trigger_destination_is_cash_choice' => 'Destination account is (cash) account', + 'rule_trigger_destination_is_cash' => 'Destination account is (cash) account', + 'rule_trigger_source_account_nr_starts_choice' => 'Source account number / IBAN starts with..', + 'rule_trigger_source_account_nr_starts' => 'Source account number / IBAN starts with ":trigger_value"', + 'rule_trigger_source_account_nr_ends_choice' => 'Source account number / IBAN ends with..', + 'rule_trigger_source_account_nr_ends' => 'Source account number / IBAN ends with ":trigger_value"', + 'rule_trigger_source_account_nr_is_choice' => 'Source account number / IBAN is..', + 'rule_trigger_source_account_nr_is' => 'Source account number / IBAN is ":trigger_value"', + 'rule_trigger_source_account_nr_contains_choice' => 'Source account number / IBAN contains..', + 'rule_trigger_source_account_nr_contains' => 'Source account number / IBAN contains ":trigger_value"', + 'rule_trigger_destination_account_starts_choice' => 'Destination account name starts with..', + 'rule_trigger_destination_account_starts' => 'Destination account name starts with ":trigger_value"', + 'rule_trigger_destination_account_ends_choice' => 'Destination account name ends with..', + 'rule_trigger_destination_account_ends' => 'Destination account name ends with ":trigger_value"', + 'rule_trigger_destination_account_is_choice' => 'Destination account name is..', + 'rule_trigger_destination_account_is' => 'Destination account name is ":trigger_value"', + 'rule_trigger_destination_account_contains_choice' => 'Destination account name contains..', + 'rule_trigger_destination_account_contains' => 'Destination account name contains ":trigger_value"', + 'rule_trigger_destination_account_nr_starts_choice' => 'Destination account number / IBAN starts with..', + 'rule_trigger_destination_account_nr_starts' => 'Destination account number / IBAN starts with ":trigger_value"', + 'rule_trigger_destination_account_nr_ends_choice' => 'Destination account number / IBAN ends with..', + 'rule_trigger_destination_account_nr_ends' => 'Destination account number / IBAN ends with ":trigger_value"', + 'rule_trigger_destination_account_nr_is_choice' => 'Destination account number / IBAN is..', + 'rule_trigger_destination_account_nr_is' => 'Destination account number / IBAN is ":trigger_value"', + 'rule_trigger_destination_account_nr_contains_choice' => 'Destination account number / IBAN contains..', + 'rule_trigger_destination_account_nr_contains' => 'Destination account number / IBAN contains ":trigger_value"', + 'rule_trigger_transaction_type_choice' => 'Transaction is of type..', + 'rule_trigger_transaction_type' => 'Transaction is of type ":trigger_value"', + 'rule_trigger_category_is_choice' => 'Category is..', + 'rule_trigger_category_is' => 'Category is ":trigger_value"', + 'rule_trigger_amount_less_choice' => 'Amount is less than or equal to ..', + 'rule_trigger_amount_less' => 'Amount is less than or equal to :trigger_value', + 'rule_trigger_amount_is_choice' => 'Amount is..', + 'rule_trigger_amount_is' => 'Amount is :trigger_value', + 'rule_trigger_amount_more_choice' => 'Amount is more than or equal to..', + 'rule_trigger_amount_more' => 'Amount is more than or equal to :trigger_value', + 'rule_trigger_description_starts_choice' => 'Description starts with..', + 'rule_trigger_description_starts' => 'Description starts with ":trigger_value"', + 'rule_trigger_description_ends_choice' => 'Description ends with..', + 'rule_trigger_description_ends' => 'Description ends with ":trigger_value"', + 'rule_trigger_description_contains_choice' => 'Description contains..', + 'rule_trigger_description_contains' => 'Description contains ":trigger_value"', + 'rule_trigger_description_is_choice' => 'Description is..', + 'rule_trigger_description_is' => 'Description is ":trigger_value"', + 'rule_trigger_date_on_choice' => 'Transaction date is..', + 'rule_trigger_date_on' => 'Transaction date is ":trigger_value"', + 'rule_trigger_date_before_choice' => 'Transaction date is before..', + 'rule_trigger_date_before' => 'Transaction date is before ":trigger_value"', + 'rule_trigger_date_after_choice' => 'Transaction date is after..', + 'rule_trigger_date_after' => 'Transaction date is after ":trigger_value"', + 'rule_trigger_created_at_on_choice' => 'Transaction was made on..', + 'rule_trigger_created_at_on' => 'Transaction was made on ":trigger_value"', + 'rule_trigger_updated_at_on_choice' => 'Transaction was last edited on..', + 'rule_trigger_updated_at_on' => 'Transaction was last edited on ":trigger_value"', + 'rule_trigger_budget_is_choice' => 'Budget is..', + 'rule_trigger_budget_is' => 'Budget is ":trigger_value"', + 'rule_trigger_tag_is_choice' => 'Any tag is..', + 'rule_trigger_tag_is' => 'Any tag is ":trigger_value"', + 'rule_trigger_tag_contains_choice' => 'Any tag contains..', + 'rule_trigger_tag_contains' => 'Any tag contains ":trigger_value"', + 'rule_trigger_tag_ends_choice' => 'Any tag ends with..', + 'rule_trigger_tag_ends' => 'Any tag ends with ":trigger_value"', + 'rule_trigger_tag_starts_choice' => 'Any tag starts with..', + 'rule_trigger_tag_starts' => 'Any tag starts with ":trigger_value"', + 'rule_trigger_currency_is_choice' => 'Transaction currency is..', + 'rule_trigger_currency_is' => 'Transaction currency is ":trigger_value"', + 'rule_trigger_foreign_currency_is_choice' => 'Transaction foreign currency is..', + 'rule_trigger_foreign_currency_is' => 'Transaction foreign currency is ":trigger_value"', + 'rule_trigger_has_attachments_choice' => 'Has any attachments', + 'rule_trigger_has_attachments' => 'Has any attachment(s)', + 'rule_trigger_has_no_category_choice' => 'Has no category', + 'rule_trigger_has_no_category' => 'Transaction has no category', + 'rule_trigger_has_any_category_choice' => 'Has a (any) category', + 'rule_trigger_has_any_category' => 'Transaction has a (any) category', + 'rule_trigger_has_no_budget_choice' => 'Has no budget', + 'rule_trigger_has_no_budget' => 'Transaction has no budget', + 'rule_trigger_has_any_budget_choice' => 'Has a (any) budget', + 'rule_trigger_has_any_budget' => 'Transaction has a (any) budget', + 'rule_trigger_has_no_bill_choice' => 'Has no subscription', + 'rule_trigger_has_no_bill' => 'Transaction has no subscription', + 'rule_trigger_has_any_bill_choice' => 'Has a (any) subscription', + 'rule_trigger_has_any_bill' => 'Transaction has a (any) subscription', + 'rule_trigger_has_no_tag_choice' => 'Has no tag(s)', + 'rule_trigger_has_no_tag' => 'Transaction has no tag(s)', + 'rule_trigger_has_any_tag_choice' => 'Has one or more (any) tags', + 'rule_trigger_has_any_tag' => 'Transaction has one or more (any) tags', + 'rule_trigger_any_notes_choice' => 'Has (any) notes', + 'rule_trigger_any_notes' => 'Transaction has (any) notes', + 'rule_trigger_no_notes_choice' => 'Has no notes', + 'rule_trigger_no_notes' => 'Transaction has no notes', + 'rule_trigger_notes_is_choice' => 'Notes are..', + 'rule_trigger_notes_is' => 'Notes are ":trigger_value"', + 'rule_trigger_notes_contains_choice' => 'Notes contain..', + 'rule_trigger_notes_contains' => 'Notes contain ":trigger_value"', + 'rule_trigger_notes_starts_choice' => 'Notes start with..', + 'rule_trigger_notes_starts' => 'Notes start with ":trigger_value"', + 'rule_trigger_notes_ends_choice' => 'Notes end with..', + 'rule_trigger_notes_ends' => 'Notes end with ":trigger_value"', + 'rule_trigger_bill_is_choice' => 'Subscription is..', + 'rule_trigger_bill_is' => 'Subscription is ":trigger_value"', + 'rule_trigger_external_id_is_choice' => 'External ID is..', + 'rule_trigger_external_id_is' => 'External ID is ":trigger_value"', + 'rule_trigger_internal_reference_is_choice' => 'Internal reference is..', + 'rule_trigger_internal_reference_is' => 'Internal reference is ":trigger_value"', + 'rule_trigger_journal_id_choice' => 'Transaction journal ID is..', + 'rule_trigger_journal_id' => 'Transaction journal ID is ":trigger_value"', + 'rule_trigger_any_external_url' => 'Transaction has an (any) external URL', + 'rule_trigger_any_external_url_choice' => 'Transaction has an (any) external URL', + 'rule_trigger_any_external_id' => 'Transaction has an (any) external ID', + 'rule_trigger_any_external_id_choice' => 'Transaction has an (any) external ID', + 'rule_trigger_no_external_url_choice' => 'Transaction has no external URL', + 'rule_trigger_no_external_url' => 'Transaction has no external URL', + 'rule_trigger_no_external_id_choice' => 'Transaction has no external ID', + 'rule_trigger_no_external_id' => 'Transaction has no external ID', + 'rule_trigger_id_choice' => 'Transaction ID is..', + 'rule_trigger_id' => 'Transaction ID is ":trigger_value"', + 'rule_trigger_sepa_ct_is_choice' => 'SEPA CT is..', + 'rule_trigger_sepa_ct_is' => 'SEPA CT is ":trigger_value"', // new values: - 'rule_trigger_user_action_choice' => 'User action is ":trigger_value"', - 'rule_trigger_tag_is_not_choice' => 'No tag is..', - 'rule_trigger_tag_is_not' => 'No tag is ":trigger_value"', - 'rule_trigger_account_is_choice' => 'Either account is exactly..', - 'rule_trigger_account_is' => 'Either account is exactly ":trigger_value"', - 'rule_trigger_account_contains_choice' => 'Either account contains..', - 'rule_trigger_account_contains' => 'Either account contains ":trigger_value"', - 'rule_trigger_account_ends_choice' => 'Either account ends with..', - 'rule_trigger_account_ends' => 'Either account ends with ":trigger_value"', - 'rule_trigger_account_starts_choice' => 'Either account starts with..', - 'rule_trigger_account_starts' => 'Either account starts with ":trigger_value"', - 'rule_trigger_account_nr_is_choice' => 'Either account number / IBAN is..', - 'rule_trigger_account_nr_is' => 'Either account number / IBAN is ":trigger_value"', - 'rule_trigger_account_nr_contains_choice' => 'Either account number / IBAN contains..', - 'rule_trigger_account_nr_contains' => 'Either account number / IBAN contains ":trigger_value"', - 'rule_trigger_account_nr_ends_choice' => 'Either account number / IBAN ends with..', - 'rule_trigger_account_nr_ends' => 'Either account number / IBAN ends with ":trigger_value"', - 'rule_trigger_account_nr_starts_choice' => 'Either account number / IBAN starts with..', - 'rule_trigger_account_nr_starts' => 'Either account number / IBAN starts with ":trigger_value"', - 'rule_trigger_category_contains_choice' => 'Category contains..', - 'rule_trigger_category_contains' => 'Category contains ":trigger_value"', - 'rule_trigger_category_ends_choice' => 'Category ends with..', - 'rule_trigger_category_ends' => 'Category ends with ":trigger_value"', - 'rule_trigger_category_starts_choice' => 'Category starts with..', - 'rule_trigger_category_starts' => 'Category starts with ":trigger_value"', - 'rule_trigger_budget_contains_choice' => 'Budget contains..', - 'rule_trigger_budget_contains' => 'Budget contains ":trigger_value"', - 'rule_trigger_budget_ends_choice' => 'Budget ends with..', - 'rule_trigger_budget_ends' => 'Budget ends with ":trigger_value"', - 'rule_trigger_budget_starts_choice' => 'Budget starts with..', - 'rule_trigger_budget_starts' => 'Budget starts with ":trigger_value"', - 'rule_trigger_bill_contains_choice' => 'Bill contains..', - 'rule_trigger_bill_contains' => 'Bill contains ":trigger_value"', - 'rule_trigger_bill_ends_choice' => 'Bill ends with..', - 'rule_trigger_bill_ends' => 'Bill ends with ":trigger_value"', - 'rule_trigger_bill_starts_choice' => 'Bill starts with..', - 'rule_trigger_bill_starts' => 'Bill starts with ":trigger_value"', - 'rule_trigger_external_id_contains_choice' => 'External ID contains..', - 'rule_trigger_external_id_contains' => 'External ID contains ":trigger_value"', - 'rule_trigger_external_id_ends_choice' => 'External ID ends with..', - 'rule_trigger_external_id_ends' => 'External ID ends with ":trigger_value"', - 'rule_trigger_external_id_starts_choice' => 'External ID starts with..', - 'rule_trigger_external_id_starts' => 'External ID starts with ":trigger_value"', - 'rule_trigger_internal_reference_contains_choice' => 'Internal reference contains..', - 'rule_trigger_internal_reference_contains' => 'Internal reference contains ":trigger_value"', - 'rule_trigger_internal_reference_ends_choice' => 'Internal reference ends with..', - 'rule_trigger_internal_reference_ends' => 'Internal reference ends with ":trigger_value"', - 'rule_trigger_internal_reference_starts_choice' => 'Internal reference starts with..', - 'rule_trigger_internal_reference_starts' => 'Internal reference starts with ":trigger_value"', - 'rule_trigger_external_url_is_choice' => 'External URL is..', - 'rule_trigger_external_url_is' => 'External URL is ":trigger_value"', - 'rule_trigger_external_url_contains_choice' => 'External URL contains..', - 'rule_trigger_external_url_contains' => 'External URL contains ":trigger_value"', - 'rule_trigger_external_url_ends_choice' => 'External URL ends with..', - 'rule_trigger_external_url_ends' => 'External URL ends with ":trigger_value"', - 'rule_trigger_external_url_starts_choice' => 'External URL starts with..', - 'rule_trigger_external_url_starts' => 'External URL starts with ":trigger_value"', - 'rule_trigger_has_no_attachments_choice' => 'Has no attachments', - 'rule_trigger_has_no_attachments' => 'Transaction has no attachments', - 'rule_trigger_recurrence_id_choice' => 'Recurring transaction ID is..', - 'rule_trigger_recurrence_id' => 'Recurring transaction ID is ":trigger_value"', - 'rule_trigger_interest_date_on_choice' => 'Interest date is on..', - 'rule_trigger_interest_date_on' => 'Interest date is on ":trigger_value"', - 'rule_trigger_interest_date_before_choice' => 'Interest date is before..', - 'rule_trigger_interest_date_before' => 'Interest date is before ":trigger_value"', - 'rule_trigger_interest_date_after_choice' => 'Interest date is after..', - 'rule_trigger_interest_date_after' => 'Interest date is after ":trigger_value"', - 'rule_trigger_book_date_on_choice' => 'Book date is on..', - 'rule_trigger_book_date_on' => 'Book date is on ":trigger_value"', - 'rule_trigger_book_date_before_choice' => 'Book date is before..', - 'rule_trigger_book_date_before' => 'Book date is before ":trigger_value"', - 'rule_trigger_book_date_after_choice' => 'Book date is after..', - 'rule_trigger_book_date_after' => 'Book date is after ":trigger_value"', - 'rule_trigger_process_date_on_choice' => 'Process date is on..', - 'rule_trigger_process_date_on' => 'Process date is ":trigger_value"', - 'rule_trigger_process_date_before_choice' => 'Process date is before..', - 'rule_trigger_process_date_before' => 'Process date is before ":trigger_value"', - 'rule_trigger_process_date_after_choice' => 'Process date is after..', - 'rule_trigger_process_date_after' => 'Process date is after ":trigger_value"', - 'rule_trigger_due_date_on_choice' => 'Due date is on..', - 'rule_trigger_due_date_on' => 'Due date is on ":trigger_value"', - 'rule_trigger_due_date_before_choice' => 'Due date is before..', - 'rule_trigger_due_date_before' => 'Due date is before ":trigger_value"', - 'rule_trigger_due_date_after_choice' => 'Due date is after..', - 'rule_trigger_due_date_after' => 'Due date is after ":trigger_value"', - 'rule_trigger_payment_date_on_choice' => 'Payment date is on..', - 'rule_trigger_payment_date_on' => 'Payment date is on ":trigger_value"', - 'rule_trigger_payment_date_before_choice' => 'Payment date is before..', - 'rule_trigger_payment_date_before' => 'Payment date is before ":trigger_value"', - 'rule_trigger_payment_date_after_choice' => 'Payment date is after..', - 'rule_trigger_payment_date_after' => 'Payment date is after ":trigger_value"', - 'rule_trigger_invoice_date_on_choice' => 'Invoice date is on..', - 'rule_trigger_invoice_date_on' => 'Invoice date is on ":trigger_value"', - 'rule_trigger_invoice_date_before_choice' => 'Invoice date is before..', - 'rule_trigger_invoice_date_before' => 'Invoice date is before ":trigger_value"', - 'rule_trigger_invoice_date_after_choice' => 'Invoice date is after..', - 'rule_trigger_invoice_date_after' => 'Invoice date is after ":trigger_value"', - 'rule_trigger_created_at_before_choice' => 'Transaction was created before..', - 'rule_trigger_created_at_before' => 'Transaction was created before ":trigger_value"', - 'rule_trigger_created_at_after_choice' => 'Transaction was created after..', - 'rule_trigger_created_at_after' => 'Transaction was created after ":trigger_value"', - 'rule_trigger_updated_at_before_choice' => 'Transaction was last updated before..', - 'rule_trigger_updated_at_before' => 'Transaction was last updated before ":trigger_value"', - 'rule_trigger_updated_at_after_choice' => 'Transaction was last updated after..', - 'rule_trigger_updated_at_after' => 'Transaction was last updated after ":trigger_value"', - 'rule_trigger_foreign_amount_is_choice' => 'Foreign amount is exactly..', - 'rule_trigger_foreign_amount_is' => 'Foreign amount is exactly ":trigger_value"', - 'rule_trigger_foreign_amount_less_choice' => 'Foreign amount is less than..', - 'rule_trigger_foreign_amount_less' => 'Foreign amount is less than ":trigger_value"', - 'rule_trigger_foreign_amount_more_choice' => 'Foreign amount is more than..', - 'rule_trigger_foreign_amount_more' => 'Foreign amount is more than ":trigger_value"', - 'rule_trigger_attachment_name_is_choice' => 'Any attachment\'s name is..', - 'rule_trigger_attachment_name_is' => 'Any attachment\'s name is ":trigger_value"', - 'rule_trigger_attachment_name_contains_choice' => 'Any attachment\'s name contains..', - 'rule_trigger_attachment_name_contains' => 'Any attachment\'s name contains ":trigger_value"', - 'rule_trigger_attachment_name_starts_choice' => 'Any attachment\'s name starts with..', - 'rule_trigger_attachment_name_starts' => 'Any attachment\'s name starts with ":trigger_value"', - 'rule_trigger_attachment_name_ends_choice' => 'Any attachment\'s name ends with..', - 'rule_trigger_attachment_name_ends' => 'Any attachment\'s name ends with ":trigger_value"', - 'rule_trigger_attachment_notes_are_choice' => 'Any attachment\'s notes are..', - 'rule_trigger_attachment_notes_are' => 'Any attachment\'s notes are ":trigger_value"', - 'rule_trigger_attachment_notes_contains_choice' => 'Any attachment\'s notes contain..', - 'rule_trigger_attachment_notes_contains' => 'Any attachment\'s notes contain ":trigger_value"', - 'rule_trigger_attachment_notes_starts_choice' => 'Any attachment\'s notes start with..', - 'rule_trigger_attachment_notes_starts' => 'Any attachment\'s notes start with ":trigger_value"', - 'rule_trigger_attachment_notes_ends_choice' => 'Any attachment\'s notes end with..', - 'rule_trigger_attachment_notes_ends' => 'Any attachment\'s notes end with ":trigger_value"', - 'rule_trigger_reconciled_choice' => 'Transaction is reconciled', - 'rule_trigger_reconciled' => 'Transaction is reconciled', - 'rule_trigger_exists_choice' => 'Any transaction matches(!)', - 'rule_trigger_exists' => 'Any transaction matches', + 'rule_trigger_user_action_choice' => 'User action is ":trigger_value"', + 'rule_trigger_tag_is_not_choice' => 'No tag is..', + 'rule_trigger_tag_is_not' => 'No tag is ":trigger_value"', + 'rule_trigger_account_is_choice' => 'Either account is exactly..', + 'rule_trigger_account_is' => 'Either account is exactly ":trigger_value"', + 'rule_trigger_account_contains_choice' => 'Either account contains..', + 'rule_trigger_account_contains' => 'Either account contains ":trigger_value"', + 'rule_trigger_account_ends_choice' => 'Either account ends with..', + 'rule_trigger_account_ends' => 'Either account ends with ":trigger_value"', + 'rule_trigger_account_starts_choice' => 'Either account starts with..', + 'rule_trigger_account_starts' => 'Either account starts with ":trigger_value"', + 'rule_trigger_account_nr_is_choice' => 'Either account number / IBAN is..', + 'rule_trigger_account_nr_is' => 'Either account number / IBAN is ":trigger_value"', + 'rule_trigger_account_nr_contains_choice' => 'Either account number / IBAN contains..', + 'rule_trigger_account_nr_contains' => 'Either account number / IBAN contains ":trigger_value"', + 'rule_trigger_account_nr_ends_choice' => 'Either account number / IBAN ends with..', + 'rule_trigger_account_nr_ends' => 'Either account number / IBAN ends with ":trigger_value"', + 'rule_trigger_account_nr_starts_choice' => 'Either account number / IBAN starts with..', + 'rule_trigger_account_nr_starts' => 'Either account number / IBAN starts with ":trigger_value"', + 'rule_trigger_category_contains_choice' => 'Category contains..', + 'rule_trigger_category_contains' => 'Category contains ":trigger_value"', + 'rule_trigger_category_ends_choice' => 'Category ends with..', + 'rule_trigger_category_ends' => 'Category ends with ":trigger_value"', + 'rule_trigger_category_starts_choice' => 'Category starts with..', + 'rule_trigger_category_starts' => 'Category starts with ":trigger_value"', + 'rule_trigger_budget_contains_choice' => 'Budget contains..', + 'rule_trigger_budget_contains' => 'Budget contains ":trigger_value"', + 'rule_trigger_budget_ends_choice' => 'Budget ends with..', + 'rule_trigger_budget_ends' => 'Budget ends with ":trigger_value"', + 'rule_trigger_budget_starts_choice' => 'Budget starts with..', + 'rule_trigger_budget_starts' => 'Budget starts with ":trigger_value"', + 'rule_trigger_bill_contains_choice' => 'Subscription contains..', + 'rule_trigger_bill_contains' => 'Subscription contains ":trigger_value"', + 'rule_trigger_bill_ends_choice' => 'Subscription ends with..', + 'rule_trigger_bill_ends' => 'Subscription ends with ":trigger_value"', + 'rule_trigger_bill_starts_choice' => 'Subscription starts with..', + 'rule_trigger_bill_starts' => 'Subscription starts with ":trigger_value"', + 'rule_trigger_external_id_contains_choice' => 'External ID contains..', + 'rule_trigger_external_id_contains' => 'External ID contains ":trigger_value"', + 'rule_trigger_external_id_ends_choice' => 'External ID ends with..', + 'rule_trigger_external_id_ends' => 'External ID ends with ":trigger_value"', + 'rule_trigger_external_id_starts_choice' => 'External ID starts with..', + 'rule_trigger_external_id_starts' => 'External ID starts with ":trigger_value"', + 'rule_trigger_internal_reference_contains_choice' => 'Internal reference contains..', + 'rule_trigger_internal_reference_contains' => 'Internal reference contains ":trigger_value"', + 'rule_trigger_internal_reference_ends_choice' => 'Internal reference ends with..', + 'rule_trigger_internal_reference_ends' => 'Internal reference ends with ":trigger_value"', + 'rule_trigger_internal_reference_starts_choice' => 'Internal reference starts with..', + 'rule_trigger_internal_reference_starts' => 'Internal reference starts with ":trigger_value"', + 'rule_trigger_external_url_is_choice' => 'External URL is..', + 'rule_trigger_external_url_is' => 'External URL is ":trigger_value"', + 'rule_trigger_external_url_contains_choice' => 'External URL contains..', + 'rule_trigger_external_url_contains' => 'External URL contains ":trigger_value"', + 'rule_trigger_external_url_ends_choice' => 'External URL ends with..', + 'rule_trigger_external_url_ends' => 'External URL ends with ":trigger_value"', + 'rule_trigger_external_url_starts_choice' => 'External URL starts with..', + 'rule_trigger_external_url_starts' => 'External URL starts with ":trigger_value"', + 'rule_trigger_has_no_attachments_choice' => 'Has no attachments', + 'rule_trigger_has_no_attachments' => 'Transaction has no attachments', + 'rule_trigger_recurrence_id_choice' => 'Recurring transaction ID is..', + 'rule_trigger_recurrence_id' => 'Recurring transaction ID is ":trigger_value"', + 'rule_trigger_interest_date_on_choice' => 'Interest date is on..', + 'rule_trigger_interest_date_on' => 'Interest date is on ":trigger_value"', + 'rule_trigger_interest_date_before_choice' => 'Interest date is before..', + 'rule_trigger_interest_date_before' => 'Interest date is before ":trigger_value"', + 'rule_trigger_interest_date_after_choice' => 'Interest date is after..', + 'rule_trigger_interest_date_after' => 'Interest date is after ":trigger_value"', + 'rule_trigger_book_date_on_choice' => 'Book date is on..', + 'rule_trigger_book_date_on' => 'Book date is on ":trigger_value"', + 'rule_trigger_book_date_before_choice' => 'Book date is before..', + 'rule_trigger_book_date_before' => 'Book date is before ":trigger_value"', + 'rule_trigger_book_date_after_choice' => 'Book date is after..', + 'rule_trigger_book_date_after' => 'Book date is after ":trigger_value"', + 'rule_trigger_process_date_on_choice' => 'Process date is on..', + 'rule_trigger_process_date_on' => 'Process date is ":trigger_value"', + 'rule_trigger_process_date_before_choice' => 'Process date is before..', + 'rule_trigger_process_date_before' => 'Process date is before ":trigger_value"', + 'rule_trigger_process_date_after_choice' => 'Process date is after..', + 'rule_trigger_process_date_after' => 'Process date is after ":trigger_value"', + 'rule_trigger_due_date_on_choice' => 'Due date is on..', + 'rule_trigger_due_date_on' => 'Due date is on ":trigger_value"', + 'rule_trigger_due_date_before_choice' => 'Due date is before..', + 'rule_trigger_due_date_before' => 'Due date is before ":trigger_value"', + 'rule_trigger_due_date_after_choice' => 'Due date is after..', + 'rule_trigger_due_date_after' => 'Due date is after ":trigger_value"', + 'rule_trigger_payment_date_on_choice' => 'Payment date is on..', + 'rule_trigger_payment_date_on' => 'Payment date is on ":trigger_value"', + 'rule_trigger_payment_date_before_choice' => 'Payment date is before..', + 'rule_trigger_payment_date_before' => 'Payment date is before ":trigger_value"', + 'rule_trigger_payment_date_after_choice' => 'Payment date is after..', + 'rule_trigger_payment_date_after' => 'Payment date is after ":trigger_value"', + 'rule_trigger_invoice_date_on_choice' => 'Invoice date is on..', + 'rule_trigger_invoice_date_on' => 'Invoice date is on ":trigger_value"', + 'rule_trigger_invoice_date_before_choice' => 'Invoice date is before..', + 'rule_trigger_invoice_date_before' => 'Invoice date is before ":trigger_value"', + 'rule_trigger_invoice_date_after_choice' => 'Invoice date is after..', + 'rule_trigger_invoice_date_after' => 'Invoice date is after ":trigger_value"', + 'rule_trigger_created_at_before_choice' => 'Transaction was created before..', + 'rule_trigger_created_at_before' => 'Transaction was created before ":trigger_value"', + 'rule_trigger_created_at_after_choice' => 'Transaction was created after..', + 'rule_trigger_created_at_after' => 'Transaction was created after ":trigger_value"', + 'rule_trigger_updated_at_before_choice' => 'Transaction was last updated before..', + 'rule_trigger_updated_at_before' => 'Transaction was last updated before ":trigger_value"', + 'rule_trigger_updated_at_after_choice' => 'Transaction was last updated after..', + 'rule_trigger_updated_at_after' => 'Transaction was last updated after ":trigger_value"', + 'rule_trigger_foreign_amount_is_choice' => 'Foreign amount is exactly..', + 'rule_trigger_foreign_amount_is' => 'Foreign amount is exactly ":trigger_value"', + 'rule_trigger_foreign_amount_less_choice' => 'Foreign amount is less than..', + 'rule_trigger_foreign_amount_less' => 'Foreign amount is less than ":trigger_value"', + 'rule_trigger_foreign_amount_more_choice' => 'Foreign amount is more than..', + 'rule_trigger_foreign_amount_more' => 'Foreign amount is more than ":trigger_value"', + 'rule_trigger_attachment_name_is_choice' => 'Any attachment\'s name is..', + 'rule_trigger_attachment_name_is' => 'Any attachment\'s name is ":trigger_value"', + 'rule_trigger_attachment_name_contains_choice' => 'Any attachment\'s name contains..', + 'rule_trigger_attachment_name_contains' => 'Any attachment\'s name contains ":trigger_value"', + 'rule_trigger_attachment_name_starts_choice' => 'Any attachment\'s name starts with..', + 'rule_trigger_attachment_name_starts' => 'Any attachment\'s name starts with ":trigger_value"', + 'rule_trigger_attachment_name_ends_choice' => 'Any attachment\'s name ends with..', + 'rule_trigger_attachment_name_ends' => 'Any attachment\'s name ends with ":trigger_value"', + 'rule_trigger_attachment_notes_are_choice' => 'Any attachment\'s notes are..', + 'rule_trigger_attachment_notes_are' => 'Any attachment\'s notes are ":trigger_value"', + 'rule_trigger_attachment_notes_contains_choice' => 'Any attachment\'s notes contain..', + 'rule_trigger_attachment_notes_contains' => 'Any attachment\'s notes contain ":trigger_value"', + 'rule_trigger_attachment_notes_starts_choice' => 'Any attachment\'s notes start with..', + 'rule_trigger_attachment_notes_starts' => 'Any attachment\'s notes start with ":trigger_value"', + 'rule_trigger_attachment_notes_ends_choice' => 'Any attachment\'s notes end with..', + 'rule_trigger_attachment_notes_ends' => 'Any attachment\'s notes end with ":trigger_value"', + 'rule_trigger_reconciled_choice' => 'Transaction is reconciled', + 'rule_trigger_reconciled' => 'Transaction is reconciled', + 'rule_trigger_exists_choice' => 'Any transaction matches(!)', + 'rule_trigger_exists' => 'Any transaction matches', // more values for new types: - 'rule_trigger_not_account_id' => 'Account ID is not ":trigger_value"', - 'rule_trigger_not_source_account_id' => 'Source account ID is not ":trigger_value"', - 'rule_trigger_not_destination_account_id' => 'Destination account ID is not ":trigger_value"', - 'rule_trigger_not_transaction_type' => 'Transaction type is not ":trigger_value"', - 'rule_trigger_not_tag_is' => 'Tag is not ":trigger_value"', - 'rule_trigger_not_tag_is_not' => 'Tag is ":trigger_value"', - 'rule_trigger_not_description_is' => 'Description is not ":trigger_value"', - 'rule_trigger_not_description_contains' => 'Description does not contain', - 'rule_trigger_not_description_ends' => 'Description does not end with ":trigger_value"', - 'rule_trigger_not_description_starts' => 'Description does not start with ":trigger_value"', - 'rule_trigger_not_notes_is' => 'Notes are not ":trigger_value"', - 'rule_trigger_not_notes_contains' => 'Notes do not contain ":trigger_value"', - 'rule_trigger_not_notes_ends' => 'Notes do not end on ":trigger_value"', - 'rule_trigger_not_notes_starts' => 'Notes do not start with ":trigger_value"', - 'rule_trigger_not_source_account_is' => 'Source account is not ":trigger_value"', - 'rule_trigger_not_source_account_contains' => 'Source account does not contain ":trigger_value"', - 'rule_trigger_not_source_account_ends' => 'Source account does not end on ":trigger_value"', - 'rule_trigger_not_source_account_starts' => 'Source account does not start with ":trigger_value"', - 'rule_trigger_not_source_account_nr_is' => 'Source account number / IBAN is not ":trigger_value"', - 'rule_trigger_not_source_account_nr_contains' => 'Source account number / IBAN does not contain ":trigger_value"', - 'rule_trigger_not_source_account_nr_ends' => 'Source account number / IBAN does not end on ":trigger_value"', - 'rule_trigger_not_source_account_nr_starts' => 'Source account number / IBAN does not start with ":trigger_value"', - 'rule_trigger_not_destination_account_is' => 'Destination account is not ":trigger_value"', - 'rule_trigger_not_destination_account_contains' => 'Destination account does not contain ":trigger_value"', - 'rule_trigger_not_destination_account_ends' => 'Destination account does not end on ":trigger_value"', - 'rule_trigger_not_destination_account_starts' => 'Destination account does not start with ":trigger_value"', - 'rule_trigger_not_destination_account_nr_is' => 'Destination account number / IBAN is not ":trigger_value"', - 'rule_trigger_not_destination_account_nr_contains' => 'Destination account number / IBAN does not contain ":trigger_value"', - 'rule_trigger_not_destination_account_nr_ends' => 'Destination account number / IBAN does not end on ":trigger_value"', - 'rule_trigger_not_destination_account_nr_starts' => 'Destination account number / IBAN does not start with ":trigger_value"', - 'rule_trigger_not_account_is' => 'Neither account is ":trigger_value"', - 'rule_trigger_not_account_contains' => 'Neither account contains ":trigger_value"', - 'rule_trigger_not_account_ends' => 'Neither account ends on ":trigger_value"', - 'rule_trigger_not_account_starts' => 'Neither account starts with ":trigger_value"', - 'rule_trigger_not_account_nr_is' => 'Neither account number / IBAN is ":trigger_value"', - 'rule_trigger_not_account_nr_contains' => 'Neither account number / IBAN contains ":trigger_value"', - 'rule_trigger_not_account_nr_ends' => 'Neither account number / IBAN ends on ":trigger_value"', - 'rule_trigger_not_account_nr_starts' => 'Neither account number / IBAN starts with ":trigger_value"', - 'rule_trigger_not_category_is' => 'Category is not ":trigger_value"', - 'rule_trigger_not_category_contains' => 'Category does not contain ":trigger_value"', - 'rule_trigger_not_category_ends' => 'Category does not end on ":trigger_value"', - 'rule_trigger_not_category_starts' => 'Category does not start with ":trigger_value"', - 'rule_trigger_not_budget_is' => 'Budget is not ":trigger_value"', - 'rule_trigger_not_budget_contains' => 'Budget does not contain ":trigger_value"', - 'rule_trigger_not_budget_ends' => 'Budget does not end on ":trigger_value"', - 'rule_trigger_not_budget_starts' => 'Budget does not start with ":trigger_value"', - 'rule_trigger_not_bill_is' => 'Bill is not is ":trigger_value"', - 'rule_trigger_not_bill_contains' => 'Bill does not contain ":trigger_value"', - 'rule_trigger_not_bill_ends' => 'Bill does not end on ":trigger_value"', - 'rule_trigger_not_bill_starts' => 'Bill does not end with ":trigger_value"', - 'rule_trigger_not_external_id_is' => 'External ID is not ":trigger_value"', - 'rule_trigger_not_external_id_contains' => 'External ID does not contain ":trigger_value"', - 'rule_trigger_not_external_id_ends' => 'External ID does not end on ":trigger_value"', - 'rule_trigger_not_external_id_starts' => 'External ID does not start with ":trigger_value"', - 'rule_trigger_not_internal_reference_is' => 'Internal reference is not ":trigger_value"', - 'rule_trigger_not_internal_reference_contains' => 'Internal reference does not contain ":trigger_value"', - 'rule_trigger_not_internal_reference_ends' => 'Internal reference does not end on ":trigger_value"', - 'rule_trigger_not_internal_reference_starts' => 'Internal reference does not start with ":trigger_value"', - 'rule_trigger_not_external_url_is' => 'External URL is not ":trigger_value"', - 'rule_trigger_not_external_url_contains' => 'External URL does not contain ":trigger_value"', - 'rule_trigger_not_external_url_ends' => 'External URL does not end on ":trigger_value"', - 'rule_trigger_not_external_url_starts' => 'External URL does not start with ":trigger_value"', - 'rule_trigger_not_currency_is' => 'Currency is not ":trigger_value"', - 'rule_trigger_not_foreign_currency_is' => 'Foreign currency is not ":trigger_value"', - 'rule_trigger_not_id' => 'Transaction ID is not ":trigger_value"', - 'rule_trigger_not_journal_id' => 'Transaction journal ID is not ":trigger_value"', - 'rule_trigger_not_recurrence_id' => 'Recurrence ID is not ":trigger_value"', - 'rule_trigger_not_date_on' => 'Date is not on ":trigger_value"', - 'rule_trigger_not_date_before' => 'Date is not before ":trigger_value"', - 'rule_trigger_not_date_after' => 'Date is not after ":trigger_value"', - 'rule_trigger_not_interest_date_on' => 'Interest date is not on ":trigger_value"', - 'rule_trigger_not_interest_date_before' => 'Interest date is not before ":trigger_value"', - 'rule_trigger_not_interest_date_after' => 'Interest date is not after ":trigger_value"', - 'rule_trigger_not_book_date_on' => 'Book date is not on ":trigger_value"', - 'rule_trigger_not_book_date_before' => 'Book date is not before ":trigger_value"', - 'rule_trigger_not_book_date_after' => 'Book date is not after ":trigger_value"', - 'rule_trigger_not_process_date_on' => 'Process date is not on ":trigger_value"', - 'rule_trigger_not_process_date_before' => 'Process date is not before ":trigger_value"', - 'rule_trigger_not_process_date_after' => 'Process date is not after ":trigger_value"', - 'rule_trigger_not_due_date_on' => 'Due date is not on ":trigger_value"', - 'rule_trigger_not_due_date_before' => 'Due date is not before ":trigger_value"', - 'rule_trigger_not_due_date_after' => 'Due date is not after ":trigger_value"', - 'rule_trigger_not_payment_date_on' => 'Payment date is not on ":trigger_value"', - 'rule_trigger_not_payment_date_before' => 'Payment date is not before ":trigger_value"', - 'rule_trigger_not_payment_date_after' => 'Payment date is not after ":trigger_value"', - 'rule_trigger_not_invoice_date_on' => 'Invoice date is not on ":trigger_value"', - 'rule_trigger_not_invoice_date_before' => 'Invoice date is not before ":trigger_value"', - 'rule_trigger_not_invoice_date_after' => 'Invoice date is not after ":trigger_value"', - 'rule_trigger_not_created_at_on' => 'Transaction is not created on ":trigger_value"', - 'rule_trigger_not_created_at_before' => 'Transaction is not created before ":trigger_value"', - 'rule_trigger_not_created_at_after' => 'Transaction is not created after ":trigger_value"', - 'rule_trigger_not_updated_at_on' => 'Transaction is not updated on ":trigger_value"', - 'rule_trigger_not_updated_at_before' => 'Transaction is not updated before ":trigger_value"', - 'rule_trigger_not_updated_at_after' => 'Transaction is not updated after ":trigger_value"', - 'rule_trigger_not_amount_is' => 'Transaction amount is not ":trigger_value"', - 'rule_trigger_not_amount_less' => 'Transaction amount is more than ":trigger_value"', - 'rule_trigger_not_amount_more' => 'Transaction amount is less than ":trigger_value"', - 'rule_trigger_not_foreign_amount_is' => 'Foreign transaction amount is not ":trigger_value"', - 'rule_trigger_not_foreign_amount_less' => 'Foreign transaction amount is more than ":trigger_value"', - 'rule_trigger_not_foreign_amount_more' => 'Foreign transaction amount is less than ":trigger_value"', - 'rule_trigger_not_attachment_name_is' => 'No attachment is named ":trigger_value"', - 'rule_trigger_not_attachment_name_contains' => 'No attachment name contains ":trigger_value"', - 'rule_trigger_not_attachment_name_starts' => 'No attachment name starts with ":trigger_value"', - 'rule_trigger_not_attachment_name_ends' => 'No attachment name ends on ":trigger_value"', - 'rule_trigger_not_attachment_notes_are' => 'No attachment notes are ":trigger_value"', - 'rule_trigger_not_attachment_notes_contains' => 'No attachment notes contain ":trigger_value"', - 'rule_trigger_not_attachment_notes_starts' => 'No attachment notes start with ":trigger_value"', - 'rule_trigger_not_attachment_notes_ends' => 'No attachment notes end on ":trigger_value"', - 'rule_trigger_not_reconciled' => 'Transaction is not reconciled', - 'rule_trigger_not_exists' => 'Transaction does not exist', - 'rule_trigger_not_has_attachments' => 'Transaction has no attachments', - 'rule_trigger_not_has_any_category' => 'Transaction has no category', - 'rule_trigger_not_has_any_budget' => 'Transaction has no budget', - 'rule_trigger_not_has_any_bill' => 'Transaction has no bill', - 'rule_trigger_not_has_any_tag' => 'Transaction has no tags', - 'rule_trigger_not_any_notes' => 'Transaction has no notes', - 'rule_trigger_not_any_external_url' => 'Transaction has no external URL', - 'rule_trigger_not_has_no_attachments' => 'Transaction has a (any) attachment(s)', - 'rule_trigger_not_has_no_category' => 'Transaction has a (any) category', - 'rule_trigger_not_has_no_budget' => 'Transaction has a (any) budget', - 'rule_trigger_not_has_no_bill' => 'Transaction has a (any) bill', - 'rule_trigger_not_has_no_tag' => 'Transaction has a (any) tag', - 'rule_trigger_not_no_notes' => 'Transaction has any notes', - 'rule_trigger_not_no_external_url' => 'Transaction has an external URL', - 'rule_trigger_not_source_is_cash' => 'Source account is not a cash account', - 'rule_trigger_not_destination_is_cash' => 'Destination account is not a cash account', - 'rule_trigger_not_account_is_cash' => 'Neither account is a cash account', + 'rule_trigger_not_account_id' => 'Account ID is not ":trigger_value"', + 'rule_trigger_not_source_account_id' => 'Source account ID is not ":trigger_value"', + 'rule_trigger_not_destination_account_id' => 'Destination account ID is not ":trigger_value"', + 'rule_trigger_not_transaction_type' => 'Transaction type is not ":trigger_value"', + 'rule_trigger_not_tag_is' => 'Tag is not ":trigger_value"', + 'rule_trigger_not_tag_is_not' => 'Tag is ":trigger_value"', + 'rule_trigger_not_description_is' => 'Description is not ":trigger_value"', + 'rule_trigger_not_description_contains' => 'Description does not contain', + 'rule_trigger_not_description_ends' => 'Description does not end with ":trigger_value"', + 'rule_trigger_not_description_starts' => 'Description does not start with ":trigger_value"', + 'rule_trigger_not_notes_is' => 'Notes are not ":trigger_value"', + 'rule_trigger_not_notes_contains' => 'Notes do not contain ":trigger_value"', + 'rule_trigger_not_notes_ends' => 'Notes do not end on ":trigger_value"', + 'rule_trigger_not_notes_starts' => 'Notes do not start with ":trigger_value"', + 'rule_trigger_not_source_account_is' => 'Source account is not ":trigger_value"', + 'rule_trigger_not_source_account_contains' => 'Source account does not contain ":trigger_value"', + 'rule_trigger_not_source_account_ends' => 'Source account does not end on ":trigger_value"', + 'rule_trigger_not_source_account_starts' => 'Source account does not start with ":trigger_value"', + 'rule_trigger_not_source_account_nr_is' => 'Source account number / IBAN is not ":trigger_value"', + 'rule_trigger_not_source_account_nr_contains' => 'Source account number / IBAN does not contain ":trigger_value"', + 'rule_trigger_not_source_account_nr_ends' => 'Source account number / IBAN does not end on ":trigger_value"', + 'rule_trigger_not_source_account_nr_starts' => 'Source account number / IBAN does not start with ":trigger_value"', + 'rule_trigger_not_destination_account_is' => 'Destination account is not ":trigger_value"', + 'rule_trigger_not_destination_account_contains' => 'Destination account does not contain ":trigger_value"', + 'rule_trigger_not_destination_account_ends' => 'Destination account does not end on ":trigger_value"', + 'rule_trigger_not_destination_account_starts' => 'Destination account does not start with ":trigger_value"', + 'rule_trigger_not_destination_account_nr_is' => 'Destination account number / IBAN is not ":trigger_value"', + 'rule_trigger_not_destination_account_nr_contains' => 'Destination account number / IBAN does not contain ":trigger_value"', + 'rule_trigger_not_destination_account_nr_ends' => 'Destination account number / IBAN does not end on ":trigger_value"', + 'rule_trigger_not_destination_account_nr_starts' => 'Destination account number / IBAN does not start with ":trigger_value"', + 'rule_trigger_not_account_is' => 'Neither account is ":trigger_value"', + 'rule_trigger_not_account_contains' => 'Neither account contains ":trigger_value"', + 'rule_trigger_not_account_ends' => 'Neither account ends on ":trigger_value"', + 'rule_trigger_not_account_starts' => 'Neither account starts with ":trigger_value"', + 'rule_trigger_not_account_nr_is' => 'Neither account number / IBAN is ":trigger_value"', + 'rule_trigger_not_account_nr_contains' => 'Neither account number / IBAN contains ":trigger_value"', + 'rule_trigger_not_account_nr_ends' => 'Neither account number / IBAN ends on ":trigger_value"', + 'rule_trigger_not_account_nr_starts' => 'Neither account number / IBAN starts with ":trigger_value"', + 'rule_trigger_not_category_is' => 'Category is not ":trigger_value"', + 'rule_trigger_not_category_contains' => 'Category does not contain ":trigger_value"', + 'rule_trigger_not_category_ends' => 'Category does not end on ":trigger_value"', + 'rule_trigger_not_category_starts' => 'Category does not start with ":trigger_value"', + 'rule_trigger_not_budget_is' => 'Budget is not ":trigger_value"', + 'rule_trigger_not_budget_contains' => 'Budget does not contain ":trigger_value"', + 'rule_trigger_not_budget_ends' => 'Budget does not end on ":trigger_value"', + 'rule_trigger_not_budget_starts' => 'Budget does not start with ":trigger_value"', + 'rule_trigger_not_bill_is' => 'Subscription is not is ":trigger_value"', + 'rule_trigger_not_bill_contains' => 'Subscription does not contain ":trigger_value"', + 'rule_trigger_not_bill_ends' => 'Subscription does not end on ":trigger_value"', + 'rule_trigger_not_bill_starts' => 'Subscription does not end with ":trigger_value"', + 'rule_trigger_not_external_id_is' => 'External ID is not ":trigger_value"', + 'rule_trigger_not_external_id_contains' => 'External ID does not contain ":trigger_value"', + 'rule_trigger_not_external_id_ends' => 'External ID does not end on ":trigger_value"', + 'rule_trigger_not_external_id_starts' => 'External ID does not start with ":trigger_value"', + 'rule_trigger_not_internal_reference_is' => 'Internal reference is not ":trigger_value"', + 'rule_trigger_not_internal_reference_contains' => 'Internal reference does not contain ":trigger_value"', + 'rule_trigger_not_internal_reference_ends' => 'Internal reference does not end on ":trigger_value"', + 'rule_trigger_not_internal_reference_starts' => 'Internal reference does not start with ":trigger_value"', + 'rule_trigger_not_external_url_is' => 'External URL is not ":trigger_value"', + 'rule_trigger_not_external_url_contains' => 'External URL does not contain ":trigger_value"', + 'rule_trigger_not_external_url_ends' => 'External URL does not end on ":trigger_value"', + 'rule_trigger_not_external_url_starts' => 'External URL does not start with ":trigger_value"', + 'rule_trigger_not_currency_is' => 'Currency is not ":trigger_value"', + 'rule_trigger_not_foreign_currency_is' => 'Foreign currency is not ":trigger_value"', + 'rule_trigger_not_id' => 'Transaction ID is not ":trigger_value"', + 'rule_trigger_not_journal_id' => 'Transaction journal ID is not ":trigger_value"', + 'rule_trigger_not_recurrence_id' => 'Recurrence ID is not ":trigger_value"', + 'rule_trigger_not_date_on' => 'Date is not on ":trigger_value"', + 'rule_trigger_not_date_before' => 'Date is not before ":trigger_value"', + 'rule_trigger_not_date_after' => 'Date is not after ":trigger_value"', + 'rule_trigger_not_interest_date_on' => 'Interest date is not on ":trigger_value"', + 'rule_trigger_not_interest_date_before' => 'Interest date is not before ":trigger_value"', + 'rule_trigger_not_interest_date_after' => 'Interest date is not after ":trigger_value"', + 'rule_trigger_not_book_date_on' => 'Book date is not on ":trigger_value"', + 'rule_trigger_not_book_date_before' => 'Book date is not before ":trigger_value"', + 'rule_trigger_not_book_date_after' => 'Book date is not after ":trigger_value"', + 'rule_trigger_not_process_date_on' => 'Process date is not on ":trigger_value"', + 'rule_trigger_not_process_date_before' => 'Process date is not before ":trigger_value"', + 'rule_trigger_not_process_date_after' => 'Process date is not after ":trigger_value"', + 'rule_trigger_not_due_date_on' => 'Due date is not on ":trigger_value"', + 'rule_trigger_not_due_date_before' => 'Due date is not before ":trigger_value"', + 'rule_trigger_not_due_date_after' => 'Due date is not after ":trigger_value"', + 'rule_trigger_not_payment_date_on' => 'Payment date is not on ":trigger_value"', + 'rule_trigger_not_payment_date_before' => 'Payment date is not before ":trigger_value"', + 'rule_trigger_not_payment_date_after' => 'Payment date is not after ":trigger_value"', + 'rule_trigger_not_invoice_date_on' => 'Invoice date is not on ":trigger_value"', + 'rule_trigger_not_invoice_date_before' => 'Invoice date is not before ":trigger_value"', + 'rule_trigger_not_invoice_date_after' => 'Invoice date is not after ":trigger_value"', + 'rule_trigger_not_created_at_on' => 'Transaction is not created on ":trigger_value"', + 'rule_trigger_not_created_at_before' => 'Transaction is not created before ":trigger_value"', + 'rule_trigger_not_created_at_after' => 'Transaction is not created after ":trigger_value"', + 'rule_trigger_not_updated_at_on' => 'Transaction is not updated on ":trigger_value"', + 'rule_trigger_not_updated_at_before' => 'Transaction is not updated before ":trigger_value"', + 'rule_trigger_not_updated_at_after' => 'Transaction is not updated after ":trigger_value"', + 'rule_trigger_not_amount_is' => 'Transaction amount is not ":trigger_value"', + 'rule_trigger_not_amount_less' => 'Transaction amount is more than ":trigger_value"', + 'rule_trigger_not_amount_more' => 'Transaction amount is less than ":trigger_value"', + 'rule_trigger_not_foreign_amount_is' => 'Foreign transaction amount is not ":trigger_value"', + 'rule_trigger_not_foreign_amount_less' => 'Foreign transaction amount is more than ":trigger_value"', + 'rule_trigger_not_foreign_amount_more' => 'Foreign transaction amount is less than ":trigger_value"', + 'rule_trigger_not_attachment_name_is' => 'No attachment is named ":trigger_value"', + 'rule_trigger_not_attachment_name_contains' => 'No attachment name contains ":trigger_value"', + 'rule_trigger_not_attachment_name_starts' => 'No attachment name starts with ":trigger_value"', + 'rule_trigger_not_attachment_name_ends' => 'No attachment name ends on ":trigger_value"', + 'rule_trigger_not_attachment_notes_are' => 'No attachment notes are ":trigger_value"', + 'rule_trigger_not_attachment_notes_contains' => 'No attachment notes contain ":trigger_value"', + 'rule_trigger_not_attachment_notes_starts' => 'No attachment notes start with ":trigger_value"', + 'rule_trigger_not_attachment_notes_ends' => 'No attachment notes end on ":trigger_value"', + 'rule_trigger_not_reconciled' => 'Transaction is not reconciled', + 'rule_trigger_not_exists' => 'Transaction does not exist', + 'rule_trigger_not_has_attachments' => 'Transaction has no attachments', + 'rule_trigger_not_has_any_category' => 'Transaction has no category', + 'rule_trigger_not_has_any_budget' => 'Transaction has no budget', + 'rule_trigger_not_has_any_bill' => 'Transaction has no subscription', + 'rule_trigger_not_has_any_tag' => 'Transaction has no tags', + 'rule_trigger_not_any_notes' => 'Transaction has no notes', + 'rule_trigger_not_any_external_url' => 'Transaction has no external URL', + 'rule_trigger_not_has_no_attachments' => 'Transaction has a (any) attachment(s)', + 'rule_trigger_not_has_no_category' => 'Transaction has a (any) category', + 'rule_trigger_not_has_no_budget' => 'Transaction has a (any) budget', + 'rule_trigger_not_has_no_bill' => 'Transaction has a (any) subscription', + 'rule_trigger_not_has_no_tag' => 'Transaction has a (any) tag', + 'rule_trigger_not_no_notes' => 'Transaction has any notes', + 'rule_trigger_not_no_external_url' => 'Transaction has an external URL', + 'rule_trigger_not_source_is_cash' => 'Source account is not a cash account', + 'rule_trigger_not_destination_is_cash' => 'Destination account is not a cash account', + 'rule_trigger_not_account_is_cash' => 'Neither account is a cash account', + + // new account balance things. + 'rule_trigger_destination_balance_gt_choice' => 'Destination account balance is more than..', + 'rule_trigger_destination_balance_gte_choice' => 'Destination account balance is more than or equal to..', + 'rule_trigger_destination_balance_is_choice' => 'Destination account balance is exactly..', + 'rule_trigger_destination_balance_lt_choice' => 'Destination account balance is less than..', + 'rule_trigger_destination_balance_lte_choice' => 'Destination account balance is less than or equal to..', + + 'rule_trigger_source_balance_gt_choice' => 'Source account balance is more than..', + 'rule_trigger_source_balance_gte_choice' => 'Source account balance is more than or equal to..', + 'rule_trigger_source_balance_is_choice' => 'Source account balance is exactly..', + 'rule_trigger_source_balance_lt_choice' => 'Source account balance is less than..', + 'rule_trigger_source_balance_lte_choice' => 'Source account balance is less than or equal to..', + + 'rule_trigger_destination_balance_gt' => 'Destination account balance is more than :trigger_value', + 'rule_trigger_destination_balance_gte' => 'Destination account balance is more than or equal to :trigger_value', + 'rule_trigger_destination_balance_is' => 'Destination account balance is exactly :trigger_value', + 'rule_trigger_destination_balance_lt' => 'Destination account balance is less than :trigger_value', + 'rule_trigger_destination_balance_lte' => 'Destination account balance is less than or equal to :trigger_value', + + 'rule_trigger_source_balance_gt' => 'Source account balance is more than :trigger_value', + 'rule_trigger_source_balance_gte' => 'Source account balance is more than or equal to :trigger_value', + 'rule_trigger_source_balance_is' => 'Source account balance is exactly :trigger_value', + 'rule_trigger_source_balance_lt' => 'Source account balance is less than :trigger_value', + 'rule_trigger_source_balance_lte' => 'Source account balance is less than or equal to :trigger_value', + + // NOT + 'rule_trigger_not_destination_balance_gt' => 'Destination account balance is less than or equal to :trigger_value', + 'rule_trigger_not_destination_balance_gte' => 'Destination account balance is less than :trigger_value', + 'rule_trigger_not_destination_balance_is' => 'Destination account balance is not :trigger_value', + 'rule_trigger_not_destination_balance_lt' => 'Destination account balance is more than or equal to :trigger_value', + 'rule_trigger_not_destination_balance_lte' => 'Destination account balance is more than :trigger_value', + + 'rule_trigger_not_source_balance_gt' => 'Source account balance is less than or equal to :trigger_value', + 'rule_trigger_not_source_balance_gte' => 'Source account balance is less than :trigger_value', + 'rule_trigger_not_source_balance_is' => 'Source account balance is not :trigger_value', + 'rule_trigger_not_source_balance_lt' => 'Source account balance is more than or equal to :trigger_value', + 'rule_trigger_not_source_balance_lte' => 'Source account balance is more than :trigger_value', // Ignore this comment // actions // set, clear, add, remove, append/prepend - 'rule_action_delete_transaction_choice' => 'DELETE transaction(!)', - 'rule_action_delete_transaction' => 'DELETE transaction(!)', - 'rule_action_set_category' => 'Set category to ":action_value"', - 'rule_action_clear_category' => 'Clear category', - 'rule_action_set_budget' => 'Set budget to ":action_value"', - 'rule_action_clear_budget' => 'Clear budget', - 'rule_action_add_tag' => 'Add tag ":action_value"', - 'rule_action_remove_tag' => 'Remove tag ":action_value"', - 'rule_action_remove_all_tags' => 'Remove all tags', - 'rule_action_set_description' => 'Set description to ":action_value"', - 'rule_action_append_description' => 'Append description with ":action_value"', - 'rule_action_prepend_description' => 'Prepend description with ":action_value"', - 'rule_action_set_category_choice' => 'Set category to ..', - 'rule_action_clear_category_choice' => 'Clear any category', - 'rule_action_set_budget_choice' => 'Set budget to ..', - 'rule_action_clear_budget_choice' => 'Clear any budget', - 'rule_action_add_tag_choice' => 'Add tag ..', - 'rule_action_remove_tag_choice' => 'Remove tag ..', - 'rule_action_remove_all_tags_choice' => 'Remove all tags', - 'rule_action_set_description_choice' => 'Set description to ..', - 'rule_action_update_piggy_choice' => 'Add / remove transaction amount in piggy bank ..', - 'rule_action_update_piggy' => 'Add / remove transaction amount in piggy bank ":action_value"', - 'rule_action_append_description_choice' => 'Append description with ..', - 'rule_action_prepend_description_choice' => 'Prepend description with ..', - 'rule_action_set_source_account_choice' => 'Set source account to ..', - 'rule_action_set_source_account' => 'Set source account to :action_value', - 'rule_action_set_destination_account_choice' => 'Set destination account to ..', - 'rule_action_set_destination_account' => 'Set destination account to :action_value', - 'rule_action_append_notes_choice' => 'Append notes with ..', - 'rule_action_append_notes' => 'Append notes with ":action_value"', - 'rule_action_prepend_notes_choice' => 'Prepend notes with ..', - 'rule_action_prepend_notes' => 'Prepend notes with ":action_value"', - 'rule_action_clear_notes_choice' => 'Remove any notes', - 'rule_action_clear_notes' => 'Remove any notes', - 'rule_action_set_notes_choice' => 'Set notes to ..', - 'rule_action_link_to_bill_choice' => 'Link to a bill ..', - 'rule_action_link_to_bill' => 'Link to bill ":action_value"', - 'rule_action_switch_accounts_choice' => 'Switch source and destination accounts (transfers only!)', - 'rule_action_switch_accounts' => 'Switch source and destination', - '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"', - 'rule_action_append_descr_to_notes_choice' => 'Append the description to the transaction notes', - 'rule_action_append_notes_to_descr_choice' => 'Append the transaction notes to the description', - 'rule_action_move_descr_to_notes_choice' => 'Replace the current transaction notes with the description', - 'rule_action_move_notes_to_descr_choice' => 'Replace the current description with the transaction notes', - 'rule_action_append_descr_to_notes' => 'Append description to notes', - 'rule_action_append_notes_to_descr' => 'Append notes to description', - 'rule_action_move_descr_to_notes' => 'Replace notes with description', - 'rule_action_move_notes_to_descr' => 'Replace description with notes', - 'rule_action_set_amount_choice' => 'Set amount to ..', - 'rule_action_set_amount' => 'Set amount to ":action_value"', - 'rule_action_set_destination_to_cash_choice' => 'Set destination account to (cash)', - 'rule_action_set_source_to_cash_choice' => 'Set source account to (cash)', - 'rulegroup_for_bills_title' => 'Rule group for bills', - 'rulegroup_for_bills_description' => 'A special rule group for all the rules that involve bills.', - 'rule_for_bill_title' => 'Auto-generated rule for bill ":name"', - 'rule_for_bill_description' => 'This rule is auto-generated to try to match bill ":name".', - 'create_rule_for_bill' => 'Create a new rule for bill ":name"', - 'create_rule_for_bill_txt' => 'You have just created a new bill called ":name", congratulations!Firefly III can automagically match new withdrawals to this bill. For example, whenever you pay your rent, the bill "rent" will be linked to the expense. This way, Firefly III can accurately show you which bills are due and which ones aren\'t. In order to do so, a new rule must be created. Firefly III has filled in some sensible defaults for you. Please make sure these are correct. If these values are correct, Firefly III will automatically link the correct withdrawal to the correct bill. Please check out the triggers to see if they are correct, and add some if they\'re wrong.', - 'new_rule_for_bill_title' => 'Rule for bill ":name"', - 'new_rule_for_bill_description' => 'This rule marks transactions for bill ":name".', + 'rule_action_delete_transaction_choice' => 'DELETE transaction(!)', + 'rule_action_delete_transaction' => 'DELETE transaction(!)', + 'rule_action_set_category' => 'Set category to ":action_value"', + 'rule_action_clear_category' => 'Clear category', + 'rule_action_set_budget' => 'Set budget to ":action_value"', + 'rule_action_clear_budget' => 'Clear budget', + 'rule_action_add_tag' => 'Add tag ":action_value"', + 'rule_action_remove_tag' => 'Remove tag ":action_value"', + 'rule_action_remove_all_tags' => 'Remove all tags', + 'rule_action_set_description' => 'Set description to ":action_value"', + 'rule_action_append_description' => 'Append description with ":action_value"', + 'rule_action_prepend_description' => 'Prepend description with ":action_value"', + 'rule_action_set_category_choice' => 'Set category to ..', + 'rule_action_clear_category_choice' => 'Clear any category', + 'rule_action_set_budget_choice' => 'Set budget to ..', + 'rule_action_clear_budget_choice' => 'Clear any budget', + 'rule_action_add_tag_choice' => 'Add tag ..', + 'rule_action_remove_tag_choice' => 'Remove tag ..', + 'rule_action_remove_all_tags_choice' => 'Remove all tags', + 'rule_action_set_description_choice' => 'Set description to ..', + 'rule_action_update_piggy_choice' => 'Add / remove transaction amount in piggy bank ..', + 'rule_action_update_piggy' => 'Add / remove transaction amount in piggy bank ":action_value"', + 'rule_action_append_description_choice' => 'Append description with ..', + 'rule_action_prepend_description_choice' => 'Prepend description with ..', + 'rule_action_set_source_account_choice' => 'Set source account to ..', + 'rule_action_set_source_account' => 'Set source account to :action_value', + 'rule_action_set_destination_account_choice' => 'Set destination account to ..', + 'rule_action_set_destination_account' => 'Set destination account to :action_value', + 'rule_action_append_notes_choice' => 'Append notes with ..', + 'rule_action_append_notes' => 'Append notes with ":action_value"', + 'rule_action_prepend_notes_choice' => 'Prepend notes with ..', + 'rule_action_prepend_notes' => 'Prepend notes with ":action_value"', + 'rule_action_clear_notes_choice' => 'Remove any notes', + 'rule_action_clear_notes' => 'Remove any notes', + 'rule_action_set_notes_choice' => 'Set notes to ..', + 'rule_action_link_to_bill_choice' => 'Link to a subscription ..', + 'rule_action_link_to_bill' => 'Link to subscription ":action_value"', + 'rule_action_switch_accounts_choice' => 'Switch source and destination accounts (transfers only!)', + 'rule_action_switch_accounts' => 'Switch source and destination', + '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"', + 'rule_action_append_descr_to_notes_choice' => 'Append the description to the transaction notes', + 'rule_action_append_notes_to_descr_choice' => 'Append the transaction notes to the description', + 'rule_action_move_descr_to_notes_choice' => 'Replace the current transaction notes with the description', + 'rule_action_move_notes_to_descr_choice' => 'Replace the current description with the transaction notes', + 'rule_action_append_descr_to_notes' => 'Append description to notes', + 'rule_action_append_notes_to_descr' => 'Append notes to description', + 'rule_action_move_descr_to_notes' => 'Replace notes with description', + 'rule_action_move_notes_to_descr' => 'Replace description with notes', + 'rule_action_set_amount_choice' => 'Set amount to ..', + 'rule_action_set_amount' => 'Set amount to ":action_value"', + 'rule_action_set_destination_to_cash_choice' => 'Set destination account to (cash)', + 'rule_action_set_source_to_cash_choice' => 'Set source account to (cash)', + 'rulegroup_for_bills_title' => 'Rule group for subscriptions', + 'rulegroup_for_bills_description' => 'A special rule group for all the rules that involve subscriptions.', + 'rule_for_bill_title' => 'Auto-generated rule for subscription ":name"', + 'rule_for_bill_description' => 'This rule is auto-generated to try to match subscription ":name".', + 'create_rule_for_bill' => 'Create a new rule for subscription ":name"', + 'create_rule_for_bill_txt' => 'You have just created a new subscription called ":name", congratulations!Firefly III can automagically match new withdrawals to this subscription. For example, whenever you pay your rent, the subscription "rent" will be linked to the expense. This way, Firefly III can accurately show you which subscriptions are due and which ones aren\'t. In order to do so, a new rule must be created. Firefly III has filled in some sensible defaults for you. Please make sure these are correct. If these values are correct, Firefly III will automatically link the correct withdrawal to the correct subscription. Please check out the triggers to see if they are correct, and add some if they\'re wrong.', + 'new_rule_for_bill_title' => 'Rule for subscription ":name"', + 'new_rule_for_bill_description' => 'This rule marks transactions for subscription ":name".', - 'new_rule_for_journal_title' => 'Rule based on transaction ":description"', - 'new_rule_for_journal_description' => 'This rule is based on transaction ":description". It will match transactions that are exactly the same.', + 'new_rule_for_journal_title' => 'Rule based on transaction ":description"', + 'new_rule_for_journal_description' => 'This rule is based on transaction ":description". It will match transactions that are exactly the same.', // tags - 'store_new_tag' => 'Store new tag', - 'update_tag' => 'Update tag', - 'no_location_set' => 'No location set.', - 'meta_data' => 'Meta data', - 'location' => 'Location', - 'location_first_split' => 'The location for this transaction can be set on the first split of this transaction.', - 'without_date' => 'Without date', - 'result' => 'Result', - 'sums_apply_to_range' => 'All sums apply to the selected range', - 'mapbox_api_key' => 'To use map, get an API key from Mapbox. Open your .env file and enter this code after MAPBOX_API_KEY=.', - 'press_object_location' => 'Right click or long press to set the object\'s location.', - 'click_tap_location' => 'Click or tap the map to add a location', - 'clear_location' => 'Clear location', - 'delete_all_selected_tags' => 'Delete all selected tags', - 'select_tags_to_delete' => 'Don\'t forget to select some tags.', - 'deleted_x_tags' => 'Deleted :count tag.|Deleted :count tags.', - 'create_rule_from_transaction' => 'Create rule based on transaction', - 'create_recurring_from_transaction' => 'Create recurring transaction based on transaction', + 'store_new_tag' => 'Store new tag', + 'update_tag' => 'Update tag', + 'no_location_set' => 'No location set.', + 'meta_data' => 'Meta data', + 'location' => 'Location', + 'location_first_split' => 'The location for this transaction can be set on the first split of this transaction.', + 'without_date' => 'Without date', + 'result' => 'Result', + 'sums_apply_to_range' => 'All sums apply to the selected range', + 'mapbox_api_key' => 'To use map, get an API key from Mapbox. Open your .env file and enter this code after MAPBOX_API_KEY=.', + 'press_object_location' => 'Right click or long press to set the object\'s location.', + 'click_tap_location' => 'Click or tap the map to add a location', + 'clear_location' => 'Clear location', + 'delete_all_selected_tags' => 'Delete all selected tags', + 'select_tags_to_delete' => 'Don\'t forget to select some tags.', + 'deleted_x_tags' => 'Deleted :count tag.|Deleted :count tags.', + 'create_rule_from_transaction' => 'Create rule based on transaction', + 'create_recurring_from_transaction' => 'Create recurring transaction based on transaction', // preferences - 'dark_mode_option_browser' => 'Let your browser decide', - 'dark_mode_option_light' => 'Always light', - 'dark_mode_option_dark' => 'Always dark', - 'equal_to_language' => '(equal to language)', - 'dark_mode_preference' => 'Dark mode', - 'dark_mode_preference_help' => 'Tell Firefly III when to use dark mode.', - 'pref_home_screen_accounts' => 'Home screen accounts', - 'pref_home_screen_accounts_help' => 'Which accounts should be displayed on the home page?', - 'pref_view_range' => 'View range', - 'pref_view_range_help' => 'Some charts are automatically grouped in periods. Your budgets will also be grouped in periods. What period would you prefer?', - 'pref_1D' => 'One day', - 'pref_1W' => 'One week', - 'pref_1M' => 'One month', - 'pref_3M' => 'Three months (quarter)', - 'pref_6M' => 'Six months', - 'pref_1Y' => 'One year', - 'pref_last365' => 'Last year', - 'pref_last90' => 'Last 90 days', - 'pref_last30' => 'Last 30 days', - 'pref_last7' => 'Last 7 days', - 'pref_YTD' => 'Year to date', - 'pref_QTD' => 'Quarter to date', - 'pref_MTD' => 'Month to date', - 'pref_languages' => 'Languages', - 'pref_locale' => 'Locale settings', - 'pref_languages_help' => 'Firefly III supports several languages. Which one do you prefer?', - 'pref_locale_help' => 'Firefly III allows you to set other local settings, like how currencies, numbers and dates are formatted. Entries in this list may not be supported by your system. Firefly III doesn\'t have the correct date settings for every locale; contact me for improvements.', - 'pref_locale_no_demo' => 'This feature won\'t work for the demo user.', - 'pref_custom_fiscal_year' => 'Fiscal year settings', - 'pref_custom_fiscal_year_label' => 'Enabled', - 'pref_custom_fiscal_year_help' => 'In countries that use a financial year other than January 1 to December 31, you can switch this on and specify start / end days of the fiscal year', - 'pref_fiscal_year_start_label' => 'Fiscal year start date', - 'pref_two_factor_auth' => 'Multi-factor authentication', - 'pref_two_factor_auth_help' => 'When you enable multi-factor authentication (also known as two-factor authentication), you add an extra layer of security to your account. You sign in with something you know (your password) and something you have (a verification code). Verification codes are generated by an application on your phone, such as Authy or Google Authenticator.', - 'pref_enable_two_factor_auth' => 'Enable multi-factor authentication', - 'pref_two_factor_auth_disabled' => 'Multi-factor authentication verification code removed and disabled', - 'pref_two_factor_auth_remove_it' => 'Don\'t forget to remove the account from your authentication app!', - 'pref_two_factor_auth_code' => 'Verify code', - 'pref_two_factor_auth_code_help' => 'Scan the QR code with an application on your phone such as Authy or Google Authenticator and enter the generated code. The QR code changes every time you visit this page. Make sure you use the most recent one.', - 'pref_two_factor_auth_reset_code' => 'Reset verification code', - 'pref_two_factor_auth_disable_2fa' => 'Disable MFA', - '2fa_use_secret_instead' => 'If you cannot scan the QR code, feel free to use the secret instead: :secret.', - '2fa_backup_codes' => 'Store these backup codes for access in case you lose your device.', - '2fa_already_enabled' => 'Multi-factor authentication verification is already enabled.', - 'wrong_mfa_code' => 'This MFA code is not valid.', - 'pref_save_settings' => 'Save settings', - 'saved_preferences' => 'Preferences saved!', - 'preferences_general' => 'General', - 'preferences_frontpage' => 'Home screen', - 'preferences_security' => 'Security', - 'preferences_layout' => 'Layout', - 'preferences_notifications' => 'Notifications', - 'pref_home_show_deposits' => 'Show deposits on the home screen', - 'pref_home_show_deposits_info' => 'The home screen already shows your expense accounts. Should it also show your revenue accounts?', - 'pref_home_do_show_deposits' => 'Yes, show them', - 'successful_count' => 'of which :count successful', - 'list_page_size_title' => 'Page size', - 'list_page_size_help' => 'Any list of things (accounts, transactions, etc) shows at most this many per page.', - 'list_page_size_label' => 'Page size', - 'between_dates' => '(:start and :end)', - 'pref_optional_fields_transaction' => 'Optional fields for transactions', - 'pref_optional_fields_transaction_help' => 'By default not all fields are enabled when creating a new transaction (because of the clutter). Below, you can enable these fields if you think they could be useful for you. Of course, any field that is disabled, but already filled in, will be visible regardless of the setting.', - 'optional_tj_date_fields' => 'Date fields', - 'optional_tj_other_fields' => 'Other fields', - 'optional_tj_attachment_fields' => 'Attachment fields', - 'pref_optional_tj_interest_date' => 'Interest date', - 'pref_optional_tj_book_date' => 'Book date', - 'pref_optional_tj_process_date' => 'Processing date', - 'pref_optional_tj_due_date' => 'Due date', - 'pref_optional_tj_payment_date' => 'Payment date', - 'pref_optional_tj_invoice_date' => 'Invoice date', - 'pref_optional_tj_internal_reference' => 'Internal reference', - 'pref_optional_tj_notes' => 'Notes', - 'pref_optional_tj_attachments' => 'Attachments', - 'pref_optional_tj_external_url' => 'External URL', - 'pref_optional_tj_location' => 'Location', - 'pref_optional_tj_links' => 'Transaction links', - 'optional_field_meta_dates' => 'Dates', - 'optional_field_meta_business' => 'Business', - 'optional_field_attachments' => 'Attachments', - 'optional_field_meta_data' => 'Optional meta data', - 'external_url' => 'External URL', - 'pref_notification_bill_reminder' => 'Reminder about expiring bills', - 'pref_notification_new_access_token' => 'Alert when a new API access token is created', - 'pref_notification_transaction_creation' => 'Alert when a transaction is created automatically', - 'pref_notification_user_login' => 'Alert when you login from a new location', - 'pref_notification_rule_action_failures' => 'Alert when rule actions fail to execute (Slack or Discord only)', - 'pref_notifications' => 'Notifications', - 'pref_notifications_help' => 'Indicate if these are notifications you would like to get. Some notifications may contain sensitive financial information.', - 'slack_webhook_url' => 'Slack Webhook URL', - 'slack_webhook_url_help' => 'If you want Firefly III to notify you using Slack, enter the webhook URL here. Otherwise leave the field blank. If you are an admin, you need to set this URL in the administration as well.', - 'slack_url_label' => 'Slack "incoming webhook" URL', + 'test_notifications_buttons' => 'To test your configuration, use the buttons below. Please note that the buttons have no spam control.', + 'dark_mode_option_browser' => 'Let your browser decide', + 'dark_mode_option_light' => 'Always light', + 'dark_mode_option_dark' => 'Always dark', + 'equal_to_language' => '(equal to language)', + 'dark_mode_preference' => 'Dark mode', + 'dark_mode_preference_help' => 'Tell Firefly III when to use dark mode.', + 'pref_home_screen_accounts' => 'Home screen accounts', + 'pref_home_screen_accounts_help' => 'Which accounts should be displayed on the home page?', + 'pref_view_range' => 'View range', + 'pref_view_range_help' => 'Some charts are automatically grouped in periods. Your budgets will also be grouped in periods. What period would you prefer?', + 'pref_1D' => 'One day', + 'pref_1W' => 'One week', + 'pref_1M' => 'One month', + 'pref_3M' => 'Three months (quarter)', + 'pref_6M' => 'Six months', + 'pref_1Y' => 'One year', + 'pref_last365' => 'Last year', + 'pref_last90' => 'Last 90 days', + 'pref_last30' => 'Last 30 days', + 'pref_last7' => 'Last 7 days', + 'pref_YTD' => 'Year to date', + 'pref_QTD' => 'Quarter to date', + 'pref_MTD' => 'Month to date', + 'pref_languages' => 'Languages', + 'pref_locale' => 'Locale settings', + 'pref_languages_help' => 'Firefly III supports several languages. Which one do you prefer?', + 'pref_locale_help' => 'Firefly III allows you to set other local settings, like how currencies, numbers and dates are formatted. Entries in this list may not be supported by your system. Firefly III doesn\'t have the correct date settings for every locale; contact me for improvements.', + 'pref_locale_no_demo' => 'This feature won\'t work for the demo user.', + 'pref_convert_to_native' => 'Display amounts in your native currency', + 'pref_convert_to_native_help' => 'This option will make Firefly III try to display and show your native currency in as many places as possible, converting amounts where necessary. This sacrifices accuracy for ease of use, because conversion is not always exact. Please verify that Firefly III has the necessary conversion rates on the "exchange rates"-page.', + 'pref_convert_native_help' => 'Display native amounts', + 'pref_custom_fiscal_year' => 'Fiscal year settings', + 'pref_custom_fiscal_year_label' => 'Enabled', + 'pref_custom_fiscal_year_help' => 'In countries that use a financial year other than January 1 to December 31, you can switch this on and specify start / end days of the fiscal year', + 'pref_fiscal_year_start_label' => 'Fiscal year start date', + 'pref_two_factor_auth' => 'Multi-factor authentication', + 'pref_two_factor_auth_help' => 'When you enable multi-factor authentication (also known as two-factor authentication), you add an extra layer of security to your account. You sign in with something you know (your password) and something you have (a verification code). Verification codes are generated by an application on your phone, such as Authy or Google Authenticator.', + 'pref_enable_two_factor_auth' => 'Enable multi-factor authentication', + 'pref_two_factor_auth_disabled' => 'Multi-factor authentication verification code removed and disabled', + 'pref_two_factor_auth_remove_it' => 'Don\'t forget to remove the account from your authentication app!', + 'pref_two_factor_auth_code' => 'Verify code', + 'pref_two_factor_auth_code_help' => 'Scan the QR code with an application on your phone such as Authy or Google Authenticator and enter the generated code. The QR code changes every time you visit this page. Make sure you use the most recent one.', + 'pref_two_factor_auth_reset_code' => 'Reset verification code', + 'pref_two_factor_auth_disable_2fa' => 'Disable MFA', + '2fa_use_secret_instead' => 'If you cannot scan the QR code, feel free to use the secret instead: :secret.', + '2fa_backup_codes' => 'Store these backup codes for access in case you lose your device.', + '2fa_already_enabled' => 'Multi-factor authentication verification is already enabled.', + 'wrong_mfa_code' => 'This MFA code is not valid.', + 'pref_save_settings' => 'Save settings', + 'saved_preferences' => 'Preferences saved!', + 'preferences_general' => 'General', + 'preferences_frontpage' => 'Home screen', + 'preferences_security' => 'Security', + 'preferences_layout' => 'Layout', + 'preferences_notifications' => 'Notifications', + 'pref_home_show_deposits' => 'Show deposits on the home screen', + 'pref_home_show_deposits_info' => 'The home screen already shows your expense accounts. Should it also show your revenue accounts?', + 'pref_home_do_show_deposits' => 'Yes, show them', + 'successful_count' => 'of which :count successful', + 'list_page_size_title' => 'Page size', + 'list_page_size_help' => 'Any list of things (accounts, transactions, etc) shows at most this many per page.', + 'list_page_size_label' => 'Page size', + 'between_dates' => '(:start and :end)', + 'pref_optional_fields_transaction' => 'Optional fields for transactions', + 'pref_optional_fields_transaction_help' => 'By default not all fields are enabled when creating a new transaction (because of the clutter). Below, you can enable these fields if you think they could be useful for you. Of course, any field that is disabled, but already filled in, will be visible regardless of the setting.', + 'optional_tj_date_fields' => 'Date fields', + 'optional_tj_other_fields' => 'Other fields', + 'optional_tj_attachment_fields' => 'Attachment fields', + 'pref_optional_tj_interest_date' => 'Interest date', + 'pref_optional_tj_book_date' => 'Book date', + 'pref_optional_tj_process_date' => 'Processing date', + 'pref_optional_tj_due_date' => 'Due date', + 'pref_optional_tj_payment_date' => 'Payment date', + 'pref_optional_tj_invoice_date' => 'Invoice date', + 'pref_optional_tj_internal_reference' => 'Internal reference', + 'pref_optional_tj_notes' => 'Notes', + 'pref_optional_tj_attachments' => 'Attachments', + 'pref_optional_tj_external_url' => 'External URL', + 'pref_optional_tj_location' => 'Location', + 'pref_optional_tj_links' => 'Transaction links', + 'optional_field_meta_dates' => 'Dates', + 'optional_field_meta_business' => 'Business', + 'optional_field_attachments' => 'Attachments', + 'optional_field_meta_data' => 'Optional meta data', + 'external_url' => 'External URL', + 'pref_notification_login_failure' => 'Login failure', + 'pref_notification_bill_reminder' => 'Reminder about expiring subscriptions', + 'pref_notification_new_access_token' => 'Alert when a new API access token is created', + 'pref_notification_transaction_creation' => 'Alert when a transaction is created automatically', + 'pref_notification_user_login' => 'Alert when you login from a new location', + 'pref_notification_rule_action_failures' => 'Alert when rule actions fail to execute (not over email)', + 'pref_notification_new_password' => 'Your password changed', + 'pref_notification_enabled_mfa' => 'Multi factor authentication is enabled', + 'pref_notification_disabled_mfa' => 'Multi factor authentication is disabled', + 'pref_notification_few_left_mfa' => 'You have just a few backup codes left', + 'pref_notification_no_left_mfa' => 'You have no backup codes left', + 'pref_notification_many_failed_mfa' => 'The multi factor authentication check keeps failing', + 'pref_notification_new_backup_codes' => 'New backup codes have been generated', + 'pref_notifications' => 'Notifications', + 'pref_notifications_help' => 'Indicate if these are notifications you would like to get. Some notifications may contain sensitive financial information.', + 'pref_notifications_settings' => 'Notifications settings', + 'pref_notifications_settings_help' => 'Use these settings to configure your notification channels. Please note that notifications will be sent to ALL channels. Please save your settings FIRST.', + 'slack_url_label' => 'Slack "incoming webhook" URL', + 'discord_url_label' => 'Discord webhook URL', + + // exchange rates + 'menu_exchange_rates_index' => 'Exchange rates', + 'header_exchange_rates' => 'Exchange rates', + 'exchange_rates_intro' => 'Firefly III supports downloading and using exchange rates. Read more about this in the documentation.', + 'exchange_rates_from_to' => 'Between {from} and {to} (and the other way around)', + 'header_exchange_rates_rates' => 'Exchange rates', + 'exchange_rates_intro_rates' => 'Firefly III uses the following exchange rates. The inverse is automatically calculated when it is not provided. If no exchange rate exists for the date of the transaction, Firefly III will go back in time to find one. If none are present, the rate "1" will be used.', + 'header_exchange_rates_table' => 'Table with exchange rates', + 'help_rate_form' => 'On this day, how many {to} will you get for one {from}?', + 'save_new_rate' => 'Save new rate', + 'add_new_rate' => 'Add a new exchange rate', // Financial administrations - 'administration_index' => 'Financial administration', - 'administrations_index_menu' => 'Financial administration(s)', - 'administrations_breadcrumb' => 'Financial administrations', - 'administrations_page_title' => 'Financial administrations', - 'administrations_page_sub_title' => 'Overview', - 'create_administration' => 'Create new administration', - 'administration_owner' => 'Administration owner: {{email}}', - 'administration_you' => 'Your role: {{role}}', - 'other_users_in_admin' => 'Other users in this administration', - 'administrations_create_breadcrumb' => 'Create new financial administration', - 'administrations_page_create_sub_title' => 'Create new financial administration', - 'basic_administration_information' => 'Basic administration information', - 'new_administration_created' => 'New financial administration "{{title}}" has been created', - 'edit_administration_breadcrumb' => 'Edit financial administration ":title"', - 'administrations_page_edit_sub_title' => 'Edit financial administration ":title"', + 'administration_index' => 'Financial administration', + 'administrations_index_menu' => 'Financial administration(s)', + 'administrations_breadcrumb' => 'Financial administrations', + 'administrations_page_title' => 'Financial administrations', + 'administrations_page_sub_title' => 'Overview', + 'create_administration' => 'Create new administration', + 'administration_owner' => 'Administration owner: {{email}}', + 'administration_you' => 'Your role: {{role}}', + 'other_users_in_admin' => 'Other users in this administration', + 'administrations_create_breadcrumb' => 'Create new financial administration', + 'administrations_page_create_sub_title' => 'Create new financial administration', + 'basic_administration_information' => 'Basic administration information', + 'new_administration_created' => 'New financial administration "{{title}}" has been created', + 'edit_administration_breadcrumb' => 'Edit financial administration ":title"', + 'administrations_page_edit_sub_title' => 'Edit financial administration ":title"', // roles - 'administration_role_owner' => 'Owner', - 'administration_role_ro' => 'Read-only', - 'administration_role_mng_trx' => 'Manage transactions', - 'administration_role_mng_meta' => 'Manage classification and meta-data', - 'administration_role_mng_budgets' => 'Manage budgets', - 'administration_role_mng_piggies' => 'Manage piggy banks', - 'administration_role_mng_subscriptions' => 'Manage subscriptions', - 'administration_role_mng_rules' => 'Manage rules', - 'administration_role_mng_recurring' => 'Manage recurring transactions', - 'administration_role_mng_webhooks' => 'Manage webhooks', - 'administration_role_mng_currencies' => 'Manage currencies', - 'administration_role_view_reports' => 'View reports', - 'administration_role_full' => 'Full access', + 'administration_role_owner' => 'Owner', + 'administration_role_ro' => 'Read-only', + 'administration_role_mng_trx' => 'Manage transactions', + 'administration_role_mng_meta' => 'Manage classification and meta-data', + 'administration_role_mng_budgets' => 'Manage budgets', + 'administration_role_mng_piggies' => 'Manage piggy banks', + 'administration_role_mng_subscriptions' => 'Manage subscriptions', + 'administration_role_mng_rules' => 'Manage rules', + 'administration_role_mng_recurring' => 'Manage recurring transactions', + 'administration_role_mng_webhooks' => 'Manage webhooks', + 'administration_role_mng_currencies' => 'Manage currencies', + 'administration_role_view_reports' => 'View reports', + 'administration_role_full' => 'Full access', // mfa - 'enable_mfa' => 'Enable multi-factor authentication', - 'mfa_index_title' => 'Multi-factor authentication', - 'mfa_index_intro' => 'Firefly III supports multi-factor authentication (MFA). You can enable MFA for your account to add an extra layer of security. Applications like Authy, Google Authenticator and FreeOTP can be used to generate the codes you need to log in. Security keys are not supported by Firefly III but you can use a security key as a storage device for your MFA secret.', - 'mfa_index_enabled' => 'Multi-factor authentication is enabled for your account.', - 'mfa_index_disabled' => 'Multi-factor authentication is not enabled for your account.', - 'mfa_index_owner' => 'The owner of this instance will always be able to disable multi-factor authentication for your account.', - 'current_password_confirm_mfa' => 'Enter your current password', - 'mfa_warning_code_changes' => 'You may get a MFA dialog after you entered your password and a MFA code. In that case, please wait for your application to generate a new MFA code, and do not recycle the one you just used.', - 'mfa_already_disabled' => 'Multi-factor authentication is not enabled, so you cannot disable it.', - 'disable_mfa_page' => 'Disable multi-factor authentication', - 'disable_mfa_intro' => 'You can disable multi-factor authentication. To do so, please enter your password and a multi-factor authentication code. If you want to disable multi-factor authentication because you have lost access to your code generator, please refer to the documentation instead.', - 'pref_disable_mfa' => 'Disable multi-factor authentication', - 'mfa_not_enabled' => 'Multi-factor authentication is not enabled.', - 'mfa_backup_codes_intro' => 'Firefly III can generate backup codes for you. These codes can be used to log in when you cannot use your code generator. You can generate a new set of codes at any time. If you generate a new set, the old set will be invalidated.', - 'mfa_backup_codes_quick' => 'If you are very fast coming from the setup page of multi-factor authentication, your app may not have generated a new code yet. Please know that MFA codes can only be used once. Make sure you use a different code from the previous one.', - 'mfa_backup_codes_title' => 'Multi-factor authentication backup codes', - 'mfa_backup_codes_post_title' => 'Multi-factor authentication backup codes', + 'enable_mfa' => 'Enable multi-factor authentication', + 'mfa_index_title' => 'Multi-factor authentication', + 'mfa_index_intro' => 'Firefly III supports multi-factor authentication (MFA). You can enable MFA for your account to add an extra layer of security. Applications like Authy, Google Authenticator and FreeOTP can be used to generate the codes you need to log in. Security keys are not supported by Firefly III but you can use a security key as a storage device for your MFA secret.', + 'mfa_index_enabled' => 'Multi-factor authentication is enabled for your account.', + 'mfa_index_disabled' => 'Multi-factor authentication is not enabled for your account.', + 'mfa_index_owner' => 'The owner of this instance will always be able to disable multi-factor authentication for your account.', + 'current_password_confirm_mfa' => 'Enter your current password', + 'mfa_warning_code_changes' => 'You may get a MFA dialog after you entered your password and a MFA code. In that case, please wait for your application to generate a new MFA code, and do not recycle the one you just used.', + 'mfa_already_disabled' => 'Multi-factor authentication is not enabled, so you cannot disable it.', + 'disable_mfa_page' => 'Disable multi-factor authentication', + 'disable_mfa_intro' => 'You can disable multi-factor authentication. To do so, please enter your password and a multi-factor authentication code. If you want to disable multi-factor authentication because you have lost access to your code generator, please refer to the documentation instead.', + 'pref_disable_mfa' => 'Disable multi-factor authentication', + 'mfa_not_enabled' => 'Multi-factor authentication is not enabled.', + 'mfa_backup_codes_intro' => 'Firefly III can generate backup codes for you. These codes can be used to log in when you cannot use your code generator. You can generate a new set of codes at any time. If you generate a new set, the old set will be invalidated.', + 'mfa_backup_codes_quick' => 'If you are very fast coming from the setup page of multi-factor authentication, your app may not have generated a new code yet. Please know that MFA codes can only be used once. Make sure you use a different code from the previous one.', + 'mfa_backup_codes_title' => 'Multi-factor authentication backup codes', + 'mfa_backup_codes_post_title' => 'Multi-factor authentication backup codes', // profile: - 'manage_mfa_settings' => 'Manage multi-factor authentication settings', - 'purge_data_title' => 'Purge data from Firefly III', - 'purge_data_expl' => '"Purging" means "deleting that which is already deleted". In normal circumstances, Firefly III deletes nothing permanently. It just hides it. The button below deletes all of these previously "deleted" records FOREVER.', - 'delete_stuff_header' => 'Delete and purge data', - 'purge_all_data' => 'Purge all deleted records', - 'purge_data' => 'Purge data', - 'purged_all_records' => 'All deleted records have been purged.', - 'delete_data_title' => 'Delete data from Firefly III', - 'permanent_delete_stuff' => 'You can delete stuff from Firefly III. Using the buttons below means that your items will be removed from view and hidden. There is no undo-button for this, but the items may remain in the database where you can salvage them if necessary.', - 'other_sessions_logged_out' => 'All your other sessions have been logged out.', - 'delete_unused_accounts' => 'Deleting unused accounts will clean your auto-complete lists.', - 'delete_all_unused_accounts' => 'Delete unused accounts', - 'deleted_all_unused_accounts' => 'All unused accounts are deleted', - 'delete_all_budgets' => 'Delete ALL your budgets', - 'delete_all_categories' => 'Delete ALL your categories', - 'delete_all_tags' => 'Delete ALL your tags', - 'delete_all_bills' => 'Delete ALL your bills', - 'delete_all_piggy_banks' => 'Delete ALL your piggy banks', - 'delete_all_rules' => 'Delete ALL your rules', - 'delete_all_recurring' => 'Delete ALL your recurring transactions', - 'delete_all_object_groups' => 'Delete ALL your object groups', - 'delete_all_accounts' => 'Delete ALL your accounts', - 'delete_all_asset_accounts' => 'Delete ALL your asset accounts', - 'delete_all_expense_accounts' => 'Delete ALL your expense accounts', - 'delete_all_revenue_accounts' => 'Delete ALL your revenue accounts', - 'delete_all_liabilities' => 'Delete ALL your liabilities', - 'delete_all_transactions' => 'Delete ALL your transactions', - 'delete_all_withdrawals' => 'Delete ALL your withdrawals', - 'delete_all_deposits' => 'Delete ALL your deposits', - 'delete_all_transfers' => 'Delete ALL your transfers', - 'also_delete_transactions' => 'Deleting accounts will also delete ALL associated withdrawals, deposits and transfers!', - 'deleted_all_budgets' => 'All budgets have been deleted', - 'deleted_all_categories' => 'All categories have been deleted', - 'deleted_all_tags' => 'All tags have been deleted', - 'deleted_all_bills' => 'All bills have been deleted', - 'deleted_all_piggy_banks' => 'All piggy banks have been deleted', - 'deleted_all_rules' => 'All rules and rule groups have been deleted', - 'deleted_all_object_groups' => 'All groups have been deleted', - 'deleted_all_accounts' => 'All accounts have been deleted', - 'deleted_all_asset_accounts' => 'All asset accounts have been deleted', - 'deleted_all_expense_accounts' => 'All expense accounts have been deleted', - 'deleted_all_revenue_accounts' => 'All revenue accounts have been deleted', - 'deleted_all_liabilities' => 'All liabilities have been deleted', - 'deleted_all_transactions' => 'All transactions have been deleted', - 'deleted_all_withdrawals' => 'All withdrawals have been deleted', - 'deleted_all_deposits' => 'All deposits have been deleted', - 'deleted_all_transfers' => 'All transfers have been deleted', - 'deleted_all_recurring' => 'All recurring transactions have been deleted', - 'change_your_password' => 'Change your password', - 'delete_account' => 'Delete account', - 'current_password' => 'Current password', - 'new_password' => 'New password', - 'new_password_again' => 'New password (again)', - 'delete_your_account' => 'Delete your account', - 'delete_your_account_help' => 'Deleting your account will also delete any accounts, transactions, anything you might have saved into Firefly III. It\'ll be GONE.', - 'delete_your_account_password' => 'Enter your password to continue.', - 'password' => 'Password', - 'are_you_sure' => 'Are you sure? You cannot undo this.', - 'are_you_sure_confirm' => 'Are you sure?', - 'delete_account_button' => 'DELETE your account', - 'invalid_current_password' => 'Invalid current password!', - 'password_changed' => 'Password changed!', - 'should_change' => 'The idea is to change your password.', - 'invalid_password' => 'Invalid password!', - 'what_is_pw_security' => 'What is "verify password security"?', - 'secure_pw_title' => 'How to choose a secure password', - 'forgot_password_response' => 'Thank you. If an account exists with this email address, you will find instructions in your inbox.', - 'secure_pw_history' => 'Not a week goes by that you read in the news about a site losing the passwords of its users. Hackers and thieves use these passwords to try to steal your private information. This information is valuable.', - 'secure_pw_ff' => 'Do you use the same password all over the internet? If one site loses your password, hackers have access to all your data. Firefly III relies on you to choose a strong and unique password to protect your financial records.', - 'secure_pw_check_box' => 'To help you do that Firefly III can check if the password you want to use has been stolen in the past. If this is the case, Firefly III advises you NOT to use that password.', - 'secure_pw_working_title' => 'How does it work?', - 'secure_pw_working' => 'By checking the box, Firefly III will send the first five characters of the SHA1 hash of your password to the website of Troy Hunt to see if it is on the list. This will stop you from using unsafe passwords as is recommended in the latest NIST Special Publication on this subject.', - 'secure_pw_should' => 'Should I check the box?', - 'secure_pw_long_password' => 'Yes. Always verify your password is safe.', - 'command_line_token' => 'Command line token', - 'explain_command_line_token' => 'You need this token to perform command line options, such as exporting data. Without it, that sensitive command will not work. Do not share your command line token. Nobody will ask you for this token, not even me. If you fear you lost this, or when you\'re paranoid, regenerate this token using the button.', - 'regenerate_command_line_token' => 'Regenerate command line token', - 'token_regenerated' => 'A new command line token was generated', - 'change_your_email' => 'Change your email address', - 'email_verification' => 'An email message will be sent to your old AND new email address. For security purposes, you will not be able to login until you verify your new email address. If you are unsure if your Firefly III installation is capable of sending email, please do not use this feature. If you are an administrator, you can test this in the Administration.', - 'email_changed_logout' => 'Until you verify your email address, you cannot login.', - 'login_with_new_email' => 'You can now login with your new email address.', - 'login_with_old_email' => 'You can now login with your old email address again.', - 'login_provider_local_only' => 'This action is not available when authenticating through ":login_provider".', - 'external_user_mgt_disabled' => 'This action is not available when Firefly III isn\'t responsible for user management or authentication handling.', - 'external_auth_disabled' => 'This action is not available when Firefly III isn\'t responsible for authentication handling.', - 'delete_local_info_only' => "Because Firefly III isn't responsible for user management or authentication handling, this function will only delete local Firefly III information.", - 'oauth' => 'OAuth', - 'profile_oauth_clients' => 'OAuth Clients', - 'profile_oauth_no_clients' => 'You have not created any OAuth clients.', - 'profile_oauth_clients_external_auth' => 'If you\'re using an external authentication provider like Authelia, OAuth Clients will not work. You can use Personal Access Tokens only.', - 'profile_oauth_clients_header' => 'Clients', - 'profile_oauth_client_id' => 'Client ID', - 'profile_oauth_client_name' => 'Name', - 'profile_oauth_client_secret' => 'Secret', - 'profile_oauth_create_new_client' => 'Create New Client', - 'profile_oauth_create_client' => 'Create Client', - 'profile_oauth_edit_client' => 'Edit Client', - 'profile_oauth_name_help' => 'Something your users will recognize and trust.', - 'profile_oauth_redirect_url' => 'Redirect URL', - 'profile_oauth_redirect_url_help' => 'Your application\'s authorization callback URL.', - 'profile_authorized_apps' => 'Authorized applications', - 'profile_authorized_clients' => 'Authorized clients', - 'profile_scopes' => 'Scopes', - 'profile_revoke' => 'Revoke', - 'profile_oauth_client_secret_title' => 'Client Secret', - 'profile_oauth_client_secret_expl' => 'Here is your new client secret. This is the only time it will be shown so don\'t lose it! You may now use this secret to make API requests.', - 'profile_personal_access_tokens' => 'Personal Access Tokens', - 'profile_personal_access_token' => 'Personal Access Token', - 'profile_oauth_confidential' => 'Confidential', - 'profile_oauth_confidential_help' => 'Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.', - 'profile_personal_access_token_explanation' => 'Here is your new personal access token. This is the only time it will be shown so don\'t lose it! You may now use this token to make API requests.', - 'profile_no_personal_access_token' => 'You have not created any personal access tokens.', - 'profile_create_new_token' => 'Create new token', - 'profile_create_token' => 'Create token', - 'profile_create' => 'Create', - 'profile_save_changes' => 'Save changes', - 'profile_whoops' => 'Whoops!', - 'profile_something_wrong' => 'Something went wrong!', - 'profile_try_again' => 'Something went wrong. Please try again.', - 'amounts' => 'Amounts', - 'multi_account_warning_unknown' => 'Depending on the type of transaction you create, the source and/or destination account of subsequent splits may be overruled by whatever is defined in the first split of the transaction.', - 'multi_account_warning_withdrawal' => 'Keep in mind that the source account of subsequent splits will be overruled by whatever is defined in the first split of the withdrawal.', - 'multi_account_warning_deposit' => 'Keep in mind that the destination account of subsequent splits will be overruled by whatever is defined in the first split of the deposit.', - 'multi_account_warning_transfer' => 'Keep in mind that the source + destination account of subsequent splits will be overruled by whatever is defined in the first split of the transfer.', + 'manage_mfa_settings' => 'Manage multi-factor authentication settings', + 'purge_data_title' => 'Purge data from Firefly III', + 'purge_data_expl' => '"Purging" means "deleting that which is already deleted". In normal circumstances, Firefly III deletes nothing permanently. It just hides it. The button below deletes all of these previously "deleted" records FOREVER.', + 'delete_stuff_header' => 'Delete and purge data', + 'purge_all_data' => 'Purge all deleted records', + 'purge_data' => 'Purge data', + 'purged_all_records' => 'All deleted records have been purged.', + 'delete_data_title' => 'Delete data from Firefly III', + 'permanent_delete_stuff' => 'You can delete stuff from Firefly III. Using the buttons below means that your items will be removed from view and hidden. There is no undo-button for this, but the items may remain in the database where you can salvage them if necessary.', + 'other_sessions_logged_out' => 'All your other sessions have been logged out.', + 'delete_unused_accounts' => 'Deleting unused accounts will clean your auto-complete lists.', + 'delete_all_unused_accounts' => 'Delete unused accounts', + 'deleted_all_unused_accounts' => 'All unused accounts are deleted', + 'delete_all_budgets' => 'Delete ALL your budgets', + 'delete_all_categories' => 'Delete ALL your categories', + 'delete_all_tags' => 'Delete ALL your tags', + 'delete_all_bills' => 'Delete ALL your subscriptions', + 'delete_all_piggy_banks' => 'Delete ALL your piggy banks', + 'delete_all_rules' => 'Delete ALL your rules', + 'delete_all_recurring' => 'Delete ALL your recurring transactions', + 'delete_all_object_groups' => 'Delete ALL your object groups', + 'delete_all_accounts' => 'Delete ALL your accounts', + 'delete_all_asset_accounts' => 'Delete ALL your asset accounts', + 'delete_all_expense_accounts' => 'Delete ALL your expense accounts', + 'delete_all_revenue_accounts' => 'Delete ALL your revenue accounts', + 'delete_all_liabilities' => 'Delete ALL your liabilities', + 'delete_all_transactions' => 'Delete ALL your transactions', + 'delete_all_withdrawals' => 'Delete ALL your withdrawals', + 'delete_all_deposits' => 'Delete ALL your deposits', + 'delete_all_transfers' => 'Delete ALL your transfers', + 'also_delete_transactions' => 'Deleting accounts will also delete ALL associated withdrawals, deposits and transfers!', + 'deleted_all_budgets' => 'All budgets have been deleted', + 'deleted_all_categories' => 'All categories have been deleted', + 'deleted_all_tags' => 'All tags have been deleted', + 'deleted_all_bills' => 'All subscriptions have been deleted', + 'deleted_all_piggy_banks' => 'All piggy banks have been deleted', + 'deleted_all_rules' => 'All rules and rule groups have been deleted', + 'deleted_all_object_groups' => 'All groups have been deleted', + 'deleted_all_accounts' => 'All accounts have been deleted', + 'deleted_all_asset_accounts' => 'All asset accounts have been deleted', + 'deleted_all_expense_accounts' => 'All expense accounts have been deleted', + 'deleted_all_revenue_accounts' => 'All revenue accounts have been deleted', + 'deleted_all_liabilities' => 'All liabilities have been deleted', + 'deleted_all_transactions' => 'All transactions have been deleted', + 'deleted_all_withdrawals' => 'All withdrawals have been deleted', + 'deleted_all_deposits' => 'All deposits have been deleted', + 'deleted_all_transfers' => 'All transfers have been deleted', + 'deleted_all_recurring' => 'All recurring transactions have been deleted', + 'change_your_password' => 'Change your password', + 'delete_account' => 'Delete account', + 'current_password' => 'Current password', + 'new_password' => 'New password', + 'new_password_again' => 'New password (again)', + 'delete_your_account' => 'Delete your account', + 'delete_your_account_help' => 'Deleting your account will also delete any accounts, transactions, anything you might have saved into Firefly III. It\'ll be GONE.', + 'delete_your_account_password' => 'Enter your password to continue.', + 'password' => 'Password', + 'are_you_sure' => 'Are you sure? You cannot undo this.', + 'are_you_sure_confirm' => 'Are you sure?', + 'delete_account_button' => 'DELETE your account', + 'invalid_current_password' => 'Invalid current password!', + 'password_changed' => 'Password changed!', + 'should_change' => 'The idea is to change your password.', + 'invalid_password' => 'Invalid password!', + 'what_is_pw_security' => 'What is "verify password security"?', + 'secure_pw_title' => 'How to choose a secure password', + 'forgot_password_response' => 'Thank you. If an account exists with this email address, you will find instructions in your inbox.', + 'secure_pw_history' => 'Not a week goes by that you read in the news about a site losing the passwords of its users. Hackers and thieves use these passwords to try to steal your private information. This information is valuable.', + 'secure_pw_ff' => 'Do you use the same password all over the internet? If one site loses your password, hackers have access to all your data. Firefly III relies on you to choose a strong and unique password to protect your financial records.', + 'secure_pw_check_box' => 'To help you do that Firefly III can check if the password you want to use has been stolen in the past. If this is the case, Firefly III advises you NOT to use that password.', + 'secure_pw_working_title' => 'How does it work?', + 'secure_pw_working' => 'By checking the box, Firefly III will send the first five characters of the SHA1 hash of your password to the website of Troy Hunt to see if it is on the list. This will stop you from using unsafe passwords as is recommended in the latest NIST Special Publication on this subject.', + 'secure_pw_should' => 'Should I check the box?', + 'secure_pw_long_password' => 'Yes. Always verify your password is safe.', + 'command_line_token' => 'Command line token', + 'explain_command_line_token' => 'You need this token to perform command line options, such as exporting data. Without it, that sensitive command will not work. Do not share your command line token. Nobody will ask you for this token, not even me. If you fear you lost this, or when you\'re paranoid, regenerate this token using the button.', + 'regenerate_command_line_token' => 'Regenerate command line token', + 'token_regenerated' => 'A new command line token was generated', + 'change_your_email' => 'Change your email address', + 'email_verification' => 'An email message will be sent to your old AND new email address. For security purposes, you will not be able to login until you verify your new email address. If you are unsure if your Firefly III installation is capable of sending email, please do not use this feature. If you are an administrator, you can test this in the Administration.', + 'email_changed_logout' => 'Until you verify your email address, you cannot login.', + 'login_with_new_email' => 'You can now login with your new email address.', + 'login_with_old_email' => 'You can now login with your old email address again.', + 'login_provider_local_only' => 'This action is not available when authenticating through ":login_provider".', + 'external_user_mgt_disabled' => 'This action is not available when Firefly III isn\'t responsible for user management or authentication handling.', + 'external_auth_disabled' => 'This action is not available when Firefly III isn\'t responsible for authentication handling.', + 'delete_local_info_only' => "Because Firefly III isn't responsible for user management or authentication handling, this function will only delete local Firefly III information.", + 'oauth' => 'OAuth', + 'profile_oauth_clients' => 'OAuth Clients', + 'profile_oauth_no_clients' => 'You have not created any OAuth clients.', + 'profile_oauth_clients_external_auth' => 'If you\'re using an external authentication provider like Authelia, OAuth Clients will not work. You can use Personal Access Tokens only.', + 'profile_oauth_clients_header' => 'Clients', + 'profile_oauth_client_id' => 'Client ID', + 'profile_oauth_client_name' => 'Name', + 'profile_oauth_client_secret' => 'Secret', + 'profile_oauth_create_new_client' => 'Create New Client', + 'profile_oauth_create_client' => 'Create Client', + 'profile_oauth_edit_client' => 'Edit Client', + 'profile_oauth_name_help' => 'Something your users will recognize and trust.', + 'profile_oauth_redirect_url' => 'Redirect URL', + 'profile_oauth_redirect_url_help' => 'Your application\'s authorization callback URL.', + 'profile_authorized_apps' => 'Authorized applications', + 'profile_authorized_clients' => 'Authorized clients', + 'profile_scopes' => 'Scopes', + 'profile_revoke' => 'Revoke', + 'profile_oauth_client_secret_title' => 'Client Secret', + 'profile_oauth_client_secret_expl' => 'Here is your new client secret. This is the only time it will be shown so don\'t lose it! You may now use this secret to make API requests.', + 'profile_personal_access_tokens' => 'Personal Access Tokens', + 'profile_personal_access_token' => 'Personal Access Token', + 'profile_oauth_confidential' => 'Confidential', + 'profile_oauth_confidential_help' => 'Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.', + 'profile_personal_access_token_explanation' => 'Here is your new personal access token. This is the only time it will be shown so don\'t lose it! You may now use this token to make API requests.', + 'profile_no_personal_access_token' => 'You have not created any personal access tokens.', + 'profile_create_new_token' => 'Create new token', + 'profile_create_token' => 'Create token', + 'profile_create' => 'Create', + 'profile_save_changes' => 'Save changes', + 'profile_whoops' => 'Whoops!', + 'profile_something_wrong' => 'Something went wrong!', + 'profile_try_again' => 'Something went wrong. Please try again.', + 'amounts' => 'Amounts', + 'multi_account_warning_unknown' => 'Depending on the type of transaction you create, the source and/or destination account of subsequent splits may be overruled by whatever is defined in the first split of the transaction.', + 'multi_account_warning_withdrawal' => 'Keep in mind that the source account of subsequent splits will be overruled by whatever is defined in the first split of the withdrawal.', + 'multi_account_warning_deposit' => 'Keep in mind that the destination account of subsequent splits will be overruled by whatever is defined in the first split of the deposit.', + 'multi_account_warning_transfer' => 'Keep in mind that the source + destination account of subsequent splits will be overruled by whatever is defined in the first split of the transfer.', // Ignore this comment // export data: - 'export_data_title' => 'Export data from Firefly III', - 'export_data_menu' => 'Export data', - 'export_data_bc' => 'Export data from Firefly III', - 'export_data_main_title' => 'Export data from Firefly III', - 'export_data_expl' => 'This link allows you to export all transactions + meta data from Firefly III. Please refer to the help (top right (?)-icon) for more information about the process.', - 'export_data_all_transactions' => 'Export all transactions', - 'export_data_advanced_expl' => 'If you need a more advanced or specific type of export, read the help on how to use the console command php artisan help firefly-iii:export-data.', + 'export_data_title' => 'Export data from Firefly III', + 'export_data_menu' => 'Export data', + 'export_data_bc' => 'Export data from Firefly III', + 'export_data_main_title' => 'Export data from Firefly III', + 'export_data_expl' => 'This link allows you to export all transactions + meta data from Firefly III. Please refer to the help (top right (?)-icon) for more information about the process.', + 'export_data_all_transactions' => 'Export all transactions', + 'export_data_advanced_expl' => 'If you need a more advanced or specific type of export, read the help on how to use the console command php artisan help firefly-iii:export-data.', // attachments - 'nr_of_attachments' => 'One attachment|:count attachments', - 'attachments' => 'Attachments', - 'edit_attachment' => 'Edit attachment ":name"', - 'update_attachment' => 'Update attachment', - 'delete_attachment' => 'Delete attachment ":name"', - 'attachment_deleted' => 'Deleted attachment ":name"', - 'liabilities_deleted' => 'Deleted liability ":name"', - 'attachment_updated' => 'Updated attachment ":name"', - 'upload_max_file_size' => 'Maximum file size: :size', - 'list_all_attachments' => 'List of all attachments', + 'nr_of_attachments' => 'One attachment|:count attachments', + 'attachments' => 'Attachments', + 'edit_attachment' => 'Edit attachment ":name"', + 'update_attachment' => 'Update attachment', + 'delete_attachment' => 'Delete attachment ":name"', + 'attachment_deleted' => 'Deleted attachment ":name"', + 'liabilities_deleted' => 'Deleted liability ":name"', + 'attachment_updated' => 'Updated attachment ":name"', + 'upload_max_file_size' => 'Maximum file size: :size', + 'list_all_attachments' => 'List of all attachments', // transaction index - 'is_reconciled_fields_dropped' => 'Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).', - 'title_expenses' => 'Expenses', - 'title_withdrawal' => 'Expenses', - 'title_revenue' => 'Revenue / income', - 'title_deposit' => 'Revenue / income', - 'title_transfer' => 'Transfers', - 'title_transfers' => 'Transfers', - 'submission_options' => 'Submission options', - 'apply_rules_checkbox' => 'Apply rules', - 'fire_webhooks_checkbox' => 'Fire webhooks', - 'select_source_account' => 'Please select or type a valid source account name', - 'select_dest_account' => 'Please select or type a valid destination account name', + 'is_reconciled_fields_dropped' => 'Because this transaction is reconciled, you will not be able to update the accounts, nor the amount(s).', + 'title_expenses' => 'Expenses', + 'title_withdrawal' => 'Expenses', + 'title_revenue' => 'Revenue / income', + 'title_deposit' => 'Revenue / income', + 'title_transfer' => 'Transfers', + 'title_transfers' => 'Transfers', + 'submission_options' => 'Submission options', + 'apply_rules_checkbox' => 'Apply rules', + 'fire_webhooks_checkbox' => 'Fire webhooks', + 'select_source_account' => 'Please select or type a valid source account name', + 'select_dest_account' => 'Please select or type a valid destination account name', // convert stuff: - 'convert_is_already_type_Withdrawal' => 'This transaction is already a withdrawal', - 'convert_is_already_type_Deposit' => 'This transaction is already a deposit', - 'convert_is_already_type_Transfer' => 'This transaction is already a transfer', - 'convert_to_Withdrawal' => 'Convert ":description" to a withdrawal', - 'convert_to_Deposit' => 'Convert ":description" to a deposit', - 'convert_to_Transfer' => 'Convert ":description" to a transfer', - 'convert_options_WithdrawalDeposit' => 'Convert a withdrawal into a deposit', - 'convert_options_WithdrawalTransfer' => 'Convert a withdrawal into a transfer', - 'convert_options_DepositTransfer' => 'Convert a deposit into a transfer', - 'convert_options_DepositWithdrawal' => 'Convert a deposit into a withdrawal', - 'convert_options_TransferWithdrawal' => 'Convert a transfer into a withdrawal', - 'convert_options_TransferDeposit' => 'Convert a transfer into a deposit', - 'convert_Withdrawal_to_deposit' => 'Convert this withdrawal to a deposit', - 'convert_Withdrawal_to_transfer' => 'Convert this withdrawal to a transfer', - 'convert_Deposit_to_withdrawal' => 'Convert this deposit to a withdrawal', - 'convert_Deposit_to_transfer' => 'Convert this deposit to a transfer', - 'convert_Transfer_to_deposit' => 'Convert this transfer to a deposit', - 'convert_Transfer_to_withdrawal' => 'Convert this transfer to a withdrawal', - 'convert_please_set_revenue_source' => 'Please pick the revenue account where the money will come from.', - 'convert_please_set_asset_destination' => 'Please pick the asset account where the money will go to.', - 'convert_please_set_expense_destination' => 'Please pick the expense account where the money will go to.', - 'convert_please_set_asset_source' => 'Please pick the asset account where the money will come from.', - 'convert_expl_w_d' => 'When converting from a withdrawal to a deposit, the money will be deposited into the displayed destination account, instead of being withdrawn from it.|When converting from a withdrawal to a deposit, the money will be deposited into the displayed destination accounts, instead of being withdrawn from them.', - 'convert_expl_w_t' => 'When converting a withdrawal into a transfer, the money will be transferred away from the source account into other asset or liability account instead of being spent on the original expense account.|When converting a withdrawal into a transfer, the money will be transferred away from the source accounts into other asset or liability accounts instead of being spent on the original expense accounts.', - 'convert_expl_d_w' => 'When converting a deposit into a withdrawal, the money will be withdrawn from the displayed source account, instead of being deposited into it.|When converting a deposit into a withdrawal, the money will be withdrawn from the displayed source accounts, instead of being deposited into them.', - 'convert_expl_d_t' => 'When you convert a deposit into a transfer, the money will be deposited into the listed destination account from any of your asset or liability account.|When you convert a deposit into a transfer, the money will be deposited into the listed destination accounts from any of your asset or liability accounts.', - 'convert_expl_t_w' => 'When you convert a transfer into a withdrawal, the money will be spent on the destination account you set here, instead of being transferred away.|When you convert a transfer into a withdrawal, the money will be spent on the destination accounts you set here, instead of being transferred away.', - 'convert_expl_t_d' => 'When you convert a transfer into a deposit, the money will be deposited into the destination account you see here, instead of being transferred into it.|When you convert a transfer into a deposit, the money will be deposited into the destination accounts you see here, instead of being transferred into them.', - 'convert_select_sources' => 'To complete the conversion, please set the new source account below.|To complete the conversion, please set the new source accounts below.', - 'convert_select_destinations' => 'To complete the conversion, please select the new destination account below.|To complete the conversion, please select the new destination accounts below.', - 'converted_to_Withdrawal' => 'The transaction has been converted to a withdrawal', - 'converted_to_Deposit' => 'The transaction has been converted to a deposit', - 'converted_to_Transfer' => 'The transaction has been converted to a transfer', - 'invalid_convert_selection' => 'The account you have selected is already used in this transaction or does not exist.', - 'source_or_dest_invalid' => 'Cannot find the correct transaction details. Conversion is not possible.', - 'convert_to_withdrawal' => 'Convert to a withdrawal', - 'convert_to_deposit' => 'Convert to a deposit', - 'convert_to_transfer' => 'Convert to a transfer', + 'convert_is_already_type_Withdrawal' => 'This transaction is already a withdrawal', + 'convert_is_already_type_Deposit' => 'This transaction is already a deposit', + 'convert_is_already_type_Transfer' => 'This transaction is already a transfer', + 'convert_to_Withdrawal' => 'Convert ":description" to a withdrawal', + 'convert_to_Deposit' => 'Convert ":description" to a deposit', + 'convert_to_Transfer' => 'Convert ":description" to a transfer', + 'convert_options_WithdrawalDeposit' => 'Convert a withdrawal into a deposit', + 'convert_options_WithdrawalTransfer' => 'Convert a withdrawal into a transfer', + 'convert_options_DepositTransfer' => 'Convert a deposit into a transfer', + 'convert_options_DepositWithdrawal' => 'Convert a deposit into a withdrawal', + 'convert_options_TransferWithdrawal' => 'Convert a transfer into a withdrawal', + 'convert_options_TransferDeposit' => 'Convert a transfer into a deposit', + 'convert_Withdrawal_to_deposit' => 'Convert this withdrawal to a deposit', + 'convert_Withdrawal_to_transfer' => 'Convert this withdrawal to a transfer', + 'convert_Deposit_to_withdrawal' => 'Convert this deposit to a withdrawal', + 'convert_Deposit_to_transfer' => 'Convert this deposit to a transfer', + 'convert_Transfer_to_deposit' => 'Convert this transfer to a deposit', + 'convert_Transfer_to_withdrawal' => 'Convert this transfer to a withdrawal', + 'convert_please_set_revenue_source' => 'Please pick the revenue account where the money will come from.', + 'convert_please_set_asset_destination' => 'Please pick the asset account where the money will go to.', + 'convert_please_set_expense_destination' => 'Please pick the expense account where the money will go to.', + 'convert_please_set_asset_source' => 'Please pick the asset account where the money will come from.', + 'convert_expl_w_d' => 'When converting from a withdrawal to a deposit, the money will be deposited into the displayed destination account, instead of being withdrawn from it.|When converting from a withdrawal to a deposit, the money will be deposited into the displayed destination accounts, instead of being withdrawn from them.', + 'convert_expl_w_t' => 'When converting a withdrawal into a transfer, the money will be transferred away from the source account into other asset or liability account instead of being spent on the original expense account.|When converting a withdrawal into a transfer, the money will be transferred away from the source accounts into other asset or liability accounts instead of being spent on the original expense accounts.', + 'convert_expl_d_w' => 'When converting a deposit into a withdrawal, the money will be withdrawn from the displayed source account, instead of being deposited into it.|When converting a deposit into a withdrawal, the money will be withdrawn from the displayed source accounts, instead of being deposited into them.', + 'convert_expl_d_t' => 'When you convert a deposit into a transfer, the money will be deposited into the listed destination account from any of your asset or liability account.|When you convert a deposit into a transfer, the money will be deposited into the listed destination accounts from any of your asset or liability accounts.', + 'convert_expl_t_w' => 'When you convert a transfer into a withdrawal, the money will be spent on the destination account you set here, instead of being transferred away.|When you convert a transfer into a withdrawal, the money will be spent on the destination accounts you set here, instead of being transferred away.', + 'convert_expl_t_d' => 'When you convert a transfer into a deposit, the money will be deposited into the destination account you see here, instead of being transferred into it.|When you convert a transfer into a deposit, the money will be deposited into the destination accounts you see here, instead of being transferred into them.', + 'convert_select_sources' => 'To complete the conversion, please set the new source account below.|To complete the conversion, please set the new source accounts below.', + 'convert_select_destinations' => 'To complete the conversion, please select the new destination account below.|To complete the conversion, please select the new destination accounts below.', + 'converted_to_Withdrawal' => 'The transaction has been converted to a withdrawal', + 'converted_to_Deposit' => 'The transaction has been converted to a deposit', + 'converted_to_Transfer' => 'The transaction has been converted to a transfer', + 'invalid_convert_selection' => 'The account you have selected is already used in this transaction or does not exist.', + 'source_or_dest_invalid' => 'Cannot find the correct transaction details. Conversion is not possible.', + 'convert_to_withdrawal' => 'Convert to a withdrawal', + 'convert_to_deposit' => 'Convert to a deposit', + 'convert_to_transfer' => 'Convert to a transfer', // create new stuff: - 'create_new_withdrawal' => 'Create new withdrawal', - 'create_new_deposit' => 'Create new deposit', - 'create_new_transfer' => 'Create new transfer', - 'create_new_asset' => 'Create new asset account', - 'create_new_liabilities' => 'Create new liability', - 'create_new_expense' => 'Create new expense account', - 'create_new_revenue' => 'Create new revenue account', - 'create_new_piggy_bank' => 'Create new piggy bank', - 'create_new_bill' => 'Create new bill', - 'create_new_subscription' => 'Create new subscription', - 'create_new_rule' => 'Create new rule', + 'create_new_withdrawal' => 'Create new withdrawal', + 'create_new_deposit' => 'Create new deposit', + 'create_new_transfer' => 'Create new transfer', + 'create_new_asset' => 'Create new asset account', + 'create_new_liabilities' => 'Create new liability', + 'create_new_expense' => 'Create new expense account', + 'create_new_revenue' => 'Create new revenue account', + 'create_new_piggy_bank' => 'Create new piggy bank', + 'create_new_bill' => 'Create new subscription', + 'create_new_subscription' => 'Create new subscription', + 'create_new_rule' => 'Create new rule', // currencies: - 'create_currency' => 'Create a new currency', - 'store_currency' => 'Store new currency', - 'update_currency' => 'Update currency', - 'new_default_currency' => '":name" is now the default currency.', - 'default_currency_failed' => 'Could not make ":name" the default currency. Please check the logs.', - 'cannot_delete_currency' => 'Cannot delete :name because it is still in use.', - 'cannot_delete_fallback_currency' => ':name is the system fallback currency and can\'t be deleted.', - 'cannot_disable_currency_journals' => 'Cannot disable :name because transactions are still using it.', - 'cannot_disable_currency_last_left' => 'Cannot disable :name because it is the last enabled currency.', - 'cannot_disable_currency_account_meta' => 'Cannot disable :name because it is used in asset accounts.', - 'cannot_disable_currency_bills' => 'Cannot disable :name because it is used in bills.', - 'cannot_disable_currency_recurring' => 'Cannot disable :name because it is used in recurring transactions.', - 'cannot_disable_currency_available_budgets' => 'Cannot disable :name because it is used in available budgets.', - 'cannot_disable_currency_budget_limits' => 'Cannot disable :name because it is used in budget limits.', - 'cannot_disable_currency_current_default' => 'Cannot disable :name because it is the current default currency.', - 'cannot_disable_currency_system_fallback' => 'Cannot disable :name because it is the system default currency.', - 'disable_EUR_side_effects' => 'The Euro is the system\'s emergency fallback currency. Disabling it may have unintended side-effects and may void your warranty.', - 'deleted_currency' => 'Currency :name deleted', - 'created_currency' => 'Currency :name created', - 'could_not_store_currency' => 'Could not store the new currency.', - 'updated_currency' => 'Currency :name updated', - 'ask_site_owner' => 'Please ask :owner to add, remove or edit currencies.', - 'currencies_intro' => 'Firefly III supports various currencies which you can set and enable here.', - 'make_default_currency' => 'Make default', - 'default_currency' => 'default', - 'currency_is_disabled' => 'Disabled', - 'enable_currency' => 'Enable', - 'disable_currency' => 'Disable', - 'currencies_default_disabled' => 'Most of these currencies are disabled by default. To use them, you must enable them first.', - 'currency_is_now_enabled' => 'Currency ":name" has been enabled', - 'could_not_enable_currency' => 'Could not enable currency ":name". Please review the logs.', - 'currency_is_now_disabled' => 'Currency ":name" has been disabled', - 'could_not_disable_currency' => 'Could not disable currency ":name". Perhaps it is still in use?', + 'create_currency' => 'Create a new currency', + 'store_currency' => 'Store new currency', + 'update_currency' => 'Update currency', + 'new_default_currency' => '":name" is now the default currency.', + 'default_currency_failed' => 'Could not make ":name" the default currency. Please check the logs.', + 'cannot_delete_currency' => 'Cannot delete :name because it is still in use.', + 'cannot_delete_fallback_currency' => ':name is the system fallback currency and can\'t be deleted.', + 'cannot_disable_currency_journals' => 'Cannot disable :name because transactions are still using it.', + 'cannot_disable_currency_last_left' => 'Cannot disable :name because it is the last enabled currency.', + 'cannot_disable_currency_account_meta' => 'Cannot disable :name because it is used in asset accounts.', + 'cannot_disable_currency_bills' => 'Cannot disable :name because it is used in subscriptions.', + 'cannot_disable_currency_recurring' => 'Cannot disable :name because it is used in recurring transactions.', + 'cannot_disable_currency_available_budgets' => 'Cannot disable :name because it is used in available budgets.', + 'cannot_disable_currency_budget_limits' => 'Cannot disable :name because it is used in budget limits.', + 'cannot_disable_currency_current_default' => 'Cannot disable :name because it is the current default currency.', + 'cannot_disable_currency_system_fallback' => 'Cannot disable :name because it is the system default currency.', + 'disable_EUR_side_effects' => 'The Euro is the system\'s emergency fallback currency. Disabling it may have unintended side-effects and may void your warranty.', + 'deleted_currency' => 'Currency :name deleted', + 'created_currency' => 'Currency :name created', + 'could_not_store_currency' => 'Could not store the new currency.', + 'updated_currency' => 'Currency :name updated', + 'ask_site_owner' => 'Please ask :owner to add, remove or edit currencies.', + 'currencies_intro' => 'Firefly III supports various currencies which you can set and enable here.', + 'currencies_switch_default' => 'If you have a large database, switching from one default currency to another may take a moment.', + 'make_default_currency' => 'Make default', + 'default_currency' => 'default', + 'currency_is_disabled' => 'Disabled', + 'enable_currency' => 'Enable', + 'disable_currency' => 'Disable', + 'currencies_default_disabled' => 'Most of these currencies are disabled by default. To use them, you must enable them first.', + 'currency_is_now_enabled' => 'Currency ":name" has been enabled', + 'could_not_enable_currency' => 'Could not enable currency ":name". Please review the logs.', + 'currency_is_now_disabled' => 'Currency ":name" has been disabled', + 'could_not_disable_currency' => 'Could not disable currency ":name". Perhaps it is still in use?', // forms: - 'mandatoryFields' => 'Mandatory fields', - 'optionalFields' => 'Optional fields', - 'options' => 'Options', + 'mandatoryFields' => 'Mandatory fields', + 'optionalFields' => 'Optional fields', + 'options' => 'Options', // budgets: - 'daily_budgets' => 'Daily budgets', - 'weekly_budgets' => 'Weekly budgets', - 'monthly_budgets' => 'Monthly budgets', - 'quarterly_budgets' => 'Quarterly budgets', - 'half_year_budgets' => 'Half-yearly budgets', - 'yearly_budgets' => 'Yearly budgets', - 'other_budgets' => 'Custom timed budgets', - 'budget_limit_not_in_range' => 'This amount applies from :start to :end:', - 'total_available_budget' => 'Total available budget (between :start and :end)', - 'total_available_budget_in_currency' => 'Total available budget in :currency', - 'see_below' => 'see below', - 'create_new_budget' => 'Create a new budget', - 'store_new_budget' => 'Store new budget', - 'stored_new_budget' => 'Stored new budget ":name"', - 'available_between' => 'Available between :start and :end', - 'transactionsWithoutBudget' => 'Expenses without budget', - 'transactions_no_budget' => 'Expenses without budget between :start and :end', - 'spent_between' => 'Already spent between :start and :end', - 'spent_between_left' => 'Spent :spent between :start and :end, leaving :left.', - 'set_available_amount' => 'Set available amount', - 'update_available_amount' => 'Update available amount', - 'ab_basic_modal_explain' => 'Use this form to indicate how much you expect to be able to budget (in total, in :currency) in the indicated period.', - 'createBudget' => 'New budget', - 'invalid_currency' => 'This is an invalid currency', - 'invalid_amount' => 'Please enter an amount', - 'set_ab' => 'The available budget amount has been set', - 'updated_ab' => 'The available budget amount has been updated', - 'deleted_ab' => 'The available budget amount has been deleted', - 'deleted_bl' => 'The budgeted amount has been removed', - 'alt_currency_ab_create' => 'Set the available budget in another currency', - 'bl_create_btn' => 'Set budget in another currency', - 'inactiveBudgets' => 'Inactive budgets', - 'without_budget_between' => 'Transactions without a budget between :start and :end', - 'delete_budget' => 'Delete budget ":name"', - 'deleted_budget' => 'Deleted budget ":name"', - 'edit_budget' => 'Edit budget ":name"', - 'updated_budget' => 'Updated budget ":name"', - 'update_amount' => 'Update amount', - 'update_budget' => 'Update budget', - 'update_budget_amount_range' => 'Update (expected) available amount between :start and :end', - 'set_budget_limit_title' => 'Set budgeted amount for budget :budget between :start and :end', - 'set_budget_limit' => 'Set budgeted amount', - 'budget_period_navigator' => 'Period navigator', - 'info_on_available_amount' => 'What do I have available?', - 'available_amount_indication' => 'Use these amounts to get an indication of what your total budget could be.', - 'suggested' => 'Suggested', - 'average_between' => 'Average between :start and :end', - 'transferred_in' => 'Transferred (in)', - 'transferred_away' => 'Transferred (away)', - 'auto_budget_none' => 'No auto-budget', - 'auto_budget_reset' => 'Set a fixed amount every period', - 'auto_budget_rollover' => 'Add an amount every period', - 'auto_budget_adjusted' => 'Add an amount every period and correct for overspending', - 'auto_budget_period_daily' => 'Daily', - 'auto_budget_period_weekly' => 'Weekly', - 'auto_budget_period_monthly' => 'Monthly', - 'auto_budget_period_quarterly' => 'Quarterly', - 'auto_budget_period_half_year' => 'Every half year', - 'auto_budget_period_yearly' => 'Yearly', - 'auto_budget_help' => 'You can read more about this feature in the help. Click the top-right (?) icon.', - 'auto_budget_reset_icon' => 'This budget will be set periodically', - 'auto_budget_rollover_icon' => 'The budget amount will increase periodically', - 'auto_budget_adjusted_icon' => 'The budget amount will increase periodically and will correct for overspending', - 'remove_budgeted_amount' => 'Remove budgeted amount in :currency', + 'daily_budgets' => 'Daily budgets', + 'weekly_budgets' => 'Weekly budgets', + 'monthly_budgets' => 'Monthly budgets', + 'quarterly_budgets' => 'Quarterly budgets', + 'half_year_budgets' => 'Half-yearly budgets', + 'yearly_budgets' => 'Yearly budgets', + 'other_budgets' => 'Custom timed budgets', + 'budget_limit_not_in_range' => 'This amount applies from :start to :end:', + 'total_available_budget' => 'Total available budget (between :start and :end)', + 'total_available_budget_in_currency' => 'Total available budget in :currency', + 'see_below' => 'see below', + 'create_new_budget' => 'Create a new budget', + 'store_new_budget' => 'Store new budget', + 'stored_new_budget' => 'Stored new budget ":name"', + 'available_between' => 'Available between :start and :end', + 'transactionsWithoutBudget' => 'Expenses without budget', + 'transactions_no_budget' => 'Expenses without budget between :start and :end', + 'spent_between' => 'Already spent between :start and :end', + 'spent_between_left' => 'Spent :spent between :start and :end, leaving :left.', + 'set_available_amount' => 'Set available amount', + 'update_available_amount' => 'Update available amount', + 'ab_basic_modal_explain' => 'Use this form to indicate how much you expect to be able to budget (in total, in :currency) in the indicated period.', + 'createBudget' => 'New budget', + 'invalid_currency' => 'This is an invalid currency', + 'invalid_amount' => 'Please enter an amount', + 'set_ab' => 'The available budget amount has been set', + 'updated_ab' => 'The available budget amount has been updated', + 'deleted_ab' => 'The available budget amount has been deleted', + 'deleted_bl' => 'The budgeted amount has been removed', + 'alt_currency_ab_create' => 'Set the available budget in another currency', + 'bl_create_btn' => 'Set budget in another currency', + 'inactiveBudgets' => 'Inactive budgets', + 'without_budget_between' => 'Transactions without a budget between :start and :end', + 'delete_budget' => 'Delete budget ":name"', + 'deleted_budget' => 'Deleted budget ":name"', + 'edit_budget' => 'Edit budget ":name"', + 'updated_budget' => 'Updated budget ":name"', + 'update_amount' => 'Update amount', + 'update_budget' => 'Update budget', + 'update_budget_amount_range' => 'Update (expected) available amount between :start and :end', + 'set_budget_limit_title' => 'Set budgeted amount for budget :budget between :start and :end', + 'set_budget_limit' => 'Set budgeted amount', + 'budget_period_navigator' => 'Period navigator', + 'info_on_available_amount' => 'What do I have available?', + 'available_amount_indication' => 'Use these amounts to get an indication of what your total budget could be.', + 'suggested' => 'Suggested', + 'average_between' => 'Average between :start and :end', + 'transferred_in' => 'Transferred (in)', + 'transferred_away' => 'Transferred (away)', + 'auto_budget_none' => 'No auto-budget', + 'auto_budget_reset' => 'Set a fixed amount every period', + 'auto_budget_rollover' => 'Add an amount every period', + 'auto_budget_adjusted' => 'Add an amount every period and correct for overspending', + 'auto_budget_period_daily' => 'Daily', + 'auto_budget_period_weekly' => 'Weekly', + 'auto_budget_period_monthly' => 'Monthly', + 'auto_budget_period_quarterly' => 'Quarterly', + 'auto_budget_period_half_year' => 'Every half year', + 'auto_budget_period_yearly' => 'Yearly', + 'auto_budget_help' => 'You can read more about this feature in the help. Click the top-right (?) icon.', + 'auto_budget_reset_icon' => 'This budget will be set periodically', + 'auto_budget_rollover_icon' => 'The budget amount will increase periodically', + 'auto_budget_adjusted_icon' => 'The budget amount will increase periodically and will correct for overspending', + 'remove_budgeted_amount' => 'Remove budgeted amount in :currency', // bills: - 'subscription' => 'Subscription', - 'not_expected_period' => 'Not expected this period', - 'subscriptions_in_group' => 'Subscriptions in group "%{title}"', - 'subscr_expected_x_times' => 'Expect to pay %{amount} %{times} times this period', - 'not_or_not_yet' => 'Not (yet)', - 'visit_bill' => 'Visit bill ":name" at Firefly III', - 'match_between_amounts' => 'Bill matches transactions between :low and :high.', - 'running_again_loss' => 'Previously linked transactions to this bill may lose their connection, if they (no longer) match the rule(s).', - 'bill_related_rules' => 'Rules related to this bill', - 'repeats' => 'Repeats', - 'bill_end_date_help' => 'Optional field. The bill is expected to end on this date.', - 'bill_extension_date_help' => 'Optional field. The bill must be extended (or cancelled) on or before this date.', - 'bill_end_index_line' => 'This bill ends on :date', - 'bill_extension_index_line' => 'This bill must be extended or cancelled on :date', - 'connected_journals' => 'Connected transactions', - 'auto_match_on' => 'Automatically matched by Firefly III', - 'auto_match_off' => 'Not automatically matched by Firefly III', - 'next_expected_match' => 'Next expected match', - 'delete_bill' => 'Delete bill ":name"', - 'deleted_bill' => 'Deleted bill ":name"', - 'edit_bill' => 'Edit bill ":name"', - 'more' => 'More', - 'rescan_old' => 'Run rules again, on all transactions', - 'update_bill' => 'Update bill', - 'updated_bill' => 'Updated bill ":name"', - 'store_new_bill' => 'Store new bill', - 'stored_new_bill' => 'Stored new bill ":name"', - 'cannot_scan_inactive_bill' => 'Inactive bills cannot be scanned.', - 'rescanned_bill' => 'Rescanned everything, and linked :count transaction to the bill.|Rescanned everything, and linked :count transactions to the bill.', - 'average_bill_amount_year' => 'Average bill amount (:year)', - 'average_bill_amount_overall' => 'Average bill amount (overall)', - 'bill_is_active' => 'Bill is active', - 'bill_expected_between' => 'Expected between :start and :end', - 'bill_will_automatch' => 'Bill will automatically linked to matching transactions', - 'skips_over' => 'skips over', - 'bill_store_error' => 'An unexpected error occurred while storing your new bill. Please check the log files', - 'list_inactive_rule' => 'inactive rule', - 'bill_edit_rules' => 'Firefly III will attempt to edit the rule related to this bill as well. If you\'ve edited this rule yourself however, Firefly III won\'t change anything.|Firefly III will attempt to edit the :count rules related to this bill as well. If you\'ve edited these rules yourself however, Firefly III won\'t change anything.', - 'bill_expected_date' => 'Expected :date', - 'bill_expected_date_js' => 'Expected {date}', - 'expected_amount' => '(Expected) amount', - 'bill_paid_on' => 'Paid on {date}', - 'bill_repeats_weekly' => 'Repeats weekly', - 'bill_repeats_monthly' => 'Repeats monthly', - 'bill_repeats_quarterly' => 'Repeats quarterly', - 'bill_repeats_half-year' => 'Repeats every half year', - 'bill_repeats_yearly' => 'Repeats yearly', - 'bill_repeats_weekly_other' => 'Repeats every other week', - 'bill_repeats_monthly_other' => 'Repeats every other month', - 'bill_repeats_quarterly_other' => 'Repeats every other quarter', - 'bill_repeats_half-year_other' => 'Repeats yearly', - 'bill_repeats_yearly_other' => 'Repeats every other year', - 'bill_repeats_weekly_skip' => 'Repeats every {skip} weeks', - 'bill_repeats_monthly_skip' => 'Repeats every {skip} months', - 'bill_repeats_quarterly_skip' => 'Repeats every {skip} quarters', - 'bill_repeats_half-year_skip' => 'Repeats every {skip} half years', - 'bill_repeats_yearly_skip' => 'Repeats every {skip} years', - 'subscriptions' => 'Subscriptions', - 'go_to_subscriptions' => 'Go to your subscriptions', - 'forever' => 'Forever', - 'extension_date_is' => 'Extension date is {date}', + 'subscription' => 'Subscription', + 'not_expected_period' => 'Not expected this period', + 'subscriptions_in_group' => 'Subscriptions in group "%{title}"', + 'subscr_expected_x_times' => 'Expect to pay %{amount} %{times} times this period', + 'not_or_not_yet' => 'Not (yet)', + 'visit_bill' => 'Visit subscription ":name" at Firefly III', + 'match_between_amounts' => 'Subscription matches transactions between :low and :high.', + 'running_again_loss' => 'Previously linked transactions to this subscription may lose their connection, if they (no longer) match the rule(s).', + 'bill_related_rules' => 'Rules related to this subscription', + 'repeats' => 'Repeats', + 'bill_end_date_help' => 'Optional field. The subscription is expected to end on this date.', + 'bill_extension_date_help' => 'Optional field. The subscription must be extended (or cancelled) on or before this date.', + 'bill_end_index_line' => 'This subscription ends on :date', + 'bill_extension_index_line' => 'This subscription must be extended or cancelled on :date', + 'connected_journals' => 'Connected transactions', + 'auto_match_on' => 'Automatically matched by Firefly III', + 'auto_match_off' => 'Not automatically matched by Firefly III', + 'next_expected_match' => 'Next expected match', + 'delete_bill' => 'Delete subscription ":name"', + 'deleted_bill' => 'Deleted subscription ":name"', + 'edit_bill' => 'Edit subscription ":name"', + 'more' => 'More', + 'rescan_old' => 'Run rules again, on all transactions', + 'update_bill' => 'Update subscription', + 'updated_bill' => 'Updated subscription ":name"', + 'store_new_bill' => 'Store new subscription', + 'stored_new_bill' => 'Stored new subscription ":name"', + 'cannot_scan_inactive_bill' => 'Inactive subscriptions cannot be scanned.', + 'rescanned_bill' => 'Rescanned everything, and linked :count transaction to the subscription.|Rescanned everything, and linked :count transactions to the subscription.', + 'average_bill_amount_year' => 'Average subscription amount (:year)', + 'average_bill_amount_overall' => 'Average subscription amount (overall)', + 'bill_is_active' => 'Subscription is active', + 'bill_expected_between' => 'Expected between :start and :end', + 'bill_will_automatch' => 'Subscription will automatically linked to matching transactions', + 'skips_over' => 'skips over', + 'bill_store_error' => 'An unexpected error occurred while storing your new subscription. Please check the log files', + 'list_inactive_rule' => 'inactive rule', + 'bill_edit_rules' => 'Firefly III will attempt to edit the rule related to this subscription as well. If you\'ve edited this rule yourself however, Firefly III won\'t change anything.|Firefly III will attempt to edit the :count rules related to this subscription as well. If you\'ve edited these rules yourself however, Firefly III won\'t change anything.', + 'bill_expected_date' => 'Expected :date', + 'bill_expected_date_js' => 'Expected {date}', + 'expected_amount' => '(Expected) amount', + 'bill_paid_on' => 'Paid on {date}', + 'bill_repeats_weekly' => 'Repeats weekly', + 'bill_repeats_monthly' => 'Repeats monthly', + 'bill_repeats_quarterly' => 'Repeats quarterly', + 'bill_repeats_half-year' => 'Repeats every half year', + 'bill_repeats_yearly' => 'Repeats yearly', + 'bill_repeats_weekly_other' => 'Repeats every other week', + 'bill_repeats_monthly_other' => 'Repeats every other month', + 'bill_repeats_quarterly_other' => 'Repeats every other quarter', + 'bill_repeats_half-year_other' => 'Repeats yearly', + 'bill_repeats_yearly_other' => 'Repeats every other year', + 'bill_repeats_weekly_skip' => 'Repeats every {skip} weeks', + 'bill_repeats_monthly_skip' => 'Repeats every {skip} months', + 'bill_repeats_quarterly_skip' => 'Repeats every {skip} quarters', + 'bill_repeats_half-year_skip' => 'Repeats every {skip} half years', + 'bill_repeats_yearly_skip' => 'Repeats every {skip} years', + 'subscriptions' => 'Subscriptions', + 'go_to_subscriptions' => 'Go to your subscriptions', + 'forever' => 'Forever', + 'extension_date_is' => 'Extension date is {date}', // accounts: - 'i_am_owed_amount' => 'I am owed amount', - 'i_owe_amount' => 'I owe amount', - 'inactive_account_link' => 'You have :count inactive (archived) account, which you can view on this separate page.|You have :count inactive (archived) accounts, which you can view on this separate page.', - 'all_accounts_inactive' => 'These are your inactive accounts.', - 'active_account_link' => 'This link goes back to your active accounts.', - 'account_missing_transaction' => 'Account #:id (":name") cannot be viewed directly, but Firefly is missing redirect information.', - 'cc_monthly_payment_date_help' => 'Select any year and any month, it will be ignored anyway. Only the day of the month is relevant.', - 'details_for_asset' => 'Details for asset account ":name"', - 'details_for_expense' => 'Details for expense account ":name"', - 'details_for_revenue' => 'Details for revenue account ":name"', - 'details_for_cash' => 'Details for cash account ":name"', - 'store_new_asset_account' => 'Store new asset account', - 'store_new_expense_account' => 'Store new expense account', - 'store_new_revenue_account' => 'Store new revenue account', - 'edit_asset_account' => 'Edit asset account ":name"', - 'edit_expense_account' => 'Edit expense account ":name"', - 'edit_revenue_account' => 'Edit revenue account ":name"', - 'delete_asset_account' => 'Delete asset account ":name"', - 'delete_expense_account' => 'Delete expense account ":name"', - 'delete_revenue_account' => 'Delete revenue account ":name"', - 'delete_liabilities_account' => 'Delete liability ":name"', - 'asset_deleted' => 'Successfully deleted asset account ":name"', - 'account_deleted' => 'Successfully deleted account ":name"', - 'expense_deleted' => 'Successfully deleted expense account ":name"', - 'revenue_deleted' => 'Successfully deleted revenue account ":name"', - 'update_asset_account' => 'Update asset account', - 'update_undefined_account' => 'Update account', - 'update_liabilities_account' => 'Update liability', - 'update_expense_account' => 'Update expense account', - 'update_revenue_account' => 'Update revenue account', - 'make_new_asset_account' => 'Create a new asset account', - 'make_new_expense_account' => 'Create a new expense account', - 'make_new_revenue_account' => 'Create a new revenue account', - 'make_new_liabilities_account' => 'Create a new liability', - 'asset_accounts' => 'Asset accounts', - 'undefined_accounts' => 'Accounts', - 'asset_accounts_inactive' => 'Asset accounts (inactive)', - 'expense_account' => 'Expense account', - 'expense_accounts' => 'Expense accounts', - 'expense_accounts_inactive' => 'Expense accounts (inactive)', - 'revenue_account' => 'Revenue account', - 'revenue_accounts' => 'Revenue accounts', - 'revenue_accounts_inactive' => 'Revenue accounts (inactive)', - 'cash_accounts' => 'Cash accounts', - 'Cash account' => 'Cash account', - 'liabilities_accounts' => 'Liabilities', - 'liabilities_accounts_inactive' => 'Liabilities (inactive)', - 'reconcile_account' => 'Reconcile account ":account"', - 'overview_of_reconcile_modal' => 'Overview of reconciliation', - 'delete_reconciliation' => 'Delete reconciliation', - 'update_reconciliation' => 'Update reconciliation', - 'amount_cannot_be_zero' => 'The amount cannot be zero', - 'end_of_reconcile_period' => 'End of reconcile period: :period', - 'start_of_reconcile_period' => 'Start of reconcile period: :period', - 'start_balance' => 'Start balance', - 'end_balance' => 'End balance', - 'update_balance_dates_instruction' => 'Match the amounts and dates above to your bank statement, and press "Start reconciling"', - 'select_transactions_instruction' => 'Select the transactions that appear on your bank statement.', - 'select_range_and_balance' => 'First verify the date-range and balances. Then press "Start reconciling"', - 'date_change_instruction' => 'If you change the date range now, any progress will be lost.', - 'update_selection' => 'Update selection', - 'store_reconcile' => 'Store reconciliation', - 'reconciliation_transaction' => 'Reconciliation transaction', - 'Reconciliation' => 'Reconciliation', - 'reconciliation' => 'Reconciliation', - 'reconcile_options' => 'Reconciliation options', - 'reconcile_range' => 'Reconciliation range', - 'start_reconcile' => 'Start reconciling', - 'cash_account_type' => 'Cash', - 'cash' => 'cash', - 'cant_find_redirect_account' => 'Firefly III tried to redirect you but couldn\'t. Sorry about that. Back to the index.', - 'account_type' => 'Account type', - 'save_transactions_by_moving' => 'Save this transaction by moving it to another account:|Save these transactions by moving them to another account:', - 'save_transactions_by_moving_js' => 'No transactions|Save this transaction by moving it to another account. |Save these transactions by moving them to another account.', - 'stored_new_account' => 'New account ":name" stored!', - 'stored_new_account_js' => 'New account "{name}" stored!', - 'updated_account' => 'Updated account ":name"', - 'updated_account_js' => 'Updated account "{title}".', - 'credit_card_options' => 'Credit card options', - 'no_transactions_account' => 'There are no transactions (in this period) for asset account ":name".', - 'no_transactions_period' => 'There are no transactions (in this period).', - 'no_data_for_chart' => 'There is not enough information (yet) to generate this chart.', - 'select_at_least_one_account' => 'Please select at least one asset account', - 'select_at_least_one_category' => 'Please select at least one category', - 'select_at_least_one_budget' => 'Please select at least one budget', - 'select_at_least_one_tag' => 'Please select at least one tag', - 'select_at_least_one_expense' => 'Please select at least one combination of expense/revenue accounts. If you have none (the list is empty) this report is not available.', - 'account_default_currency' => 'This will be the default currency associated with this account.', - 'reconcile_has_more' => 'Your Firefly III ledger has more money in it than your bank claims you should have. There are several options. Please choose what to do. Then, press "Confirm reconciliation".', - 'reconcile_has_less' => 'Your Firefly III ledger has less money in it than your bank claims you should have. There are several options. Please choose what to do. Then, press "Confirm reconciliation".', - 'reconcile_is_equal' => 'Your Firefly III ledger and your bank statements match. There is nothing to do. Please press "Confirm reconciliation" to confirm your input.', - 'create_pos_reconcile_transaction' => 'Clear the selected transactions, and create a correction adding :amount to this asset account.', - 'create_neg_reconcile_transaction' => 'Clear the selected transactions, and create a correction removing :amount from this asset account.', - 'reconcile_do_nothing' => 'Clear the selected transactions, but do not correct.', - 'reconcile_go_back' => 'You can always edit or delete a correction later.', - 'must_be_asset_account' => 'You can only reconcile asset accounts', - 'reconciliation_stored' => 'Reconciliation stored', - 'reconciliation_error' => 'Due to an error the transactions were marked as reconciled but the correction has not been stored: :error.', - 'reconciliation_transaction_title' => 'Reconciliation (:from to :to)', - 'sum_of_reconciliation' => 'Sum of reconciliation', - 'reconcile_this_account' => 'Reconcile this account', - 'reconcile' => 'Reconcile', - 'show' => 'Show', - 'confirm_reconciliation' => 'Confirm reconciliation', - 'submitted_start_balance' => 'Submitted start balance', - 'selected_transactions' => 'Selected transactions (:count)', - 'already_cleared_transactions' => 'Already cleared transactions (:count)', - 'submitted_end_balance' => 'Submitted end balance', - 'initial_balance_description' => 'Initial balance for ":account"', - 'liability_credit_description' => 'Liability credit for ":account"', - 'interest_calc_' => 'unknown', - 'interest_calc_daily' => 'Per day', - 'interest_calc_monthly' => 'Per month', - 'interest_calc_yearly' => 'Per year', - 'interest_calc_weekly' => 'Per week', - 'interest_calc_half-year' => 'Per half year', - 'interest_calc_quarterly' => 'Per quarter', - 'initial_balance_account' => 'Initial balance account of :account', - 'list_options' => 'List options', - 'account_column_opt_drag_and_drop' => 'Drag and drop', - 'account_column_opt_active' => 'Active', - 'account_column_opt_name' => 'Name', - 'account_column_opt_type' => 'Type', - 'account_column_opt_liability_type' => 'Liability type', - 'account_column_opt_liability_direction' => 'Liability direction', - 'account_column_opt_liability_interest' => 'Liability interest', - 'account_column_opt_number' => 'Account number', - 'account_column_opt_current_balance' => 'Current balance', - 'account_column_opt_amount_due' => 'Amount due', - 'account_column_opt_last_activity' => 'Last activity', - 'account_column_opt_balance_difference' => 'Balance difference', - 'account_column_opt_menu' => 'Menu', + 'account_locked_currency' => 'The currency of this account must remain :name as long as piggy banks are linked to it.', + 'i_am_owed_amount' => 'I am owed amount', + 'i_owe_amount' => 'I owe amount', + 'inactive_account_link' => 'You have :count inactive (archived) account, which you can view on this separate page.|You have :count inactive (archived) accounts, which you can view on this separate page.', + 'all_accounts_inactive' => 'These are your inactive accounts.', + 'active_account_link' => 'This link goes back to your active accounts.', + 'account_missing_transaction' => 'Account #:id (":name") cannot be viewed directly, but Firefly is missing redirect information.', + 'cc_monthly_payment_date_help' => 'Select any year and any month, it will be ignored anyway. Only the day of the month is relevant.', + 'details_for_asset' => 'Details for asset account ":name"', + 'details_for_expense' => 'Details for expense account ":name"', + 'details_for_revenue' => 'Details for revenue account ":name"', + 'details_for_cash' => 'Details for cash account ":name"', + 'store_new_asset_account' => 'Store new asset account', + 'store_new_expense_account' => 'Store new expense account', + 'store_new_revenue_account' => 'Store new revenue account', + 'edit_asset_account' => 'Edit asset account ":name"', + 'edit_expense_account' => 'Edit expense account ":name"', + 'edit_revenue_account' => 'Edit revenue account ":name"', + 'delete_asset_account' => 'Delete asset account ":name"', + 'delete_expense_account' => 'Delete expense account ":name"', + 'delete_revenue_account' => 'Delete revenue account ":name"', + 'delete_liabilities_account' => 'Delete liability ":name"', + 'asset_deleted' => 'Successfully deleted asset account ":name"', + 'account_deleted' => 'Successfully deleted account ":name"', + 'expense_deleted' => 'Successfully deleted expense account ":name"', + 'revenue_deleted' => 'Successfully deleted revenue account ":name"', + 'update_asset_account' => 'Update asset account', + 'update_undefined_account' => 'Update account', + 'update_liabilities_account' => 'Update liability', + 'update_expense_account' => 'Update expense account', + 'update_revenue_account' => 'Update revenue account', + 'make_new_asset_account' => 'Create a new asset account', + 'make_new_expense_account' => 'Create a new expense account', + 'make_new_revenue_account' => 'Create a new revenue account', + 'make_new_liabilities_account' => 'Create a new liability', + 'asset_accounts' => 'Asset accounts', + 'undefined_accounts' => 'Accounts', + 'asset_accounts_inactive' => 'Asset accounts (inactive)', + 'expense_account' => 'Expense account', + 'expense_accounts' => 'Expense accounts', + 'expense_accounts_inactive' => 'Expense accounts (inactive)', + 'revenue_account' => 'Revenue account', + 'revenue_accounts' => 'Revenue accounts', + 'revenue_accounts_inactive' => 'Revenue accounts (inactive)', + 'cash_accounts' => 'Cash accounts', + 'Cash account' => 'Cash account', + 'liabilities_accounts' => 'Liabilities', + 'liabilities_accounts_inactive' => 'Liabilities (inactive)', + 'reconcile_account' => 'Reconcile account ":account"', + 'overview_of_reconcile_modal' => 'Overview of reconciliation', + 'delete_reconciliation' => 'Delete reconciliation', + 'update_reconciliation' => 'Update reconciliation', + 'amount_cannot_be_zero' => 'The amount cannot be zero', + 'end_of_reconcile_period' => 'End of reconcile period: :period', + 'start_of_reconcile_period' => 'Start of reconcile period: :period', + 'start_balance' => 'Start balance', + 'end_balance' => 'End balance', + 'update_balance_dates_instruction' => 'Match the amounts and dates above to your bank statement, and press "Start reconciling"', + 'select_transactions_instruction' => 'Select the transactions that appear on your bank statement.', + 'select_range_and_balance' => 'First verify the date-range and balances. Then press "Start reconciling"', + 'date_change_instruction' => 'If you change the date range now, any progress will be lost.', + 'update_selection' => 'Update selection', + 'store_reconcile' => 'Store reconciliation', + 'reconciliation_transaction' => 'Reconciliation transaction', + 'Reconciliation' => 'Reconciliation', + 'reconciliation' => 'Reconciliation', + 'reconcile_options' => 'Reconciliation options', + 'reconcile_range' => 'Reconciliation range', + 'start_reconcile' => 'Start reconciling', + 'cash_account_type' => 'Cash', + 'cash' => 'cash', + 'cant_find_redirect_account' => 'Firefly III tried to redirect you but couldn\'t. Sorry about that. Back to the index.', + 'account_type' => 'Account type', + 'save_transactions_by_moving' => 'Save this transaction by moving it to another account:|Save these transactions by moving them to another account:', + 'save_transactions_by_moving_js' => 'No transactions|Save this transaction by moving it to another account. |Save these transactions by moving them to another account.', + 'stored_new_account' => 'New account ":name" stored!', + 'stored_new_account_js' => 'New account "{name}" stored!', + 'updated_account' => 'Updated account ":name"', + 'updated_account_js' => 'Updated account "{title}".', + 'credit_card_options' => 'Credit card options', + 'no_transactions_account' => 'There are no transactions (in this period) for asset account ":name".', + 'no_transactions_period' => 'There are no transactions (in this period).', + 'no_data_for_chart' => 'There is not enough information (yet) to generate this chart.', + 'select_at_least_one_account' => 'Please select at least one asset account', + 'select_at_least_one_category' => 'Please select at least one category', + 'select_at_least_one_budget' => 'Please select at least one budget', + 'select_at_least_one_tag' => 'Please select at least one tag', + 'select_at_least_one_expense' => 'Please select at least one combination of expense/revenue accounts. If you have none (the list is empty) this report is not available.', + 'account_default_currency' => 'This will be the default currency associated with this account.', + 'piggy_default_currency' => 'Piggy banks can only save money in a single currency.', + 'piggy_account_currency_match' => 'Only accounts that use the previously selected currency will be accepted.', + 'reconcile_has_more' => 'Your Firefly III ledger has more money in it than your bank claims you should have. There are several options. Please choose what to do. Then, press "Confirm reconciliation".', + 'reconcile_has_less' => 'Your Firefly III ledger has less money in it than your bank claims you should have. There are several options. Please choose what to do. Then, press "Confirm reconciliation".', + 'reconcile_is_equal' => 'Your Firefly III ledger and your bank statements match. There is nothing to do. Please press "Confirm reconciliation" to confirm your input.', + 'create_pos_reconcile_transaction' => 'Clear the selected transactions, and create a correction adding :amount to this asset account.', + 'create_neg_reconcile_transaction' => 'Clear the selected transactions, and create a correction removing :amount from this asset account.', + 'reconcile_do_nothing' => 'Clear the selected transactions, but do not correct.', + 'reconcile_go_back' => 'You can always edit or delete a correction later.', + 'must_be_asset_account' => 'You can only reconcile asset accounts', + 'reconciliation_stored' => 'Reconciliation stored', + 'reconciliation_error' => 'Due to an error the transactions were marked as reconciled but the correction has not been stored: :error.', + 'reconciliation_transaction_title' => 'Reconciliation (:from to :to)', + 'sum_of_reconciliation' => 'Sum of reconciliation', + 'reconcile_this_account' => 'Reconcile this account', + 'reconcile' => 'Reconcile', + 'show' => 'Show', + 'confirm_reconciliation' => 'Confirm reconciliation', + 'submitted_start_balance' => 'Submitted start balance', + 'selected_transactions' => 'Selected transactions (:count)', + 'already_cleared_transactions' => 'Already cleared transactions (:count)', + 'submitted_end_balance' => 'Submitted end balance', + 'initial_balance_description' => 'Initial balance for ":account"', + 'liability_credit_description' => 'Liability credit for ":account"', + 'interest_calc_' => 'unknown', + 'interest_calc_daily' => 'Per day', + 'interest_calc_monthly' => 'Per month', + 'interest_calc_yearly' => 'Per year', + 'interest_calc_weekly' => 'Per week', + 'interest_calc_half-year' => 'Per half year', + 'interest_calc_quarterly' => 'Per quarter', + 'initial_balance_account' => 'Initial balance account of :account', + 'list_options' => 'List options', + 'account_column_opt_drag_and_drop' => 'Drag and drop', + 'account_column_opt_active' => 'Active', + 'account_column_opt_name' => 'Name', + 'account_column_opt_type' => 'Type', + 'account_column_opt_liability_type' => 'Liability type', + 'account_column_opt_liability_direction' => 'Liability direction', + 'account_column_opt_liability_interest' => 'Liability interest', + 'account_column_opt_number' => 'Account number', + 'account_column_opt_current_balance' => 'Current balance', + 'account_column_opt_amount_due' => 'Amount due', + 'account_column_opt_last_activity' => 'Last activity', + 'account_column_opt_balance_difference' => 'Balance difference', + 'account_column_opt_menu' => 'Menu', // categories: - 'new_category' => 'New category', - 'create_new_category' => 'Create a new category', - 'without_category' => 'Without a category', - 'update_category' => 'Update category', - 'updated_category' => 'Updated category ":name"', - 'categories' => 'Categories', - 'edit_category' => 'Edit category ":name"', - 'no_category' => '(no category)', - 'unknown_category_plain' => 'No category', - 'category' => 'Category', - 'delete_category' => 'Delete category ":name"', - 'deleted_category' => 'Deleted category ":name"', - 'store_category' => 'Store new category', - 'stored_category' => 'Stored new category ":name"', - 'without_category_between' => 'Without category between :start and :end', + 'new_category' => 'New category', + 'create_new_category' => 'Create a new category', + 'without_category' => 'Without a category', + 'update_category' => 'Update category', + 'updated_category' => 'Updated category ":name"', + 'categories' => 'Categories', + 'edit_category' => 'Edit category ":name"', + 'no_category' => '(no category)', + 'unknown_category_plain' => 'No category', + 'category' => 'Category', + 'delete_category' => 'Delete category ":name"', + 'deleted_category' => 'Deleted category ":name"', + 'store_category' => 'Store new category', + 'stored_category' => 'Stored new category ":name"', + 'without_category_between' => 'Without category between :start and :end', // Ignore this comment // transactions: - 'wait_loading_transaction' => 'Please wait for the form to load', - 'wait_loading_data' => 'Please wait for your information to load...', - 'wait_attachments' => 'Please wait for the attachments to upload.', - 'errors_upload' => 'The upload has failed. Please check your browser console for the error.', - 'amount_foreign_if' => 'Amount in foreign currency, if any', - 'amount_destination_account' => 'Amount in the currency of the destination account', - 'edit_transaction_title' => 'Edit transaction ":description"', - 'unreconcile' => 'Undo reconciliation', - 'update_withdrawal' => 'Update withdrawal', - 'update_deposit' => 'Update deposit', - 'update_transaction' => 'Update transaction', - 'update_transfer' => 'Update transfer', - 'updated_withdrawal' => 'Updated withdrawal ":description"', - 'updated_deposit' => 'Updated deposit ":description"', - 'updated_transfer' => 'Updated transfer ":description"', - 'no_changes_withdrawal' => 'Withdrawal ":description" was not changed.', - 'no_changes_deposit' => 'Deposit ":description" was not changed.', - 'no_changes_transfer' => 'Transfer ":description" was not changed.', - 'delete_withdrawal' => 'Delete withdrawal ":description"', - 'delete_deposit' => 'Delete deposit ":description"', - 'delete_transfer' => 'Delete transfer ":description"', - 'deleted_withdrawal' => 'Successfully deleted withdrawal ":description"', - 'deleted_deposit' => 'Successfully deleted deposit ":description"', - 'deleted_transfer' => 'Successfully deleted transfer ":description"', - 'deleted_reconciliation' => 'Successfully deleted reconciliation transaction ":description"', - 'stored_journal' => 'Successfully created new transaction ":description"', - 'stored_journal_js' => 'Successfully created new transaction "{{description}}"', - 'stored_journal_no_descr' => 'Successfully created your new transaction', - 'updated_journal_no_descr' => 'Successfully updated your transaction', - 'select_transactions' => 'Select transactions', - 'rule_group_select_transactions' => 'Apply ":title" to transactions', - 'rule_select_transactions' => 'Apply ":title" to transactions', - 'stop_selection' => 'Stop selecting transactions', - 'reconcile_selected' => 'Reconcile', - 'mass_delete_journals' => 'Delete a number of transactions', - 'mass_edit_journals' => 'Edit a number of transactions', - 'mass_bulk_journals' => 'Bulk edit a number of transactions', - 'mass_bulk_journals_explain' => 'This form allows you to change properties of the transactions listed below in one sweeping update. All the transactions in the table will be updated when you change the parameters you see here.', - 'part_of_split' => 'This transaction is part of a split transaction. If you have not selected all the splits, you may end up with changing only half the transaction.', - 'bulk_set_new_values' => 'Use the inputs below to set new values. If you leave them empty, they will be made empty for all. Also, note that only withdrawals will be given a budget.', - 'no_bulk_category' => 'Don\'t update category', - 'no_bulk_budget' => 'Don\'t update budget', - 'no_bulk_tags' => 'Don\'t update tag(s)', - 'replace_with_these_tags' => 'Replace with these tags', - 'append_these_tags' => 'Add these tags', - 'mass_edit' => 'Edit selected individually', - 'bulk_edit' => 'Edit selected in bulk', - 'mass_delete' => 'Delete selected', - 'cannot_edit_other_fields' => 'You cannot mass-edit other fields than the ones here, because there is no room to show them. Please follow the link and edit them by one-by-one, if you need to edit these fields.', - 'cannot_change_amount_reconciled' => 'You can\'t change the amount of reconciled transactions.', - 'no_budget' => '(no budget)', - 'no_bill' => '(no bill)', - 'account_per_budget' => 'Account per budget', - 'account_per_category' => 'Account per category', - 'create_new_object' => 'Create', - 'empty' => '(empty)', - 'all_other_budgets' => '(all other budgets)', - 'all_other_accounts' => '(all other accounts)', - 'expense_per_source_account' => 'Expenses per source account', - 'expense_per_destination_account' => 'Expenses per destination account', - 'income_per_destination_account' => 'Income per destination account', - 'spent_in_specific_category' => 'Spent in category ":category"', - 'earned_in_specific_category' => 'Earned in category ":category"', - 'spent_in_specific_tag' => 'Spent in tag ":tag"', - 'earned_in_specific_tag' => 'Earned in tag ":tag"', - 'income_per_source_account' => 'Income per source account', - 'average_spending_per_destination' => 'Average expense per destination account', - 'average_spending_per_source' => 'Average expense per source account', - 'average_earning_per_source' => 'Average earning per source account', - 'average_earning_per_destination' => 'Average earning per destination account', - 'account_per_tag' => 'Account per tag', - 'tag_report_expenses_listed_once' => 'Expenses and income are never listed twice. If a transaction has multiple tags, it may only show up under one of its tags. This list may appear to be missing data, but the amounts will be correct.', - 'double_report_expenses_charted_once' => 'Expenses and income are never displayed twice. If a transaction has multiple tags, it may only show up under one of its tags. This chart may appear to be missing data, but the amounts will be correct.', - 'tag_report_chart_single_tag' => 'This chart applies to a single tag. If a transaction has multiple tags, what you see here may be reflected in the charts of other tags as well.', - 'tag' => 'Tag', - 'no_budget_squared' => '(no budget)', - 'perm-delete-many' => 'Deleting many items in one go can be very disruptive. Please be cautious. You can delete part of a split transaction from this page, so take care.', - 'mass_deleted_transactions_success' => 'Deleted :count transaction.|Deleted :count transactions.', - 'mass_edited_transactions_success' => 'Updated :count transaction.|Updated :count transactions.', - 'opt_group_' => '(no account type)', - 'opt_group_no_account_type' => '(no account type)', - 'opt_group_defaultAsset' => 'Default asset accounts', - 'opt_group_savingAsset' => 'Savings accounts', - 'opt_group_sharedAsset' => 'Shared asset accounts', - 'opt_group_ccAsset' => 'Credit cards', - 'opt_group_cashWalletAsset' => 'Cash wallets', - 'opt_group_expense_account' => 'Expense accounts', - 'opt_group_revenue_account' => 'Revenue accounts', - 'opt_group_l_Loan' => 'Liability: Loan', - 'opt_group_cash_account' => 'Cash account', - 'opt_group_l_Debt' => 'Liability: Debt', - 'opt_group_l_Mortgage' => 'Liability: Mortgage', - 'opt_group_l_Credit card' => 'Liability: Credit card', - 'notes' => 'Notes', - 'unknown_journal_error' => 'Could not store the transaction. Please check the log files.', - 'attachment_not_found' => 'This attachment could not be found.', - 'journal_link_bill' => 'This transaction is linked to bill :name. To remove the connection, uncheck the checkbox. Use rules to connect it to another bill.', - 'transaction_stored_link' => 'Transaction #{ID} ("{title}") has been stored.', - 'transaction_new_stored_link' => 'Transaction #{ID} has been stored.', - 'transaction_updated_link' => 'Transaction #{ID} ("{title}") has been updated.', - 'transaction_updated_no_changes' => 'Transaction #{ID} ("{title}") did not receive any changes.', - 'first_split_decides' => 'The first split determines the value of this field', - 'first_split_overrules_source' => 'The first split may overrule the source account', - 'first_split_overrules_destination' => 'The first split may overrule the destination account', - 'spent_x_of_y' => 'Spent {amount} of {total}', + 'wait_loading_transaction' => 'Please wait for the form to load', + 'wait_loading_data' => 'Please wait for your information to load...', + 'wait_attachments' => 'Please wait for the attachments to upload.', + 'errors_upload' => 'The upload has failed. Please check your browser console for the error.', + 'amount_foreign_if' => 'Amount in foreign currency, if any', + 'amount_destination_account' => 'Amount in the currency of the destination account', + 'edit_transaction_title' => 'Edit transaction ":description"', + 'unreconcile' => 'Undo reconciliation', + 'update_withdrawal' => 'Update withdrawal', + 'update_deposit' => 'Update deposit', + 'update_transaction' => 'Update transaction', + 'update_transfer' => 'Update transfer', + 'updated_withdrawal' => 'Updated withdrawal ":description"', + 'updated_deposit' => 'Updated deposit ":description"', + 'updated_transfer' => 'Updated transfer ":description"', + 'no_changes_withdrawal' => 'Withdrawal ":description" was not changed.', + 'no_changes_deposit' => 'Deposit ":description" was not changed.', + 'no_changes_transfer' => 'Transfer ":description" was not changed.', + 'delete_withdrawal' => 'Delete withdrawal ":description"', + 'delete_deposit' => 'Delete deposit ":description"', + 'delete_transfer' => 'Delete transfer ":description"', + 'deleted_withdrawal' => 'Successfully deleted withdrawal ":description"', + 'deleted_deposit' => 'Successfully deleted deposit ":description"', + 'deleted_transfer' => 'Successfully deleted transfer ":description"', + 'deleted_reconciliation' => 'Successfully deleted reconciliation transaction ":description"', + 'stored_journal' => 'Successfully created new transaction ":description"', + 'stored_journal_js' => 'Successfully created new transaction "{{description}}"', + 'stored_journal_no_descr' => 'Successfully created your new transaction', + 'updated_journal_no_descr' => 'Successfully updated your transaction', + 'select_transactions' => 'Select transactions', + 'rule_group_select_transactions' => 'Apply ":title" to transactions', + 'rule_select_transactions' => 'Apply ":title" to transactions', + 'stop_selection' => 'Stop selecting transactions', + 'reconcile_selected' => 'Reconcile', + 'mass_delete_journals' => 'Delete a number of transactions', + 'mass_edit_journals' => 'Edit a number of transactions', + 'mass_bulk_journals' => 'Bulk edit a number of transactions', + 'mass_bulk_journals_explain' => 'This form allows you to change properties of the transactions listed below in one sweeping update. All the transactions in the table will be updated when you change the parameters you see here.', + 'part_of_split' => 'This transaction is part of a split transaction. If you have not selected all the splits, you may end up with changing only half the transaction.', + 'bulk_set_new_values' => 'Use the inputs below to set new values. If you leave them empty, they will be made empty for all. Also, note that only withdrawals will be given a budget.', + 'no_bulk_category' => 'Don\'t update category', + 'no_bulk_budget' => 'Don\'t update budget', + 'no_bulk_tags' => 'Don\'t update tag(s)', + 'replace_with_these_tags' => 'Replace with these tags', + 'append_these_tags' => 'Add these tags', + 'mass_edit' => 'Edit selected individually', + 'bulk_edit' => 'Edit selected in bulk', + 'mass_delete' => 'Delete selected', + 'cannot_edit_other_fields' => 'You cannot mass-edit other fields than the ones here, because there is no room to show them. Please follow the link and edit them by one-by-one, if you need to edit these fields.', + 'cannot_change_amount_reconciled' => 'You can\'t change the amount of reconciled transactions.', + 'no_budget' => '(no budget)', + 'no_bill' => '(no subscription)', + 'account_per_budget' => 'Account per budget', + 'account_per_category' => 'Account per category', + 'create_new_object' => 'Create', + 'empty' => '(empty)', + 'all_other_budgets' => '(all other budgets)', + 'all_other_accounts' => '(all other accounts)', + 'expense_per_source_account' => 'Expenses per source account', + 'expense_per_destination_account' => 'Expenses per destination account', + 'income_per_destination_account' => 'Income per destination account', + 'spent_in_specific_category' => 'Spent in category ":category"', + 'earned_in_specific_category' => 'Earned in category ":category"', + 'spent_in_specific_tag' => 'Spent in tag ":tag"', + 'earned_in_specific_tag' => 'Earned in tag ":tag"', + 'income_per_source_account' => 'Income per source account', + 'average_spending_per_destination' => 'Average expense per destination account', + 'average_spending_per_source' => 'Average expense per source account', + 'average_earning_per_source' => 'Average earning per source account', + 'average_earning_per_destination' => 'Average earning per destination account', + 'account_per_tag' => 'Account per tag', + 'tag_report_expenses_listed_once' => 'Expenses and income are never listed twice. If a transaction has multiple tags, it may only show up under one of its tags. This list may appear to be missing data, but the amounts will be correct.', + 'double_report_expenses_charted_once' => 'Expenses and income are never displayed twice. If a transaction has multiple tags, it may only show up under one of its tags. This chart may appear to be missing data, but the amounts will be correct.', + 'tag_report_chart_single_tag' => 'This chart applies to a single tag. If a transaction has multiple tags, what you see here may be reflected in the charts of other tags as well.', + 'tag' => 'Tag', + 'no_budget_squared' => '(no budget)', + 'perm-delete-many' => 'Deleting many items in one go can be very disruptive. Please be cautious. You can delete part of a split transaction from this page, so take care.', + 'mass_deleted_transactions_success' => 'Deleted :count transaction.|Deleted :count transactions.', + 'mass_edited_transactions_success' => 'Updated :count transaction.|Updated :count transactions.', + 'opt_group_' => '(no account type)', + 'opt_group_no_account_type' => '(no account type)', + 'opt_group_defaultAsset' => 'Default asset accounts', + 'opt_group_savingAsset' => 'Savings accounts', + 'opt_group_sharedAsset' => 'Shared asset accounts', + 'opt_group_ccAsset' => 'Credit cards', + 'opt_group_cashWalletAsset' => 'Cash wallets', + 'opt_group_expense_account' => 'Expense accounts', + 'opt_group_revenue_account' => 'Revenue accounts', + 'opt_group_l_Loan' => 'Liability: Loan', + 'opt_group_cash_account' => 'Cash account', + 'opt_group_l_Debt' => 'Liability: Debt', + 'opt_group_l_Mortgage' => 'Liability: Mortgage', + 'opt_group_l_Credit card' => 'Liability: Credit card', + 'notes' => 'Notes', + 'view_notes' => 'View notes', + 'set_budget_limit_notes' => 'View the notes for this budgeted amount', + 'edit_bl_notes' => 'Edit notes', + 'update_bl_notes' => 'Update notes', + 'unknown_journal_error' => 'Could not store the transaction. Please check the log files.', + 'attachment_not_found' => 'This attachment could not be found.', + 'journal_link_bill' => 'This transaction is linked to subscription :name. To remove the connection, uncheck the checkbox. Use rules to connect it to another subscription.', + 'transaction_stored_link' => 'Transaction #{ID} ("{title}") has been stored.', + 'transaction_new_stored_link' => 'Transaction #{ID} has been stored.', + 'transaction_updated_link' => 'Transaction #{ID} ("{title}") has been updated.', + 'transaction_updated_no_changes' => 'Transaction #{ID} ("{title}") did not receive any changes.', + 'first_split_decides' => 'The first split determines the value of this field', + 'first_split_overrules_source' => 'The first split may overrule the source account', + 'first_split_overrules_destination' => 'The first split may overrule the destination account', + 'spent_x_of_y' => 'Spent {amount} of {total}', // new user: - 'welcome' => 'Welcome to Firefly III!', - 'submit' => 'Submit', - 'submission' => 'Submission', - 'submit_yes_really' => 'Submit (I know what I\'m doing)', - 'getting_started' => 'Getting started', - 'to_get_started' => 'It is good to see you have successfully installed Firefly III. To get started with this tool please enter your bank\'s name and the balance of your main checking account. Do not worry yet if you have multiple accounts. You can add those later. It\'s just that Firefly III needs something to start with.', - 'savings_balance_text' => 'Firefly III will automatically create a savings account for you. By default, there will be no money in your savings account, but if you tell Firefly III the balance it will be stored as such.', - 'finish_up_new_user' => 'That\'s it! You can continue by pressing Submit. You will be taken to the index of Firefly III.', - 'stored_new_accounts_new_user' => 'Yay! Your new accounts have been stored.', - 'set_preferred_language' => 'If you prefer to use Firefly III in another language, please indicate so here.', - 'language' => 'Language', - 'new_savings_account' => ':bank_name savings account', - 'cash_wallet' => 'Cash wallet', - 'currency_not_present' => 'If the currency you normally use is not listed do not worry. You can create your own currencies under Options > Currencies.', + 'welcome' => 'Welcome to Firefly III!', + 'submit' => 'Submit', + 'submission' => 'Submission', + 'submit_yes_really' => 'Submit (I know what I\'m doing)', + 'getting_started' => 'Getting started', + 'to_get_started' => 'It is good to see you have successfully installed Firefly III. To get started with this tool please enter your bank\'s name and the balance of your main checking account. Do not worry yet if you have multiple accounts. You can add those later. It\'s just that Firefly III needs something to start with.', + 'savings_balance_text' => 'Firefly III will automatically create a savings account for you. By default, there will be no money in your savings account, but if you tell Firefly III the balance it will be stored as such.', + 'finish_up_new_user' => 'That\'s it! You can continue by pressing Submit. You will be taken to the index of Firefly III.', + 'stored_new_accounts_new_user' => 'Yay! Your new accounts have been stored.', + 'set_preferred_language' => 'If you prefer to use Firefly III in another language, please indicate so here.', + 'language' => 'Language', + 'new_savings_account' => ':bank_name savings account', + 'cash_wallet' => 'Cash wallet', + 'currency_not_present' => 'If the currency you normally use is not listed do not worry. You can create your own currencies under Options > Currencies.', // home page: - 'transaction_table_description' => 'A table containing your transactions', - 'opposing_account' => 'Opposing account', - 'yourAccounts' => 'Your accounts', - 'your_accounts' => 'Your account overview', - 'category_overview' => 'Category overview', - 'expense_overview' => 'Expense account overview', - 'revenue_overview' => 'Revenue account overview', - 'budgetsAndSpending' => 'Budgets and spending', - 'budgets_and_spending' => 'Budgets and spending', - 'go_to_budget' => 'Go to budget "{budget}"', - 'go_to_deposits' => 'Go to deposits', - 'go_to_expenses' => 'Go to expenses', - 'savings' => 'Savings', - 'newWithdrawal' => 'New expense', - 'newDeposit' => 'New deposit', - 'newTransfer' => 'New transfer', - 'bills_to_pay' => 'Bills to pay', - 'per_day' => 'Per day', - 'left_to_spend_per_day' => 'Left to spend per day', - 'bills_paid' => 'Bills paid', - 'custom_period' => 'Custom period', - 'reset_to_current' => 'Reset to current period', - 'select_period' => 'Select a period', + 'transaction_table_description' => 'A table containing your transactions', + 'opposing_account' => 'Opposing account', + 'yourAccounts' => 'Your accounts', + 'your_accounts' => 'Your account overview', + 'category_overview' => 'Category overview', + 'expense_overview' => 'Expense account overview', + 'revenue_overview' => 'Revenue account overview', + 'budgetsAndSpending' => 'Budgets and spending', + 'budgets_and_spending' => 'Budgets and spending', + 'go_to_budget' => 'Go to budget "{budget}"', + 'go_to_deposits' => 'Go to deposits', + 'go_to_expenses' => 'Go to expenses', + 'savings' => 'Savings', + 'newWithdrawal' => 'New expense', + 'newDeposit' => 'New deposit', + 'newTransfer' => 'New transfer', + 'bills_to_pay' => 'Subscriptions to pay', + 'per_day' => 'Per day', + 'left_to_spend_per_day' => 'Left to spend per day', + 'bills_paid' => 'Subscriptions paid', + 'custom_period' => 'Custom period', + 'reset_to_current' => 'Reset to current period', + 'select_period' => 'Select a period', // menu and titles, should be recycled as often as possible: - 'currency' => 'Currency', - 'preferences' => 'Preferences', - 'logout' => 'Logout', - 'logout_other_sessions' => 'Logout all other sessions', - 'toggleNavigation' => 'Toggle navigation', - 'toggle_dropdown' => 'Toggle dropdown', - 'searchPlaceholder' => 'Search...', - 'version' => 'Version', - 'dashboard' => 'Dashboard', - 'income_and_expense' => 'Income and expense', - 'all_money' => 'All your money', - 'unknown_source_plain' => 'Unknown source account', - 'unknown_dest_plain' => 'Unknown destination account', - 'unknown_any_plain' => 'Unknown account', - 'unknown_budget_plain' => 'No budget', - 'available_budget' => 'Available budget ({currency})', - 'currencies' => 'Currencies', - 'activity' => 'Activity', - 'usage' => 'Usage', - 'accounts' => 'Accounts', - 'Asset account' => 'Asset account', - 'Default account' => 'Asset account', - 'Expense account' => 'Expense account', - 'Revenue account' => 'Revenue account', - 'Initial balance account' => 'Initial balance account', - 'account_type_Asset account' => 'Asset account', - 'account_type_Expense account' => 'Expense account', - 'account_type_Revenue account' => 'Revenue account', - 'account_type_Debt' => 'Debt', - 'account_type_Loan' => 'Loan', - 'account_type_Mortgage' => 'Mortgage', - 'account_type_debt' => 'Debt', - 'account_type_loan' => 'Loan', - 'account_type_mortgage' => 'Mortgage', - 'account_type_Credit card' => 'Credit card', - 'credit_card_type_monthlyFull' => 'Full payment every month', - 'liability_direction_credit' => 'I am owed this debt', - 'liability_direction_debit' => 'I owe this debt to somebody else', - 'liability_direction_credit_short' => 'Owed this debt', - 'liability_direction_debit_short' => 'Owe this debt', - 'liability_direction__short' => 'Unknown', - 'liability_direction_null_short' => 'Unknown', - 'Liability credit' => 'Liability credit', - 'budgets' => 'Budgets', - 'tags' => 'Tags', - 'reports' => 'Reports', - 'transactions' => 'Transactions', - 'expenses' => 'Expenses', - 'income' => 'Revenue / income', - 'transfers' => 'Transfers', - 'moneyManagement' => 'Money management', - 'money_management' => 'Money management', - 'tools' => 'Tools', - 'piggyBanks' => 'Piggy banks', - 'piggy_banks' => 'Piggy banks', - 'amount_x_of_y' => '{current} of {total}', - 'bills' => 'Bills', - 'withdrawal' => 'Withdrawal', - 'opening_balance' => 'Opening balance', - 'deposit' => 'Deposit', - 'account' => 'Account', - 'transfer' => 'Transfer', - 'Withdrawal' => 'Withdrawal', - 'Deposit' => 'Deposit', - 'Transfer' => 'Transfer', - 'bill' => 'Bill', - 'yes' => 'Yes', - 'no' => 'No', - 'amount' => 'Amount', - 'overview' => 'Overview', - 'saveOnAccount' => 'Save on account', - 'unknown' => 'Unknown', - 'monthly' => 'Monthly', - 'profile' => 'Profile', - 'errors' => 'Errors', - 'debt_start_date' => 'Start date of debt', - 'debt_start_amount' => 'Start amount of debt', - 'debt_start_amount_help' => 'It\'s always best to set this value to a negative amount. Read the help pages (top right (?)-icon) for more information.', - 'interest_period_help' => 'This field is purely cosmetic and won\'t be calculated for you. As it turns out banks are very sneaky so Firefly III never gets it right.', - 'store_new_liabilities_account' => 'Store new liability', - 'edit_liabilities_account' => 'Edit liability ":name"', - 'financial_control' => 'Financial control', - 'accounting' => 'Accounting', - 'automation' => 'Automation', - 'others' => 'Others', - 'classification' => 'Classification', - 'store_transaction' => 'Store transaction', + 'currency' => 'Currency', + 'preferences' => 'Preferences', + 'logout' => 'Logout', + 'logout_other_sessions' => 'Logout all other sessions', + 'toggleNavigation' => 'Toggle navigation', + 'toggle_dropdown' => 'Toggle dropdown', + 'searchPlaceholder' => 'Search...', + 'version' => 'Version', + 'dashboard' => 'Dashboard', + 'income_and_expense' => 'Income and expense', + 'all_money' => 'All your money', + 'unknown_source_plain' => 'Unknown source account', + 'unknown_dest_plain' => 'Unknown destination account', + 'unknown_any_plain' => 'Unknown account', + 'unknown_budget_plain' => 'No budget', + 'available_budget' => 'Available budget ({currency})', + 'currencies' => 'Currencies', + 'activity' => 'Activity', + 'usage' => 'Usage', + 'accounts' => 'Accounts', + 'Asset account' => 'Asset account', + 'Default account' => 'Asset account', + 'Expense account' => 'Expense account', + 'Revenue account' => 'Revenue account', + 'Initial balance account' => 'Initial balance account', + 'account_type_Asset account' => 'Asset account', + 'account_type_Expense account' => 'Expense account', + 'account_type_Revenue account' => 'Revenue account', + 'account_type_Debt' => 'Debt', + 'account_type_Loan' => 'Loan', + 'account_type_Mortgage' => 'Mortgage', + 'account_type_debt' => 'Debt', + 'account_type_loan' => 'Loan', + 'account_type_mortgage' => 'Mortgage', + 'account_type_Credit card' => 'Credit card', + 'credit_card_type_monthlyFull' => 'Full payment every month', + 'liability_direction_credit' => 'I am owed this debt', + 'liability_direction_debit' => 'I owe this debt to somebody else', + 'liability_direction_credit_short' => 'Owed this debt', + 'liability_direction_debit_short' => 'Owe this debt', + 'liability_direction__short' => 'Unknown', + 'liability_direction_null_short' => 'Unknown', + 'Liability credit' => 'Liability credit', + 'budgets' => 'Budgets', + 'tags' => 'Tags', + 'reports' => 'Reports', + 'transactions' => 'Transactions', + 'expenses' => 'Expenses', + 'income' => 'Revenue / income', + 'transfers' => 'Transfers', + 'moneyManagement' => 'Money management', + 'money_management' => 'Money management', + 'tools' => 'Tools', + 'piggyBanks' => 'Piggy banks', + 'piggy_banks' => 'Piggy banks', + 'amount_x_of_y' => '{current} of {total}', + 'bills' => 'Subscriptions', + 'withdrawal' => 'Withdrawal', + 'opening_balance' => 'Opening balance', + 'deposit' => 'Deposit', + 'account' => 'Account', + 'transfer' => 'Transfer', + 'Withdrawal' => 'Withdrawal', + 'Deposit' => 'Deposit', + 'Transfer' => 'Transfer', + 'bill' => 'Subscription', + 'yes' => 'Yes', + 'no' => 'No', + 'amount' => 'Amount', + 'overview' => 'Overview', + 'saveOnAccount' => 'Save on account', + 'saveOnAccounts' => 'Save on account(s)', + 'unknown' => 'Unknown', + 'monthly' => 'Monthly', + 'profile' => 'Profile', + 'errors' => 'Errors', + 'debt_start_date' => 'Start date of debt', + 'debt_start_amount' => 'Start amount of debt', + 'debt_start_amount_help' => 'It\'s always best to set this value to a negative amount. Read the help pages (top right (?)-icon) for more information.', + 'interest_period_help' => 'This field is purely cosmetic and won\'t be calculated for you. As it turns out banks are very sneaky so Firefly III never gets it right.', + 'store_new_liabilities_account' => 'Store new liability', + 'edit_liabilities_account' => 'Edit liability ":name"', + 'financial_control' => 'Financial control', + 'accounting' => 'Accounting', + 'automation' => 'Automation', + 'others' => 'Others', + 'classification' => 'Classification', + 'store_transaction' => 'Store transaction', // Ignore this comment // reports: - 'report_default' => 'Default financial report between :start and :end', - 'report_audit' => 'Transaction history overview between :start and :end', - 'report_category' => 'Category report between :start and :end', - 'report_double' => 'Expense/revenue account report between :start and :end', - 'report_budget' => 'Budget report between :start and :end', - 'report_tag' => 'Tag report between :start and :end', - 'quick_link_reports' => 'Quick links', - 'quick_link_examples' => 'These are just some example links to get you started. Check out the help pages under the (?)-button for information on all reports and the magic words you can use.', - 'quick_link_default_report' => 'Default financial report', - 'quick_link_audit_report' => 'Transaction history overview', - 'report_this_month_quick' => 'Current month, all accounts', - 'report_last_month_quick' => 'Last month, all accounts', - 'report_this_year_quick' => 'Current year, all accounts', - 'report_this_fiscal_year_quick' => 'Current fiscal year, all accounts', - 'report_all_time_quick' => 'All-time, all accounts', - 'reports_can_bookmark' => 'Remember that reports can be bookmarked.', - 'incomeVsExpenses' => 'Income vs. expenses', - 'accountBalances' => 'Account balances', - 'balanceStart' => 'Balance at start of period', - 'balanceEnd' => 'Balance at end of period', - 'splitByAccount' => 'Split by account', - 'coveredWithTags' => 'Covered with tags', - 'leftInBudget' => 'Left in budget', - 'left_in_debt' => 'Amount due', - 'sumOfSums' => 'Sum of sums', - 'noCategory' => '(no category)', - 'notCharged' => 'Not charged (yet)', - 'inactive' => 'Inactive', - 'active' => 'Active', - 'difference' => 'Difference', - 'money_flowing_in' => 'In', - 'money_flowing_out' => 'Out', - 'topX' => 'top :number', - 'show_full_list' => 'Show entire list', - 'show_only_top' => 'Show only top :number', - 'report_type' => 'Report type', - 'report_type_default' => 'Default financial report', - 'report_type_audit' => 'Transaction history overview (audit)', - 'report_type_category' => 'Category report', - 'report_type_budget' => 'Budget report', - 'report_type_tag' => 'Tag report', - 'report_type_double' => 'Expense/revenue account report', - 'more_info_help' => 'More information about these types of reports can be found in the help pages. Press the (?) icon in the top right corner.', - 'report_included_accounts' => 'Included accounts', - 'report_date_range' => 'Date range', - 'report_preset_ranges' => 'Pre-set ranges', - 'shared' => 'Shared', - 'fiscal_year' => 'Fiscal year', - 'income_entry' => 'Income from account ":name" between :start and :end', - 'expense_entry' => 'Expenses to account ":name" between :start and :end', - 'category_entry' => 'Expenses and income in category ":name" between :start and :end', - 'budget_spent_amount' => 'Expenses in budget ":budget" between :start and :end', - 'balance_amount' => 'Expenses in budget ":budget" paid from account ":account" between :start and :end', - 'no_audit_activity' => 'No activity was recorded on account :account_name between :start and :end.', - 'audit_end_balance' => 'Account balance of :account_name at the end of :end was: :balance', - 'reports_extra_options' => 'Extra options', - 'report_has_no_extra_options' => 'This report has no extra options', - 'reports_submit' => 'View report', - 'end_after_start_date' => 'End date of report must be after start date.', - 'select_category' => 'Select category(ies)', - 'select_budget' => 'Select budget(s).', - 'select_tag' => 'Select tag(s).', - 'income_per_category' => 'Income per category', - 'expense_per_category' => 'Expense per category', - 'expense_per_budget' => 'Expense per budget', - 'income_per_account' => 'Income per account', - 'expense_per_account' => 'Expense per account', - 'expense_per_tag' => 'Expense per tag', - 'income_per_tag' => 'Income per tag', - 'include_expense_not_in_budget' => 'Included expenses not in the selected budget(s)', - 'include_expense_not_in_account' => 'Included expenses not in the selected account(s)', - 'include_expense_not_in_category' => 'Included expenses not in the selected category(ies)', - 'include_income_not_in_category' => 'Included income not in the selected category(ies)', - 'include_income_not_in_account' => 'Included income not in the selected account(s)', - 'include_income_not_in_tags' => 'Included income not in the selected tag(s)', - 'include_expense_not_in_tags' => 'Included expenses not in the selected tag(s)', - 'everything_else' => 'Everything else', - 'income_and_expenses' => 'Income and expenses', - 'spent_average' => 'Spent (average)', - 'income_average' => 'Income (average)', - 'transaction_count' => 'Transaction count', - 'average_spending_per_account' => 'Average spending per account', - 'average_income_per_account' => 'Average income per account', - 'total' => 'Total', - 'description' => 'Description', - 'sum_of_period' => 'Sum of period', - 'average_in_period' => 'Average in period', - 'no_account_role' => '(no role)', - 'account_role_defaultAsset' => 'Default asset account', - 'account_role_sharedAsset' => 'Shared asset account', - 'account_role_savingAsset' => 'Savings account', - 'account_role_ccAsset' => 'Credit card', - 'account_role_cashWalletAsset' => 'Cash wallet', - 'budget_chart_click' => 'Please click on a budget name in the table above to see a chart.', - 'category_chart_click' => 'Please click on a category name in the table above to see a chart.', - 'in_out_accounts' => 'Earned and spent per combination', - 'in_out_accounts_per_asset' => 'Earned and spent (per asset account)', - 'in_out_per_category' => 'Earned and spent per category', - 'out_per_budget' => 'Spent per budget', - 'select_expense_revenue' => 'Select expense/revenue account', - 'multi_currency_report_sum' => 'Because this list contains accounts with multiple currencies, the sum(s) you see may not make sense. The report will always fall back to your default currency.', - 'sum_in_default_currency' => 'The sum will always be in your default currency.', - 'net_filtered_prefs' => 'This chart will never include accounts that have the "Include in net worth"-option unchecked.', + 'report_default' => 'Default financial report between :start and :end', + 'report_audit' => 'Transaction history overview between :start and :end', + 'report_category' => 'Category report between :start and :end', + 'report_double' => 'Expense/revenue account report between :start and :end', + 'report_budget' => 'Budget report between :start and :end', + 'report_tag' => 'Tag report between :start and :end', + 'quick_link_reports' => 'Quick links', + 'quick_link_examples' => 'These are just some example links to get you started. Check out the help pages under the (?)-button for information on all reports and the magic words you can use.', + 'quick_link_default_report' => 'Default financial report', + 'quick_link_audit_report' => 'Transaction history overview', + 'report_this_month_quick' => 'Current month, all accounts', + 'report_last_month_quick' => 'Last month, all accounts', + 'report_this_year_quick' => 'Current year, all accounts', + 'report_this_fiscal_year_quick' => 'Current fiscal year, all accounts', + 'report_all_time_quick' => 'All-time, all accounts', + 'reports_can_bookmark' => 'Remember that reports can be bookmarked.', + 'incomeVsExpenses' => 'Income vs. expenses', + 'accountBalances' => 'Account balances', + 'balanceStart' => 'Balance at start of period', + 'balanceEnd' => 'Balance at end of period', + 'splitByAccount' => 'Split by account', + 'coveredWithTags' => 'Covered with tags', + 'leftInBudget' => 'Left in budget', + 'left_in_debt' => 'Amount due', + 'sumOfSums' => 'Sum of sums', + 'noCategory' => '(no category)', + 'notCharged' => 'Not charged (yet)', + 'inactive' => 'Inactive', + 'active' => 'Active', + 'difference' => 'Difference', + 'money_flowing_in' => 'In', + 'money_flowing_out' => 'Out', + 'topX' => 'top :number', + 'show_full_list' => 'Show entire list', + 'show_only_top' => 'Show only top :number', + 'report_type' => 'Report type', + 'report_type_default' => 'Default financial report', + 'report_type_audit' => 'Transaction history overview (audit)', + 'report_type_category' => 'Category report', + 'report_type_budget' => 'Budget report', + 'report_type_tag' => 'Tag report', + 'report_type_double' => 'Expense/revenue account report', + 'more_info_help' => 'More information about these types of reports can be found in the help pages. Press the (?) icon in the top right corner.', + 'report_included_accounts' => 'Included accounts', + 'report_date_range' => 'Date range', + 'report_preset_ranges' => 'Pre-set ranges', + 'shared' => 'Shared', + 'fiscal_year' => 'Fiscal year', + 'income_entry' => 'Income from account ":name" between :start and :end', + 'expense_entry' => 'Expenses to account ":name" between :start and :end', + 'category_entry' => 'Expenses and income in category ":name" between :start and :end', + 'budget_spent_amount' => 'Expenses in budget ":budget" between :start and :end', + 'balance_amount' => 'Expenses in budget ":budget" paid from account ":account" between :start and :end', + 'no_audit_activity' => 'No activity was recorded on account :account_name between :start and :end.', + 'audit_end_balance' => 'Account balance of :account_name at the end of :end was: :balance', + 'reports_extra_options' => 'Extra options', + 'report_has_no_extra_options' => 'This report has no extra options', + 'reports_submit' => 'View report', + 'end_after_start_date' => 'End date of report must be after start date.', + 'select_category' => 'Select category(ies)', + 'select_budget' => 'Select budget(s).', + 'select_tag' => 'Select tag(s).', + 'income_per_category' => 'Income per category', + 'expense_per_category' => 'Expense per category', + 'expense_per_budget' => 'Expense per budget', + 'income_per_account' => 'Income per account', + 'expense_per_account' => 'Expense per account', + 'expense_per_tag' => 'Expense per tag', + 'income_per_tag' => 'Income per tag', + 'include_expense_not_in_budget' => 'Included expenses not in the selected budget(s)', + 'include_expense_not_in_account' => 'Included expenses not in the selected account(s)', + 'include_expense_not_in_category' => 'Included expenses not in the selected category(ies)', + 'include_income_not_in_category' => 'Included income not in the selected category(ies)', + 'include_income_not_in_account' => 'Included income not in the selected account(s)', + 'include_income_not_in_tags' => 'Included income not in the selected tag(s)', + 'include_expense_not_in_tags' => 'Included expenses not in the selected tag(s)', + 'everything_else' => 'Everything else', + 'income_and_expenses' => 'Income and expenses', + 'spent_average' => 'Spent (average)', + 'income_average' => 'Income (average)', + 'transaction_count' => 'Transaction count', + 'average_spending_per_account' => 'Average spending per account', + 'average_income_per_account' => 'Average income per account', + 'total' => 'Total', + 'description' => 'Description', + 'sum_of_period' => 'Sum of period', + 'average_in_period' => 'Average in period', + 'no_account_role' => '(no role)', + 'account_role_defaultAsset' => 'Default asset account', + 'account_role_sharedAsset' => 'Shared asset account', + 'account_role_savingAsset' => 'Savings account', + 'account_role_ccAsset' => 'Credit card', + 'account_role_cashWalletAsset' => 'Cash wallet', + 'budget_chart_click' => 'Please click on a budget name in the table above to see a chart.', + 'category_chart_click' => 'Please click on a category name in the table above to see a chart.', + 'in_out_accounts' => 'Earned and spent per combination', + 'in_out_accounts_per_asset' => 'Earned and spent (per asset account)', + 'in_out_per_category' => 'Earned and spent per category', + 'out_per_budget' => 'Spent per budget', + 'select_expense_revenue' => 'Select expense/revenue account', + 'multi_currency_report_sum' => 'Because this list contains accounts with multiple currencies, the sum(s) you see may not make sense. The report will always fall back to your default currency.', + 'sum_in_default_currency' => 'The sum will always be in your default currency.', + 'net_filtered_prefs' => 'This chart will never include accounts that have the "Include in net worth"-option unchecked.', // Ignore this comment // charts: - 'chart' => 'Chart', - 'month' => 'Month', - 'budget' => 'Budget', - 'spent' => 'Spent', - 'spent_capped' => 'Spent (capped)', - 'spent_in_budget' => 'Spent in budget', - 'left_to_spend' => 'Left to spend', - 'earned' => 'Earned', - 'overspent' => 'Overspent', - 'left' => 'Left', - 'max-amount' => 'Maximum amount', - 'min-amount' => 'Minimum amount', - 'journal-amount' => 'Current bill entry', - 'name' => 'Name', - 'date' => 'Date', - 'date_and_time' => 'Date and time', - 'time' => 'Time', - 'paid' => 'Paid', - 'unpaid' => 'Unpaid', - 'day' => 'Day', - 'budgeted' => 'Budgeted', - 'period' => 'Period', - 'balance' => 'Balance', - 'in_out_period' => 'In + out this period', - 'sum' => 'Sum', - 'summary' => 'Summary', - 'average' => 'Average', - 'balanceFor' => 'Balance for :name', - 'no_tags' => '(no tags)', - 'nothing_found' => '(nothing found)', + 'chart' => 'Chart', + 'month' => 'Month', + 'budget' => 'Budget', + 'spent' => 'Spent', + 'spent_capped' => 'Spent (capped)', + 'spent_in_budget' => 'Spent in budget', + 'left_to_spend' => 'Left to spend', + 'earned' => 'Earned', + 'overspent' => 'Overspent', + 'left' => 'Left', + 'max-amount' => 'Maximum amount', + 'min-amount' => 'Minimum amount', + 'journal-amount' => 'Current subscription entry', + 'name' => 'Name', + 'date' => 'Date', + 'date_and_time' => 'Date and time', + 'time' => 'Time', + 'paid' => 'Paid', + 'unpaid' => 'Unpaid', + 'day' => 'Day', + 'budgeted' => 'Budgeted', + 'period' => 'Period', + 'balance' => 'Balance', + 'in_out_period' => 'In + out this period', + 'sum' => 'Sum', + 'summary' => 'Summary', + 'average' => 'Average', + 'balanceFor' => 'Balance for :name', + 'no_tags' => '(no tags)', + 'nothing_found' => '(nothing found)', // page settings and wizard dialogs - 'page_settings_header' => 'Page settings', - 'visible_columns' => 'Visible columns', - 'accounts_to_show' => 'Accounts to show', - 'active_accounts_only' => 'Active accounts only', - 'in_active_accounts_only' => 'Inactive accounts only', - 'show_all_accounts' => 'Show all accounts', - 'group_accounts' => 'Group accounts', + 'page_settings_header' => 'Page settings', + 'visible_columns' => 'Visible columns', + 'accounts_to_show' => 'Accounts to show', + 'active_accounts_only' => 'Active accounts only', + 'in_active_accounts_only' => 'Inactive accounts only', + 'show_all_accounts' => 'Show all accounts', + 'group_accounts' => 'Group accounts', // piggy banks: - 'event_history' => 'Event history', - 'add_money_to_piggy' => 'Add money to piggy bank ":name"', - 'piggy_bank' => 'Piggy bank', - 'new_piggy_bank' => 'New piggy bank', - 'store_piggy_bank' => 'Store new piggy bank', - 'stored_piggy_bank' => 'Store new piggy bank ":name"', - 'account_status' => 'Account status', - 'left_for_piggy_banks' => 'Left for piggy banks', - 'sum_of_piggy_banks' => 'Sum of piggy banks', - 'saved_so_far' => 'Saved so far', - 'left_to_save' => 'Left to save', - 'suggested_amount' => 'Suggested monthly amount to save', - 'add_money_to_piggy_title' => 'Add money to piggy bank ":name"', - 'remove_money_from_piggy_title' => 'Remove money from piggy bank ":name"', - 'add' => 'Add', - 'no_money_for_piggy' => 'You have no money to put in this piggy bank.', - 'suggested_savings_per_month' => 'Suggested per month', + 'event_history' => 'Event history', + 'add_money_to_piggy' => 'Add money to piggy bank ":name"', + 'piggy_bank' => 'Piggy bank', + 'new_piggy_bank' => 'New piggy bank', + 'store_piggy_bank' => 'Store new piggy bank', + 'stored_piggy_bank' => 'Store new piggy bank ":name"', + 'account_status' => 'Account status', + 'left_for_piggy_banks' => 'Left for piggy banks', + 'sum_of_piggy_banks' => 'Sum of piggy banks', + 'saved_so_far' => 'Saved so far', + 'left_to_save' => 'Left to save', + 'suggested_amount' => 'Suggested monthly amount to save', + 'add_money_to_piggy_title' => 'Add money to piggy bank ":name"', + 'remove_money_from_piggy_title' => 'Remove money from piggy bank ":name"', + 'add' => 'Add', + 'no_money_for_piggy' => 'You have no money to put in this piggy bank.', + 'suggested_savings_per_month' => 'Suggested per month', - 'remove' => 'Remove', - 'max_amount_add' => 'The maximum amount you can add is', - 'max_amount_remove' => 'The maximum amount you can remove is', - 'update_piggy_button' => 'Update piggy bank', - 'update_piggy_title' => 'Update piggy bank ":name"', - 'updated_piggy_bank' => 'Updated piggy bank ":name"', - 'details' => 'Details', - 'events' => 'Events', - 'target_amount' => 'Target amount', - 'start_date' => 'Start date', - 'no_start_date' => 'No start date', - 'target_date' => 'Target date', - 'no_target_date' => 'No target date', - 'table' => 'Table', - 'delete_piggy_bank' => 'Delete piggy bank ":name"', - 'cannot_add_amount_piggy' => 'Could not add :amount to ":name".', - 'cannot_remove_from_piggy' => 'Could not remove :amount from ":name".', - 'deleted_piggy_bank' => 'Deleted piggy bank ":name"', - 'added_amount_to_piggy' => 'Added :amount to ":name"', - 'removed_amount_from_piggy' => 'Removed :amount from ":name"', - 'piggy_events' => 'Related piggy banks', + 'remove' => 'Remove', + 'max_amount_add' => 'The maximum amount you can add is', + 'max_amount_remove' => 'The maximum amount you can remove is', + 'update_piggy_button' => 'Update piggy bank', + 'update_piggy_title' => 'Update piggy bank ":name"', + 'updated_piggy_bank' => 'Updated piggy bank ":name"', + 'details' => 'Details', + 'events' => 'Events', + 'target_amount' => 'Target amount', + 'start_date' => 'Start date', + 'no_start_date' => 'No start date', + 'target_date' => 'Target date', + 'no_target_date' => 'No target date', + 'table' => 'Table', + 'delete_piggy_bank' => 'Delete piggy bank ":name"', + 'cannot_add_amount_piggy' => 'Could not add :amount to ":name".', + 'cannot_remove_from_piggy' => 'Could not remove :amount from ":name".', + 'deleted_piggy_bank' => 'Deleted piggy bank ":name"', + 'added_amount_to_piggy' => 'Added :amount to ":name"', + 'removed_amount_from_piggy' => 'Removed :amount from ":name"', + 'piggy_events' => 'Related piggy banks', // tags - 'delete_tag' => 'Delete tag ":tag"', - 'deleted_tag' => 'Deleted tag ":tag"', - 'new_tag' => 'Make new tag', - 'edit_tag' => 'Edit tag ":tag"', - 'updated_tag' => 'Updated tag ":tag"', - 'created_tag' => 'Tag ":tag" has been created!', + 'delete_tag' => 'Delete tag ":tag"', + 'deleted_tag' => 'Deleted tag ":tag"', + 'new_tag' => 'Make new tag', + 'edit_tag' => 'Edit tag ":tag"', + 'updated_tag' => 'Updated tag ":tag"', + 'created_tag' => 'Tag ":tag" has been created!', - 'transaction_journal_information' => 'Transaction information', - 'transaction_journal_amount' => 'Amount information', - 'transaction_journal_meta' => 'Meta information', - 'transaction_journal_more' => 'More information', - 'basic_journal_information' => 'Basic transaction information', - 'transaction_journal_extra' => 'Extra information', - 'att_part_of_journal' => 'Stored under ":journal"', - 'total_amount' => 'Total amount', - 'number_of_decimals' => 'Number of decimals', + 'transaction_journal_information' => 'Transaction information', + 'transaction_journal_amount' => 'Amount information', + 'transaction_journal_meta' => 'Meta information', + 'transaction_journal_more' => 'More information', + 'basic_journal_information' => 'Basic transaction information', + 'transaction_journal_extra' => 'Extra information', + 'att_part_of_journal' => 'Stored under ":journal"', + 'total_amount' => 'Total amount', + 'number_of_decimals' => 'Number of decimals', // Ignore this comment // administration - 'invite_is_already_redeemed' => 'The invite to ":address" has already been redeemed.', - 'invite_is_deleted' => 'The invite to ":address" has been deleted.', - 'invite_new_user_title' => 'Invite new user', - 'invite_new_user_text' => 'As an administrator, you can invite users to register on your Firefly III administration. Using the direct link you can share with them, they will be able to register an account. The invited user and their invite link will appear in the table below. You are free to share the invitation link with them.', - 'invited_user_mail' => 'Email address', - 'invite_user' => 'Invite user', - 'user_is_invited' => 'Email address ":address" was invited to Firefly III', - 'administration' => 'Administration', - 'system_settings' => 'System settings', - 'code_already_used' => 'Invite code has been used', - 'user_administration' => 'User administration', - 'list_all_users' => 'All users', - 'all_users' => 'All users', - 'instance_configuration' => 'Configuration', - 'firefly_instance_configuration' => 'Configuration options for Firefly III', - 'setting_single_user_mode' => 'Single user mode', - 'setting_single_user_mode_explain' => 'By default, Firefly III only accepts one (1) registration: you. This is a security measure, preventing others from using your instance unless you allow them to. Future registrations are blocked. When you uncheck this box, others can use your instance as well, assuming they can reach it (when it is connected to the internet).', - 'store_configuration' => 'Store configuration', - 'single_user_administration' => 'User administration for :email', - 'edit_user' => 'Edit user :email', - 'hidden_fields_preferences' => 'You can enable more transaction options in your preferences.', - 'user_data_information' => 'User data', - 'user_information' => 'User information', - 'total_size' => 'total size', - 'budget_or_budgets' => ':count budget|:count budgets', - 'budgets_with_limits' => ':count budget with configured amount|:count budgets with configured amount', - 'nr_of_rules_in_total_groups' => ':count_rules rule(s) in :count_groups rule group(s)', - 'tag_or_tags' => ':count tag|:count tags', - 'configuration_updated' => 'The configuration has been updated', - 'setting_is_demo_site' => 'Demo site', - 'setting_is_demo_site_explain' => 'If you check this box, this installation will behave as if it is the demo site, which can have weird side effects.', - 'block_code_bounced' => 'Email message(s) bounced', - 'block_code_expired' => 'Demo account expired', - 'no_block_code' => 'No reason for block or user not blocked', - 'demo_user_export' => 'The demo user cannot export data', - 'block_code_email_changed' => 'User has not yet confirmed new email address', - 'admin_update_email' => 'Contrary to the profile page, the user will NOT be notified their email address has changed!', - 'update_user' => 'Update user', - 'updated_user' => 'User data has been changed.', - 'delete_user' => 'Delete user :email', - 'user_deleted' => 'The user has been deleted', - 'send_test_email' => 'Send test email message', - 'send_test_email_text' => 'To see if your installation is capable of sending email or posting Slack messages, please press this button. You will not see an error here (if any), the log files will reflect any errors. You can press this button as many times as you like. There is no spam control. The message will be sent to :email and should arrive shortly.', - 'send_message' => 'Send message', - 'send_test_triggered' => 'Test was triggered. Check your inbox and the log files.', - 'give_admin_careful' => 'Users who are given admin rights can take away yours. Be careful.', - 'admin_maintanance_title' => 'Maintenance', - 'admin_maintanance_expl' => 'Some nifty buttons for Firefly III maintenance', - 'admin_maintenance_clear_cache' => 'Clear cache', - 'admin_notifications' => 'Admin notifications', - 'admin_notifications_expl' => 'The following notifications can be enabled or disabled by the administrator. If you want to get these messages over Slack as well, set the "incoming webhook" URL.', - 'admin_notification_check_user_new_reg' => 'User gets post-registration welcome message', - 'admin_notification_check_admin_new_reg' => 'Administrator(s) get new user registration notification', - 'admin_notification_check_new_version' => 'A new version is available', - 'admin_notification_check_invite_created' => 'A user is invited to Firefly III', - 'admin_notification_check_invite_redeemed' => 'A user invitation is redeemed', - 'all_invited_users' => 'All invited users', - 'save_notification_settings' => 'Save settings', - 'notification_settings_saved' => 'The notification settings have been saved', + 'invite_is_already_redeemed' => 'The invite to ":address" has already been redeemed.', + 'invite_is_deleted' => 'The invite to ":address" has been deleted.', + 'invite_new_user_title' => 'Invite new user', + 'invite_new_user_text' => 'As an administrator, you can invite users to register on your Firefly III administration. Using the direct link you can share with them, they will be able to register an account. The invited user and their invite link will appear in the table below. You are free to share the invitation link with them.', + 'invited_user_mail' => 'Email address', + 'invite_user' => 'Invite user', + 'user_is_invited' => 'Email address ":address" was invited to Firefly III', + 'administration' => 'Administration', + 'system_settings' => 'System settings', + 'code_already_used' => 'Invite code has been used', + 'user_administration' => 'User administration', + 'list_all_users' => 'All users', + 'all_users' => 'All users', + 'instance_configuration' => 'Configuration', + 'firefly_instance_configuration' => 'Configuration options for Firefly III', + 'setting_single_user_mode' => 'Single user mode', + 'setting_single_user_mode_explain' => 'By default, Firefly III only accepts one (1) registration: you. This is a security measure, preventing others from using your instance unless you allow them to. Future registrations are blocked. When you uncheck this box, others can use your instance as well, assuming they can reach it (when it is connected to the internet).', + 'store_configuration' => 'Store configuration', + 'single_user_administration' => 'User administration for :email', + 'edit_user' => 'Edit user :email', + 'hidden_fields_preferences' => 'You can enable more transaction options in your preferences.', + 'user_data_information' => 'User data', + 'user_information' => 'User information', + 'total_size' => 'total size', + 'budget_or_budgets' => ':count budget|:count budgets', + 'budgets_with_limits' => ':count budget with configured amount|:count budgets with configured amount', + 'nr_of_rules_in_total_groups' => ':count_rules rule(s) in :count_groups rule group(s)', + 'tag_or_tags' => ':count tag|:count tags', + 'configuration_updated' => 'The configuration has been updated', + 'setting_is_demo_site' => 'Demo site', + 'setting_is_demo_site_explain' => 'If you check this box, this installation will behave as if it is the demo site, which can have weird side effects.', + 'block_code_bounced' => 'Email message(s) bounced', + 'block_code_expired' => 'Demo account expired', + 'no_block_code' => 'No reason for block or user not blocked', + 'demo_user_export' => 'The demo user cannot export data', + 'block_code_email_changed' => 'User has not yet confirmed new email address', + 'admin_update_email' => 'Contrary to the profile page, the user will NOT be notified their email address has changed!', + 'update_user' => 'Update user', + 'updated_user' => 'User data has been changed.', + 'delete_user' => 'Delete user :email', + 'user_deleted' => 'The user has been deleted', + 'send_test_email' => 'Send test email message', + 'send_test_email_text' => 'To see if your installation is capable of sending a notification, please press this button. You will not see an error here (if any), the log files will reflect any errors. You can press this button as many times as you like. There is no spam control. The message will be sent to :email and should arrive shortly.', + 'send_message' => 'Send message', + 'send_test_triggered' => 'Test was triggered. Check your inbox and the log files.', + 'give_admin_careful' => 'Users who are given admin rights can take away yours. Be careful.', + 'admin_maintanance_title' => 'Maintenance', + 'admin_maintanance_expl' => 'Some nifty buttons for Firefly III maintenance', + 'admin_maintenance_clear_cache' => 'Clear cache', + 'owner_notifications' => 'Admin notifications', + 'owner_notifications_expl' => 'The following notifications can be enabled or disabled by the administrator. It will be sent over ALL configured channels. Some channels are configured in your environment variables, others can be set here.', + 'channel_settings' => 'Settings for notification channels', + 'notification_test_failed' => 'Notification test for channel ":channel" failed. The logs will have more details.', + 'notification_test_executed' => 'Notification test for channel ":channel" executed. Check your logs for details.', + 'settings_notifications' => 'Settings for notifications', + 'title_owner_notifications' => 'Owner notifications', + 'owner_notification_check_user_new_reg' => 'User gets post-registration welcome message', + 'owner_notification_check_admin_new_reg' => 'Administrator(s) get new user registration notification', + 'owner_notification_check_new_version' => 'A new version is available', + 'owner_notification_check_invite_created' => 'A user is invited to Firefly III', + 'owner_notification_check_invite_redeemed' => 'A user invitation is redeemed', + 'owner_notification_check_unknown_user_attempt' => 'An unknown user tries to login', + 'all_invited_users' => 'All invited users', + 'save_notification_settings' => 'Save settings', + 'notification_settings' => 'Settings for notifications', + 'notification_settings_saved' => 'The notification settings have been saved', + 'available_channels_title' => 'Available channels', + 'available_channels_expl' => 'These channels are available to send notifications over. To test your configuration, use the buttons below. Please note that the buttons have no spam control.', + 'notification_channel_name_email' => 'Email', + 'slack_discord_double' => 'The Slack notification channel can also send notifications to Discord.', + 'notification_channel_name_slack' => 'Slack', + 'notification_channel_name_ntfy' => 'Ntfy.sh', + 'notification_channel_name_pushover' => 'Pushover', + 'notification_channel_name_gotify' => 'Gotify', + 'notification_channel_name_pushbullet' => 'Pushbullet', + 'channel_not_available' => 'not available', + 'configure_channel_in_env' => 'needs environment variables', + 'test_notification_channel_name_email' => 'Test email', + 'test_notification_channel_name_slack' => 'Test Slack', + 'test_notification_channel_name_ntfy' => 'Test Ntfy.sh', + 'test_notification_channel_name_pushover' => 'Test Pushover', + 'test_notification_channel_name_gotify' => 'Test Gotify', + 'test_notification_channel_name_pushbullet' => 'Test Pushbullet', - 'split_transaction_title' => 'Description of the split transaction', - 'split_transaction_title_help' => 'If you create a split transaction, there must be a global description for all splits of the transaction.', - 'split_title_help' => 'If you create a split transaction, there must be a global description for all splits of the transaction.', - 'you_create_transfer' => 'You\'re creating a transfer.', - 'you_create_withdrawal' => 'You\'re creating a withdrawal.', - 'you_create_deposit' => 'You\'re creating a deposit.', + 'split_transaction_title' => 'Description of the split transaction', + 'split_transaction_title_help' => 'If you create a split transaction, there must be a global description for all splits of the transaction.', + 'split_title_help' => 'If you create a split transaction, there must be a global description for all splits of the transaction.', + 'you_create_transfer' => 'You\'re creating a transfer.', + 'you_create_withdrawal' => 'You\'re creating a withdrawal.', + 'you_create_deposit' => 'You\'re creating a deposit.', // links - 'journal_link_configuration' => 'Transaction links configuration', - 'create_new_link_type' => 'Create new link type', - 'store_new_link_type' => 'Store new link type', - 'update_link_type' => 'Update link type', - 'edit_link_type' => 'Edit link type ":name"', - 'updated_link_type' => 'Updated link type ":name"', - 'delete_link_type' => 'Delete link type ":name"', - 'deleted_link_type' => 'Deleted link type ":name"', - 'stored_new_link_type' => 'Store new link type ":name"', - 'cannot_edit_link_type' => 'Cannot edit link type ":name"', - 'link_type_help_name' => 'Ie. "Duplicates"', - 'link_type_help_inward' => 'Ie. "duplicates"', - 'link_type_help_outward' => 'Ie. "is duplicated by"', - 'save_connections_by_moving' => 'Save the link between these transactions by moving them to another link type:', - 'do_not_save_connection' => '(do not save connection)', - 'link_transaction' => 'Link transaction', - 'link_to_other_transaction' => 'Link this transaction to another transaction', - 'select_transaction_to_link' => 'Select a transaction to link this transaction to. The links are currently unused in Firefly III (apart from being shown), but I plan to change this in the future. Use the search box to select a transaction either by title or by ID. If you want to add custom link types, check out the administration section.', - 'this_transaction' => 'This transaction', - 'transaction' => 'Transaction', - 'comments' => 'Comments', - 'link_notes' => 'Any notes you wish to store with the link.', - 'invalid_link_selection' => 'Cannot link these transactions', - 'selected_transaction' => 'Selected transaction', - 'journals_linked' => 'Transactions are linked.', - 'journals_error_linked' => 'These transactions are already linked.', - 'journals_link_to_self' => 'You cannot link a transaction to itself', - 'journal_links' => 'Transaction links', - 'this_withdrawal' => 'This withdrawal', - 'this_deposit' => 'This deposit', - 'this_transfer' => 'This transfer', - 'overview_for_link' => 'Overview for link type ":name"', - 'source_transaction' => 'Source transaction', - 'link_description' => 'Link description', - 'destination_transaction' => 'Destination transaction', - 'delete_journal_link' => 'Delete the link between :source and :destination', - 'deleted_link' => 'Deleted link', + 'journal_link_configuration' => 'Transaction links configuration', + 'create_new_link_type' => 'Create new link type', + 'store_new_link_type' => 'Store new link type', + 'update_link_type' => 'Update link type', + 'edit_link_type' => 'Edit link type ":name"', + 'updated_link_type' => 'Updated link type ":name"', + 'delete_link_type' => 'Delete link type ":name"', + 'deleted_link_type' => 'Deleted link type ":name"', + 'stored_new_link_type' => 'Store new link type ":name"', + 'cannot_edit_link_type' => 'Cannot edit link type ":name"', + 'link_type_help_name' => 'Ie. "Duplicates"', + 'link_type_help_inward' => 'Ie. "duplicates"', + 'link_type_help_outward' => 'Ie. "is duplicated by"', + 'save_connections_by_moving' => 'Save the link between these transactions by moving them to another link type:', + 'do_not_save_connection' => '(do not save connection)', + 'link_transaction' => 'Link transaction', + 'link_to_other_transaction' => 'Link this transaction to another transaction', + 'select_transaction_to_link' => 'Select a transaction to link this transaction to. The links are currently unused in Firefly III (apart from being shown), but I plan to change this in the future. Use the search box to select a transaction either by title or by ID. If you want to add custom link types, check out the administration section.', + 'this_transaction' => 'This transaction', + 'transaction' => 'Transaction', + 'comments' => 'Comments', + 'link_notes' => 'Any notes you wish to store with the link.', + 'invalid_link_selection' => 'Cannot link these transactions', + 'selected_transaction' => 'Selected transaction', + 'journals_linked' => 'Transactions are linked.', + 'journals_error_linked' => 'These transactions are already linked.', + 'journals_link_to_self' => 'You cannot link a transaction to itself', + 'journal_links' => 'Transaction links', + 'this_withdrawal' => 'This withdrawal', + 'this_deposit' => 'This deposit', + 'this_transfer' => 'This transfer', + 'overview_for_link' => 'Overview for link type ":name"', + 'source_transaction' => 'Source transaction', + 'link_description' => 'Link description', + 'destination_transaction' => 'Destination transaction', + 'delete_journal_link' => 'Delete the link between :source and :destination', + 'deleted_link' => 'Deleted link', // link translations: - 'Paid_name' => 'Paid', - 'Refund_name' => 'Refund', - 'Reimbursement_name' => 'Reimbursement', - 'Related_name' => 'Related', - 'relates to_inward' => 'relates to', - 'is (partially) refunded by_inward' => 'is (partially) refunded by', - 'is (partially) paid for by_inward' => 'is (partially) paid for by', - 'is (partially) reimbursed by_inward' => 'is (partially) reimbursed by', - 'inward_transaction' => 'Inward transaction', - 'outward_transaction' => 'Outward transaction', - 'relates to_outward' => 'relates to', - '(partially) refunds_outward' => '(partially) refunds', - '(partially) pays for_outward' => '(partially) pays for', - '(partially) reimburses_outward' => '(partially) reimburses', - 'is (partially) refunded by' => 'is (partially) refunded by', - 'is (partially) paid for by' => 'is (partially) paid for by', - 'is (partially) reimbursed by' => 'is (partially) reimbursed by', - 'relates to' => 'relates to', - '(partially) refunds' => '(partially) refunds', - '(partially) pays for' => '(partially) pays for', - '(partially) reimburses' => '(partially) reimburses', + 'Paid_name' => 'Paid', + 'Refund_name' => 'Refund', + 'Reimbursement_name' => 'Reimbursement', + 'Related_name' => 'Related', + 'relates to_inward' => 'relates to', + 'is (partially) refunded by_inward' => 'is (partially) refunded by', + 'is (partially) paid for by_inward' => 'is (partially) paid for by', + 'is (partially) reimbursed by_inward' => 'is (partially) reimbursed by', + 'inward_transaction' => 'Inward transaction', + 'outward_transaction' => 'Outward transaction', + 'relates to_outward' => 'relates to', + '(partially) refunds_outward' => '(partially) refunds', + '(partially) pays for_outward' => '(partially) pays for', + '(partially) reimburses_outward' => '(partially) reimburses', + 'is (partially) refunded by' => 'is (partially) refunded by', + 'is (partially) paid for by' => 'is (partially) paid for by', + 'is (partially) reimbursed by' => 'is (partially) reimbursed by', + 'relates to' => 'relates to', + '(partially) refunds' => '(partially) refunds', + '(partially) pays for' => '(partially) pays for', + '(partially) reimburses' => '(partially) reimburses', // split a transaction: - 'splits' => 'Splits', - 'add_another_split' => 'Add another split', - 'cannot_edit_opening_balance' => 'You cannot edit the opening balance of an account.', - 'no_edit_multiple_left' => 'You have selected no valid transactions to edit.', - 'breadcrumb_convert_group' => 'Convert transaction', - 'convert_invalid_source' => 'Source information is invalid for transaction #%d.', - 'convert_invalid_destination' => 'Destination information is invalid for transaction #%d.', - 'create_another' => 'After storing, return here to create another one.', - 'after_update_create_another' => 'After updating, return here to continue editing.', - 'store_as_new' => 'Store as a new transaction instead of updating.', - 'reset_after' => 'Reset form after submission', - 'errors_submission' => 'There was something wrong with your submission. Please check out the errors below.', - 'errors_submission_v2' => 'There was something wrong with your submission. Please check out the errors below: {{errorMessage}}', - 'transaction_expand_split' => 'Expand split', - 'transaction_remove_split' => 'Remove split', - 'transaction_collapse_split' => 'Collapse split', + 'splits' => 'Splits', + 'add_another_split' => 'Add another split', + 'cannot_edit_opening_balance' => 'You cannot edit the opening balance of an account.', + 'no_edit_multiple_left' => 'You have selected no valid transactions to edit.', + 'breadcrumb_convert_group' => 'Convert transaction', + 'convert_invalid_source' => 'Source information is invalid for transaction #%d.', + 'convert_invalid_destination' => 'Destination information is invalid for transaction #%d.', + 'create_another' => 'After storing, return here to create another one.', + 'after_update_create_another' => 'After updating, return here to continue editing.', + 'store_as_new' => 'Store as a new transaction instead of updating.', + 'reset_after' => 'Reset form after submission', + 'errors_submission' => 'There was something wrong with your submission. Please check out the errors below.', + 'errors_submission_v2' => 'There was something wrong with your submission. Please check out the errors below: {{errorMessage}}', + 'transaction_expand_split' => 'Expand split', + 'transaction_remove_split' => 'Remove split', + 'transaction_collapse_split' => 'Collapse split', // object groups - 'default_group_title_name' => '(ungrouped)', - 'default_group_title_name_plain' => 'ungrouped', + 'default_group_title_name' => '(ungrouped)', + 'default_group_title_name_plain' => 'ungrouped', // empty lists? no objects? instructions: - 'no_accounts_title_asset' => 'Let\'s create an asset account!', - 'no_accounts_intro_asset' => 'You have no asset accounts yet. Asset accounts are your main accounts: your checking account, savings account, shared account or even your credit card.', - 'no_accounts_imperative_asset' => 'To start using Firefly III you must create at least one asset account. Let\'s do so now:', - 'no_accounts_create_asset' => 'Create an asset account', - 'no_accounts_title_expense' => 'Let\'s create an expense account!', - 'no_accounts_intro_expense' => 'You have no expense accounts yet. Expense accounts are the places where you spend money, such as shops and supermarkets.', - 'no_accounts_imperative_expense' => 'Expense accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', - 'no_accounts_create_expense' => 'Create an expense account', - 'no_accounts_title_revenue' => 'Let\'s create a revenue account!', - 'no_accounts_intro_revenue' => 'You have no revenue accounts yet. Revenue accounts are the places where you receive money from, such as your employer.', - 'no_accounts_imperative_revenue' => 'Revenue accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', - 'no_accounts_create_revenue' => 'Create a revenue account', - 'no_accounts_title_liabilities' => 'Let\'s create a liability!', - 'no_accounts_intro_liabilities' => 'You have no liabilities yet. Liabilities are the accounts that register your (student) loans and other debts.', - 'no_accounts_imperative_liabilities' => 'You don\'t have to use this feature, but it can be useful if you want to keep track of these things.', - 'no_accounts_create_liabilities' => 'Create a liability', - 'no_budgets_title_default' => 'Let\'s create a budget', - 'no_rules_title_default' => 'Let\'s create a rule', - 'no_budgets_intro_default' => 'You have no budgets yet. Budgets are used to organize your expenses into logical groups, which you can give a soft-cap to limit your expenses.', - 'no_rules_intro_default' => 'You have no rules yet. Rules are powerful automations that can handle transactions for you.', - 'no_rules_imperative_default' => 'Rules can be very useful when you\'re managing transactions. Let\'s create one now:', - 'no_budgets_imperative_default' => 'Budgets are the basic tools of financial management. Let\'s create one now:', - 'no_budgets_create_default' => 'Create a budget', - 'no_rules_create_default' => 'Create a rule', - 'no_categories_title_default' => 'Let\'s create a category!', - 'no_categories_intro_default' => 'You have no categories yet. Categories are used to fine tune your transactions and label them with their designated category.', - 'no_categories_imperative_default' => 'Categories are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', - 'no_categories_create_default' => 'Create a category', - 'no_tags_title_default' => 'Let\'s create a tag!', - 'no_tags_intro_default' => 'You have no tags yet. Tags are used to fine tune your transactions and label them with specific keywords.', - 'no_tags_imperative_default' => 'Tags are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', - 'no_tags_create_default' => 'Create a tag', - 'no_transactions_title_withdrawal' => 'Let\'s create an expense!', - 'no_transactions_intro_withdrawal' => 'You have no expenses yet. You should create expenses to start managing your finances.', - 'no_transactions_imperative_withdrawal' => 'Have you spent some money? Then you should write it down:', - 'no_transactions_create_withdrawal' => 'Create an expense', - 'no_transactions_title_deposit' => 'Let\'s create some income!', - 'no_transactions_intro_deposit' => 'You have no recorded income yet. You should create income entries to start managing your finances.', - 'no_transactions_imperative_deposit' => 'Have you received some money? Then you should write it down:', - 'no_transactions_create_deposit' => 'Create a deposit', - 'no_transactions_title_transfers' => 'Let\'s create a transfer!', - 'no_transactions_intro_transfers' => 'You have no transfers yet. When you move money between asset accounts, it is recorded as a transfer.', - 'no_transactions_imperative_transfers' => 'Have you moved some money around? Then you should write it down:', - 'no_transactions_create_transfers' => 'Create a transfer', - 'no_piggies_title_default' => 'Let\'s create a piggy bank!', - 'no_piggies_intro_default' => 'You have no piggy banks yet. You can create piggy banks to divide your savings and keep track of what you\'re saving up for.', - 'no_piggies_imperative_default' => 'Do you have things you\'re saving money for? Create a piggy bank and keep track:', - 'no_piggies_create_default' => 'Create a new piggy bank', - 'no_bills_title_default' => 'Let\'s create a bill!', - 'no_bills_intro_default' => 'You have no bills yet. You can create bills to keep track of regular expenses, like your rent or insurance.', - 'no_bills_imperative_default' => 'Do you have such regular bills? Create a bill and keep track of your payments:', - 'no_bills_create_default' => 'Create a bill', + 'no_accounts_title_asset' => 'Let\'s create an asset account!', + 'no_accounts_intro_asset' => 'You have no asset accounts yet. Asset accounts are your main accounts: your checking account, savings account, shared account or even your credit card.', + 'no_accounts_imperative_asset' => 'To start using Firefly III you must create at least one asset account. Let\'s do so now:', + 'no_accounts_create_asset' => 'Create an asset account', + 'no_accounts_title_expense' => 'Let\'s create an expense account!', + 'no_accounts_intro_expense' => 'You have no expense accounts yet. Expense accounts are the places where you spend money, such as shops and supermarkets.', + 'no_accounts_imperative_expense' => 'Expense accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', + 'no_accounts_create_expense' => 'Create an expense account', + 'no_accounts_title_revenue' => 'Let\'s create a revenue account!', + 'no_accounts_intro_revenue' => 'You have no revenue accounts yet. Revenue accounts are the places where you receive money from, such as your employer.', + 'no_accounts_imperative_revenue' => 'Revenue accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', + 'no_accounts_create_revenue' => 'Create a revenue account', + 'no_accounts_title_liabilities' => 'Let\'s create a liability!', + 'no_accounts_intro_liabilities' => 'You have no liabilities yet. Liabilities are the accounts that register your (student) loans and other debts.', + 'no_accounts_imperative_liabilities' => 'You don\'t have to use this feature, but it can be useful if you want to keep track of these things.', + 'no_accounts_create_liabilities' => 'Create a liability', + 'no_budgets_title_default' => 'Let\'s create a budget', + 'no_rules_title_default' => 'Let\'s create a rule', + 'no_budgets_intro_default' => 'You have no budgets yet. Budgets are used to organize your expenses into logical groups, which you can give a soft-cap to limit your expenses.', + 'no_rules_intro_default' => 'You have no rules yet. Rules are powerful automations that can handle transactions for you.', + 'no_rules_imperative_default' => 'Rules can be very useful when you\'re managing transactions. Let\'s create one now:', + 'no_budgets_imperative_default' => 'Budgets are the basic tools of financial management. Let\'s create one now:', + 'no_budgets_create_default' => 'Create a budget', + 'no_rules_create_default' => 'Create a rule', + 'no_categories_title_default' => 'Let\'s create a category!', + 'no_categories_intro_default' => 'You have no categories yet. Categories are used to fine tune your transactions and label them with their designated category.', + 'no_categories_imperative_default' => 'Categories are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', + 'no_categories_create_default' => 'Create a category', + 'no_tags_title_default' => 'Let\'s create a tag!', + 'no_tags_intro_default' => 'You have no tags yet. Tags are used to fine tune your transactions and label them with specific keywords.', + 'no_tags_imperative_default' => 'Tags are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', + 'no_tags_create_default' => 'Create a tag', + 'no_transactions_title_withdrawal' => 'Let\'s create an expense!', + 'no_transactions_intro_withdrawal' => 'You have no expenses yet. You should create expenses to start managing your finances.', + 'no_transactions_imperative_withdrawal' => 'Have you spent some money? Then you should write it down:', + 'no_transactions_create_withdrawal' => 'Create an expense', + 'no_transactions_title_deposit' => 'Let\'s create some income!', + 'no_transactions_intro_deposit' => 'You have no recorded income yet. You should create income entries to start managing your finances.', + 'no_transactions_imperative_deposit' => 'Have you received some money? Then you should write it down:', + 'no_transactions_create_deposit' => 'Create a deposit', + 'no_transactions_title_transfers' => 'Let\'s create a transfer!', + 'no_transactions_intro_transfers' => 'You have no transfers yet. When you move money between asset accounts, it is recorded as a transfer.', + 'no_transactions_imperative_transfers' => 'Have you moved some money around? Then you should write it down:', + 'no_transactions_create_transfers' => 'Create a transfer', + 'no_piggies_title_default' => 'Let\'s create a piggy bank!', + 'no_piggies_intro_default' => 'You have no piggy banks yet. You can create piggy banks to divide your savings and keep track of what you\'re saving up for.', + 'no_piggies_imperative_default' => 'Do you have things you\'re saving money for? Create a piggy bank and keep track:', + 'no_piggies_create_default' => 'Create a new piggy bank', + 'no_bills_title_default' => 'Let\'s create a subscription!', + 'no_bills_intro_default' => 'You have no subscriptions yet. You can create subscriptions to keep track of regular expenses, like your rent or insurance.', + 'no_bills_imperative_default' => 'Do you have such regular subscriptions? Create a subscription and keep track of your payments:', + 'no_bills_create_default' => 'Create a subscription', // recurring transactions - 'recurrence_max_count' => 'This recurring transactions will be created at most :max time(s), and has been created :count time(s) already.', - 'create_right_now' => 'Create right now', - 'no_new_transaction_in_recurrence' => 'No new transaction was created. Perhaps it was already fired for this date?', - 'recurrences' => 'Recurring transactions', - 'repeat_until_in_past' => 'This recurring transaction stopped repeating on :date.', - 'recurring_calendar_view' => 'Calendar', - 'no_recurring_title_default' => 'Let\'s create a recurring transaction!', - 'no_recurring_intro_default' => 'You have no recurring transactions yet. You can use these to make Firefly III automatically create transactions for you.', - 'no_recurring_imperative_default' => 'This is a pretty advanced feature but it can be extremely useful. Make sure you read the documentation (?)-icon in the top right corner) before you continue.', - 'no_recurring_create_default' => 'Create a recurring transaction', - 'make_new_recurring' => 'Create a recurring transaction', - 'recurring_daily' => 'Every day', - 'recurring_weekly' => 'Every week on :weekday', - 'recurring_weekly_skip' => 'Every :skip(st/nd/rd/th) week on :weekday', - 'recurring_monthly' => 'Every month on the :dayOfMonth(st/nd/rd/th) day', - 'recurring_monthly_skip' => 'Every :skip(st/nd/rd/th) month on the :dayOfMonth(st/nd/rd/th) day', - 'recurring_ndom' => 'Every month on the :dayOfMonth(st/nd/rd/th) :weekday', - 'recurring_yearly' => 'Every year on :date', - 'overview_for_recurrence' => 'Overview for recurring transaction ":title"', - 'warning_duplicates_repetitions' => 'In rare instances, dates appear twice in this list. This can happen when multiple repetitions collide. Firefly III will always generate one transaction per day.', - 'created_transactions' => 'Related transactions', - 'expected_withdrawals' => 'Expected withdrawals', - 'expected_deposits' => 'Expected deposits', - 'expected_transfers' => 'Expected transfers', - 'created_withdrawals' => 'Created withdrawals', - 'created_deposits' => 'Created deposits', - 'created_transfers' => 'Created transfers', - 'recurring_info' => 'Recurring transaction :count / :total', - 'created_from_recurrence' => 'Created from recurring transaction ":title" (#:id)', - 'recurring_never_cron' => 'It seems the cron job that is necessary to support recurring transactions has never run. This is of course normal when you have just installed Firefly III, but this should be something to set up as soon as possible. Please check out the help-pages using the (?)-icon in the top right corner of the page.', - 'recurring_cron_long_ago' => 'It looks like it has been more than 36 hours since the cron job to support recurring transactions has fired for the last time. Are you sure it has been set up correctly? Please check out the help-pages using the (?)-icon in the top right corner of the page.', + 'recurrence_max_count' => 'This recurring transactions will be created at most :max time(s), and has been created :count time(s) already.', + 'create_right_now' => 'Create right now', + 'no_new_transaction_in_recurrence' => 'No new transaction was created. Perhaps it was already fired for this date?', + 'recurrences' => 'Recurring transactions', + 'repeat_until_in_past' => 'This recurring transaction stopped repeating on :date.', + 'recurring_calendar_view' => 'Calendar', + 'no_recurring_title_default' => 'Let\'s create a recurring transaction!', + 'no_recurring_intro_default' => 'You have no recurring transactions yet. You can use these to make Firefly III automatically create transactions for you.', + 'no_recurring_imperative_default' => 'This is a pretty advanced feature but it can be extremely useful. Make sure you read the documentation (?)-icon in the top right corner) before you continue.', + 'no_recurring_create_default' => 'Create a recurring transaction', + 'make_new_recurring' => 'Create a recurring transaction', + 'recurring_daily' => 'Every day', + 'recurring_weekly' => 'Every week on :weekday', + 'recurring_weekly_skip' => 'Every :skip(st/nd/rd/th) week on :weekday', + 'recurring_monthly' => 'Every month on the :dayOfMonth(st/nd/rd/th) day', + 'recurring_monthly_skip' => 'Every :skip(st/nd/rd/th) month on the :dayOfMonth(st/nd/rd/th) day', + 'recurring_ndom' => 'Every month on the :dayOfMonth(st/nd/rd/th) :weekday', + 'recurring_yearly' => 'Every year on :date', + 'overview_for_recurrence' => 'Overview for recurring transaction ":title"', + 'warning_duplicates_repetitions' => 'In rare instances, dates appear twice in this list. This can happen when multiple repetitions collide. Firefly III will always generate one transaction per day.', + 'created_transactions' => 'Related transactions', + 'expected_withdrawals' => 'Expected withdrawals', + 'expected_deposits' => 'Expected deposits', + 'expected_transfers' => 'Expected transfers', + 'created_withdrawals' => 'Created withdrawals', + 'created_deposits' => 'Created deposits', + 'created_transfers' => 'Created transfers', + 'recurring_info' => 'Recurring transaction :count / :total', + 'created_from_recurrence' => 'Created from recurring transaction ":title" (#:id)', + 'recurring_never_cron' => 'It seems the cron job that is necessary to support recurring transactions has never run. This is of course normal when you have just installed Firefly III, but this should be something to set up as soon as possible. Please check out the help-pages using the (?)-icon in the top right corner of the page.', + 'recurring_cron_long_ago' => 'It looks like it has been more than 36 hours since the cron job to support recurring transactions has fired for the last time. Are you sure it has been set up correctly? Please check out the help-pages using the (?)-icon in the top right corner of the page.', - 'create_new_recurrence' => 'Create new recurring transaction', - 'help_first_date' => 'Indicate the first expected recurrence. This must be in the future.', - 'help_first_date_no_past' => 'Indicate the first expected recurrence. Firefly III will not create transactions in the past.', - 'no_currency' => '(no currency)', - 'mandatory_for_recurring' => 'Mandatory recurrence information', - 'mandatory_for_transaction' => 'Mandatory transaction information', - 'optional_for_recurring' => 'Optional recurrence information', - 'optional_for_transaction' => 'Optional transaction information', - 'change_date_other_options' => 'Change the "first date" to see more options.', - 'mandatory_fields_for_tranaction' => 'The values here will end up in the transaction(s) being created', - 'click_for_calendar' => 'Click here for a calendar that shows you when the transaction would repeat.', - 'repeat_forever' => 'Repeat forever', - 'repeat_until_date' => 'Repeat until date', - 'repeat_times' => 'Repeat a number of times', - 'recurring_skips_one' => 'Every other', - 'recurring_skips_more' => 'Skips :count occurrences', - 'store_new_recurrence' => 'Store recurring transaction', - 'stored_new_recurrence' => 'Recurring transaction ":title" stored successfully.', - 'edit_recurrence' => 'Edit recurring transaction ":title"', - 'recurring_repeats_until' => 'Repeats until :date', - 'recurring_repeats_forever' => 'Repeats forever', - 'recurring_repeats_x_times' => 'Repeats :count time|Repeats :count times', - 'update_recurrence' => 'Update recurring transaction', - 'updated_recurrence' => 'Updated recurring transaction ":title"', - 'recurrence_is_inactive' => 'This recurring transaction is not active and will not generate new transactions.', - 'delete_recurring' => 'Delete recurring transaction ":title"', - 'new_recurring_transaction' => 'New recurring transaction', - 'help_weekend' => 'What should Firefly III do when the recurring transaction falls on a Saturday or Sunday?', - 'do_nothing' => 'Just create the transaction', - 'skip_transaction' => 'Skip the occurrence', - 'jump_to_friday' => 'Create the transaction on the previous Friday instead', - 'jump_to_monday' => 'Create the transaction on the next Monday instead', - 'will_jump_friday' => 'Will be created on Friday instead of the weekends.', - 'will_jump_monday' => 'Will be created on Monday instead of the weekends.', - 'except_weekends' => 'Except weekends', - 'recurrence_deleted' => 'Recurring transaction ":title" deleted', + 'create_new_recurrence' => 'Create new recurring transaction', + 'help_first_date' => 'Indicate the first expected recurrence. This must be in the future.', + 'help_first_date_no_past' => 'Indicate the first expected recurrence. Firefly III will not create transactions in the past.', + 'no_currency' => '(no currency)', + 'mandatory_for_recurring' => 'Mandatory recurrence information', + 'mandatory_for_transaction' => 'Mandatory transaction information', + 'optional_for_recurring' => 'Optional recurrence information', + 'optional_for_transaction' => 'Optional transaction information', + 'change_date_other_options' => 'Change the "first date" to see more options.', + 'mandatory_fields_for_tranaction' => 'The values here will end up in the transaction(s) being created', + 'click_for_calendar' => 'Click here for a calendar that shows you when the transaction would repeat.', + 'repeat_forever' => 'Repeat forever', + 'repeat_until_date' => 'Repeat until date', + 'repeat_times' => 'Repeat a number of times', + 'recurring_skips_one' => 'Every other', + 'recurring_skips_more' => 'Skips :count occurrences', + 'store_new_recurrence' => 'Store recurring transaction', + 'stored_new_recurrence' => 'Recurring transaction ":title" stored successfully.', + 'edit_recurrence' => 'Edit recurring transaction ":title"', + 'recurring_repeats_until' => 'Repeats until :date', + 'recurring_repeats_forever' => 'Repeats forever', + 'recurring_repeats_x_times' => 'Repeats :count time|Repeats :count times', + 'update_recurrence' => 'Update recurring transaction', + 'updated_recurrence' => 'Updated recurring transaction ":title"', + 'recurrence_is_inactive' => 'This recurring transaction is not active and will not generate new transactions.', + 'delete_recurring' => 'Delete recurring transaction ":title"', + 'new_recurring_transaction' => 'New recurring transaction', + 'help_weekend' => 'What should Firefly III do when the recurring transaction falls on a Saturday or Sunday?', + 'do_nothing' => 'Just create the transaction', + 'skip_transaction' => 'Skip the occurrence', + 'jump_to_friday' => 'Create the transaction on the previous Friday instead', + 'jump_to_monday' => 'Create the transaction on the next Monday instead', + 'will_jump_friday' => 'Will be created on Friday instead of the weekends.', + 'will_jump_monday' => 'Will be created on Monday instead of the weekends.', + 'except_weekends' => 'Except weekends', + 'recurrence_deleted' => 'Recurring transaction ":title" deleted', // Ignore this comment // new lines for summary controller. - 'box_balance_in_currency' => 'Balance (:currency)', - 'box_spent_in_currency' => 'Spent (:currency)', - 'box_earned_in_currency' => 'Earned (:currency)', - 'box_budgeted_in_currency' => 'Budgeted (:currency)', - 'box_bill_paid_in_currency' => 'Bills paid (:currency)', - 'box_bill_unpaid_in_currency' => 'Bills unpaid (:currency)', - 'box_left_to_spend_in_currency' => 'Left to spend (:currency)', - 'box_net_worth_in_currency' => 'Net worth (:currency)', - 'box_spend_per_day' => 'Left to spend per day: :amount', + 'box_balance_in_currency' => 'Balance (:currency)', + 'box_spent_in_currency' => 'Spent (:currency)', + 'box_earned_in_currency' => 'Earned (:currency)', + 'box_budgeted_in_currency' => 'Budgeted (:currency)', + 'box_bill_paid_in_currency' => 'Subscriptions paid (:currency)', + 'box_bill_unpaid_in_currency' => 'Subscriptions unpaid (:currency)', + 'box_left_to_spend_in_currency' => 'Left to spend (:currency)', + 'box_net_worth_in_currency' => 'Net worth (:currency)', + 'box_spend_per_day' => 'Left to spend per day: :amount', // debug page - 'debug_page' => 'Debug page', - 'debug_submit_instructions' => 'If you are running into problems, you can use the information in this box as debug information. Please copy-and-paste into a new or existing GitHub issue. It will generate a beautiful table that can be used to quickly diagnose your problem.', - 'debug_pretty_table' => 'If you copy/paste the box below into a GitHub issue it will generate a table. Please do not surround this text with backticks or quotes.', - 'debug_additional_data' => 'You may also share the content of the box below. You can also copy-and-paste this into a new or existing GitHub issue. However, the content of this box may contain private information such as account names, transaction details or email addresses.', + 'debug_page' => 'Debug page', + 'debug_submit_instructions' => 'If you are running into problems, you can use the information in this box as debug information. Please copy-and-paste into a new or existing GitHub issue. It will generate a beautiful table that can be used to quickly diagnose your problem.', + 'debug_pretty_table' => 'If you copy/paste the box below into a GitHub issue it will generate a table. Please do not surround this text with backticks or quotes.', + 'debug_additional_data' => 'You may also share the content of the box below. You can also copy-and-paste this into a new or existing GitHub issue. However, the content of this box may contain private information such as account names, transaction details or email addresses.', // object groups - 'object_groups_menu_bar' => 'Groups', - 'object_groups_page_title' => 'Groups', - 'object_groups_breadcrumb' => 'Groups', - 'object_groups_index' => 'Overview', - 'object_groups' => 'Groups', - 'object_groups_empty_explain' => 'Some things in Firefly III can be divided into groups. Piggy banks for example, feature a "Group" field in the edit and create screens. When you set this field, you can edit the names and the order of the groups on this page. For more information, check out the help-pages in the top right corner, under the (?)-icon.', - 'object_group_title' => 'Title', - 'edit_object_group' => 'Edit group ":title"', - 'delete_object_group' => 'Delete group ":title"', - 'update_object_group' => 'Update group', - 'updated_object_group' => 'Successfully updated group ":title"', - 'deleted_object_group' => 'Successfully deleted group ":title"', - 'object_group' => 'Group', + 'object_groups_menu_bar' => 'Groups', + 'object_groups_page_title' => 'Groups', + 'object_groups_breadcrumb' => 'Groups', + 'object_groups_index' => 'Overview', + 'object_groups' => 'Groups', + 'object_groups_empty_explain' => 'Some things in Firefly III can be divided into groups. Piggy banks for example, feature a "Group" field in the edit and create screens. When you set this field, you can edit the names and the order of the groups on this page. For more information, check out the help-pages in the top right corner, under the (?)-icon.', + 'object_group_title' => 'Title', + 'edit_object_group' => 'Edit group ":title"', + 'delete_object_group' => 'Delete group ":title"', + 'update_object_group' => 'Update group', + 'updated_object_group' => 'Successfully updated group ":title"', + 'deleted_object_group' => 'Successfully deleted group ":title"', + 'object_group' => 'Group', // other stuff - 'placeholder' => '[Placeholder]', + 'placeholder' => '[Placeholder]', // audit log entries - 'audit_log_entries' => 'Audit log entries', - 'ale_action_log_add' => 'Added :amount to piggy bank ":name"', - 'ale_action_log_remove' => 'Removed :amount from piggy bank ":name"', - 'ale_action_clear_budget' => 'Removed from budget', - 'ale_action_update_group_title' => 'Updated transaction group title', - 'ale_action_update_date' => 'Updated transaction date', - 'ale_action_update_order' => 'Updated transaction order', - 'ale_action_clear_category' => 'Removed from category', - 'ale_action_clear_notes' => 'Removed notes', - 'ale_action_clear_tag' => 'Cleared tag', - 'ale_action_clear_all_tags' => 'Cleared all tags', - 'ale_action_set_bill' => 'Linked to bill', - 'ale_action_switch_accounts' => 'Switched source and destination account', - 'ale_action_set_budget' => 'Set budget', - 'ale_action_set_category' => 'Set category', - 'ale_action_set_source' => 'Set source account', - 'ale_action_set_destination' => 'Set destination account', - 'ale_action_update_transaction_type' => 'Changed transaction type', - 'ale_action_update_notes' => 'Changed notes', - 'ale_action_update_description' => 'Changed description', - 'ale_action_add_to_piggy' => 'Piggy bank', - 'ale_action_remove_from_piggy' => 'Piggy bank', - 'ale_action_add_tag' => 'Added tag', - 'ale_action_update_amount' => 'Updated amount', + 'audit_log_entries' => 'Audit log entries', + 'ale_action_log_add' => 'Added :amount to piggy bank ":name"', + 'ale_action_log_remove' => 'Removed :amount from piggy bank ":name"', + 'ale_action_clear_budget' => 'Removed from budget', + 'ale_action_update_group_title' => 'Updated transaction group title', + 'ale_action_update_date' => 'Updated transaction date', + 'ale_action_update_order' => 'Updated transaction order', + 'ale_action_clear_category' => 'Removed from category', + 'ale_action_clear_notes' => 'Removed notes', + 'ale_action_clear_tag' => 'Cleared tag', + 'ale_action_clear_all_tags' => 'Cleared all tags', + 'ale_action_set_bill' => 'Linked to subscription', + 'ale_action_switch_accounts' => 'Switched source and destination account', + 'ale_action_set_budget' => 'Set budget', + 'ale_action_set_category' => 'Set category', + 'ale_action_set_source' => 'Set source account', + 'ale_action_set_destination' => 'Set destination account', + 'ale_action_update_transaction_type' => 'Changed transaction type', + 'ale_action_update_notes' => 'Changed notes', + 'ale_action_update_description' => 'Changed description', + 'ale_action_add_to_piggy' => 'Piggy bank', + 'ale_action_remove_from_piggy' => 'Piggy bank', + 'ale_action_add_tag' => 'Added tag', + 'ale_action_update_amount' => 'Updated amount', // dashboard - 'enable_auto_convert' => 'Enable currency conversion', - 'disable_auto_convert' => 'Disable currency conversion', + 'enable_auto_convert' => 'Enable currency conversion', + 'disable_auto_convert' => 'Disable currency conversion', ]; // Ignore this comment diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 5e6ab8d583..b4fa88d128 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -26,242 +26,256 @@ declare(strict_types=1); return [ // new user: - 'bank_name' => 'Bank name', - 'bank_balance' => 'Balance', - 'current_balance' => 'Current balance', - 'savings_balance' => 'Savings balance', - 'credit_card_limit' => 'Credit card limit', - 'automatch' => 'Match automatically', - 'skip' => 'Skip', - 'enabled' => 'Enabled', - 'name' => 'Name', - 'active' => 'Active', - 'amount_min' => 'Minimum amount', - 'amount_max' => 'Maximum amount', - 'match' => 'Matches on', - 'strict' => 'Strict mode', - 'repeat_freq' => 'Repeats', - 'object_group' => 'Group', - 'location' => 'Location', - 'update_channel' => 'Update channel', - 'currency_id' => 'Currency', - 'transaction_currency_id' => 'Currency', - 'auto_budget_currency_id' => 'Currency', - 'external_ip' => 'Your server\'s external IP', - 'attachments' => 'Attachments', - 'BIC' => 'BIC', - 'verify_password' => 'Verify password security', - 'source_account' => 'Source account', - 'destination_account' => 'Destination account', - 'asset_destination_account' => 'Destination account', - 'include_net_worth' => 'Include in net worth', - 'asset_source_account' => 'Source account', - 'journal_description' => 'Description', - 'note' => 'Notes', - 'currency' => 'Currency', - 'account_id' => 'Asset account', - 'budget_id' => 'Budget', - 'bill_id' => 'Bill', - 'opening_balance' => 'Opening balance', - 'tagMode' => 'Tag mode', - 'virtual_balance' => 'Virtual balance', + 'bank_name' => 'Bank name', + 'bank_balance' => 'Balance', + 'current_balance' => 'Current balance', + 'savings_balance' => 'Savings balance', + 'credit_card_limit' => 'Credit card limit', + 'automatch' => 'Match automatically', + 'skip' => 'Skip', + 'enabled' => 'Enabled', + 'name' => 'Name', + 'active' => 'Active', + 'amount_min' => 'Minimum amount', + 'amount_max' => 'Maximum amount', + 'match' => 'Matches on', + 'strict' => 'Strict mode', + 'repeat_freq' => 'Repeats', + 'object_group' => 'Group', + 'location' => 'Location', + 'update_channel' => 'Update channel', + 'currency_id' => 'Currency', + 'transaction_currency_id' => 'Currency', + 'auto_budget_currency_id' => 'Currency', + 'external_ip' => 'Your server\'s external IP', + 'attachments' => 'Attachments', + 'BIC' => 'BIC', + 'verify_password' => 'Verify password security', + 'source_account' => 'Source account', + 'destination_account' => 'Destination account', + 'asset_destination_account' => 'Destination account', + 'include_net_worth' => 'Include in net worth', + 'asset_source_account' => 'Source account', + 'journal_description' => 'Description', + 'note' => 'Notes', + 'currency' => 'Currency', + 'account_id' => 'Asset account', + 'budget_id' => 'Budget', + 'bill_id' => 'Subscription', + 'opening_balance' => 'Opening balance', + 'tagMode' => 'Tag mode', + 'virtual_balance' => 'Virtual balance', // Ignore this comment - 'targetamount' => 'Target amount', - 'account_role' => 'Account role', - 'opening_balance_date' => 'Opening balance date', - 'cc_type' => 'Credit card payment plan', - 'cc_monthly_payment_date' => 'Credit card monthly payment date', - 'piggy_bank_id' => 'Piggy bank', - 'returnHere' => 'Return here', - 'returnHereExplanation' => 'After storing, return here to create another one.', - 'returnHereUpdateExplanation' => 'After updating, return here.', - 'description' => 'Description', - 'expense_account' => 'Expense account', - 'revenue_account' => 'Revenue account', - 'decimal_places' => 'Decimal places', - 'destination_amount' => 'Amount (destination)', - 'new_email_address' => 'New email address', - 'verification' => 'Verification', - 'api_key' => 'API key', - 'remember_me' => 'Remember me', - 'liability_type_id' => 'Liability type', - 'liability_type' => 'Liability type', - 'interest' => 'Interest', - 'interest_period' => 'Interest period', - 'extension_date' => 'Extension date', - 'type' => 'Type', - 'convert_Withdrawal' => 'Convert withdrawal', - 'convert_Deposit' => 'Convert deposit', - 'convert_Transfer' => 'Convert transfer', - 'amount' => 'Amount', - 'foreign_amount' => 'Foreign amount', - 'date' => 'Date', - 'interest_date' => 'Interest date', - 'book_date' => 'Book date', - 'process_date' => 'Processing date', - 'category' => 'Category', - 'tags' => 'Tags', - 'deletePermanently' => 'Delete permanently', - 'cancel' => 'Cancel', - 'targetdate' => 'Target date', - 'startdate' => 'Start date', - 'tag' => 'Tag', - 'under' => 'Under', - 'symbol' => 'Symbol', - 'code' => 'Code', - 'iban' => 'IBAN', - 'account_number' => 'Account number', - 'creditCardNumber' => 'Credit card number', - 'has_headers' => 'Headers', - 'date_format' => 'Date format', - 'specifix' => 'Bank- or file specific fixes', - 'attachments[]' => 'Attachments', - 'title' => 'Title', - 'notes' => 'Notes', - 'filename' => 'File name', - 'mime' => 'Mime type', - 'size' => 'Size', - 'trigger' => 'Trigger', - 'stop_processing' => 'Stop processing', - 'start_date' => 'Start of range', - 'end_date' => 'End of range', - 'enddate' => 'End date', - 'move_rules_before_delete' => 'Rule group', - 'start' => 'Start of range', - 'end' => 'End of range', - 'delete_account' => 'Delete account ":name"', - 'delete_webhook' => 'Delete webhook ":title"', - 'delete_bill' => 'Delete bill ":name"', - 'delete_budget' => 'Delete budget ":name"', - 'delete_category' => 'Delete category ":name"', - 'delete_currency' => 'Delete currency ":name"', - 'delete_journal' => 'Delete transaction with description ":description"', - 'delete_attachment' => 'Delete attachment ":name"', - 'delete_rule' => 'Delete rule ":title"', - 'delete_rule_group' => 'Delete rule group ":title"', - 'delete_link_type' => 'Delete link type ":name"', - 'delete_user' => 'Delete user ":email"', - 'delete_recurring' => 'Delete recurring transaction ":title"', - 'user_areYouSure' => 'If you delete user ":email", everything will be gone. There is no undo, undelete or anything. If you delete yourself, you will lose access to this instance of Firefly III.', - 'attachment_areYouSure' => 'Are you sure you want to delete the attachment named ":name"?', - 'account_areYouSure' => 'Are you sure you want to delete the account named ":name"?', - 'account_areYouSure_js' => 'Are you sure you want to delete the account named "{name}"?', - 'bill_areYouSure' => 'Are you sure you want to delete the bill named ":name"?', - 'rule_areYouSure' => 'Are you sure you want to delete the rule titled ":title"?', - 'object_group_areYouSure' => 'Are you sure you want to delete the group titled ":title"?', - 'ruleGroup_areYouSure' => 'Are you sure you want to delete the rule group titled ":title"?', - 'budget_areYouSure' => 'Are you sure you want to delete the budget named ":name"?', - 'webhook_areYouSure' => 'Are you sure you want to delete the webhook named ":title"?', - 'category_areYouSure' => 'Are you sure you want to delete the category named ":name"?', - 'recurring_areYouSure' => 'Are you sure you want to delete the recurring transaction titled ":title"?', - 'currency_areYouSure' => 'Are you sure you want to delete the currency named ":name"?', - 'piggyBank_areYouSure' => 'Are you sure you want to delete the piggy bank named ":name"?', - 'journal_areYouSure' => 'Are you sure you want to delete the transaction described ":description"?', - 'mass_journal_are_you_sure' => 'Are you sure you want to delete these transactions?', + 'targetamount' => 'Target amount', + 'target_amount' => 'Target amount', + 'account_role' => 'Account role', + 'opening_balance_date' => 'Opening balance date', + 'cc_type' => 'Credit card payment plan', + 'cc_monthly_payment_date' => 'Credit card monthly payment date', + 'piggy_bank_id' => 'Piggy bank', + 'returnHere' => 'Return here', + 'returnHereExplanation' => 'After storing, return here to create another one.', + 'returnHereUpdateExplanation' => 'After updating, return here.', + 'description' => 'Description', + 'expense_account' => 'Expense account', + 'revenue_account' => 'Revenue account', + 'decimal_places' => 'Decimal places', + 'destination_amount' => 'Amount (destination)', + 'new_email_address' => 'New email address', + 'verification' => 'Verification', + 'api_key' => 'API key', + 'remember_me' => 'Remember me', + 'liability_type_id' => 'Liability type', + 'liability_type' => 'Liability type', + 'interest' => 'Interest', + 'interest_period' => 'Interest period', + 'extension_date' => 'Extension date', + 'type' => 'Type', + 'convert_Withdrawal' => 'Convert withdrawal', + 'convert_Deposit' => 'Convert deposit', + 'convert_Transfer' => 'Convert transfer', + 'amount' => 'Amount', + 'foreign_amount' => 'Foreign amount', + 'date' => 'Date', + 'interest_date' => 'Interest date', + 'book_date' => 'Book date', + 'process_date' => 'Processing date', + 'category' => 'Category', + 'tags' => 'Tags', + 'deletePermanently' => 'Delete permanently', + 'cancel' => 'Cancel', + 'targetdate' => 'Target date', + 'target_date' => 'Target date', + 'startdate' => 'Start date', + 'start_date' => 'Start date', + 'tag' => 'Tag', + + // exchange rates + 'from_currency_to_currency' => '{from} → {to}', + 'to_currency_from_currency' => '{to} → {from}', + 'rate' => 'Rate', + + 'under' => 'Under', + 'symbol' => 'Symbol', + 'code' => 'Code', + 'iban' => 'IBAN', + 'account_number' => 'Account number', + 'creditCardNumber' => 'Credit card number', + 'has_headers' => 'Headers', + 'date_format' => 'Date format', + 'attachments[]' => 'Attachments', + 'title' => 'Title', + 'notes' => 'Notes', + 'filename' => 'File name', + 'mime' => 'Mime type', + 'size' => 'Size', + 'trigger' => 'Trigger', + 'stop_processing' => 'Stop processing', + 'end_date' => 'End date', + 'enddate' => 'End date', + 'move_rules_before_delete' => 'Rule group', + 'start' => 'Start of range', + 'end' => 'End of range', + 'delete_account' => 'Delete account ":name"', + 'delete_webhook' => 'Delete webhook ":title"', + 'delete_bill' => 'Delete subscription ":name"', + 'delete_budget' => 'Delete budget ":name"', + 'delete_category' => 'Delete category ":name"', + 'delete_currency' => 'Delete currency ":name"', + 'delete_journal' => 'Delete transaction with description ":description"', + 'delete_attachment' => 'Delete attachment ":name"', + 'delete_rule' => 'Delete rule ":title"', + 'delete_rule_group' => 'Delete rule group ":title"', + 'delete_link_type' => 'Delete link type ":name"', + 'delete_user' => 'Delete user ":email"', + 'delete_recurring' => 'Delete recurring transaction ":title"', + 'user_areYouSure' => 'If you delete user ":email", everything will be gone. There is no undo, undelete or anything. If you delete yourself, you will lose access to this instance of Firefly III.', + 'attachment_areYouSure' => 'Are you sure you want to delete the attachment named ":name"?', + 'account_areYouSure' => 'Are you sure you want to delete the account named ":name"?', + 'account_areYouSure_js' => 'Are you sure you want to delete the account named "{name}"?', + 'bill_areYouSure' => 'Are you sure you want to delete the subscription named ":name"?', + 'rule_areYouSure' => 'Are you sure you want to delete the rule titled ":title"?', + 'object_group_areYouSure' => 'Are you sure you want to delete the group titled ":title"?', + 'ruleGroup_areYouSure' => 'Are you sure you want to delete the rule group titled ":title"?', + 'budget_areYouSure' => 'Are you sure you want to delete the budget named ":name"?', + 'webhook_areYouSure' => 'Are you sure you want to delete the webhook named ":title"?', + 'category_areYouSure' => 'Are you sure you want to delete the category named ":name"?', + 'recurring_areYouSure' => 'Are you sure you want to delete the recurring transaction titled ":title"?', + 'currency_areYouSure' => 'Are you sure you want to delete the currency named ":name"?', + 'piggyBank_areYouSure' => 'Are you sure you want to delete the piggy bank named ":name"?', + 'journal_areYouSure' => 'Are you sure you want to delete the transaction described ":description"?', + 'mass_journal_are_you_sure' => 'Are you sure you want to delete these transactions?', // Ignore this comment - 'tag_areYouSure' => 'Are you sure you want to delete the tag ":tag"?', - 'journal_link_areYouSure' => 'Are you sure you want to delete the link between :source and :destination?', - 'linkType_areYouSure' => 'Are you sure you want to delete the link type ":name" (":inward" / ":outward")?', - 'permDeleteWarning' => 'Deleting stuff from Firefly III is permanent and cannot be undone.', - 'mass_make_selection' => 'You can still prevent items from being deleted by removing the checkbox.', - 'delete_all_permanently' => 'Delete selected permanently', - 'update_all_journals' => 'Update these transactions', - 'also_delete_transactions' => 'The only transaction connected to this account will be deleted as well.|All :count transactions connected to this account will be deleted as well.', - 'also_delete_transactions_js' => 'No transactions|The only transaction connected to this account will be deleted as well.|All {count} transactions connected to this account will be deleted as well.', - 'also_delete_connections' => 'The only transaction linked with this link type will lose this connection.|All :count transactions linked with this link type will lose their connection.', - 'also_delete_rules' => 'The only rule connected to this rule group will be deleted as well.|All :count rules connected to this rule group will be deleted as well.', - 'also_delete_piggyBanks' => 'The only piggy bank connected to this account will be deleted as well.|All :count piggy bank connected to this account will be deleted as well.', - 'also_delete_piggyBanks_js' => 'No piggy banks|The only piggy bank connected to this account will be deleted as well.|All {count} piggy banks connected to this account will be deleted as well.', - 'not_delete_piggy_banks' => 'The piggy bank connected to this group will not be deleted.|The :count piggy banks connected to this group will not be deleted.', - 'bill_keep_transactions' => 'The only transaction connected to this bill will not be deleted.|All :count transactions connected to this bill will be spared deletion.', - 'budget_keep_transactions' => 'The only transaction connected to this budget will not be deleted.|All :count transactions connected to this budget will be spared deletion.', - 'category_keep_transactions' => 'The only transaction connected to this category will not be deleted.|All :count transactions connected to this category will be spared deletion.', - 'recurring_keep_transactions' => 'The only transaction created by this recurring transaction will not be deleted.|All :count transactions created by this recurring transaction will be spared deletion.', - 'tag_keep_transactions' => 'The only transaction connected to this tag will not be deleted.|All :count transactions connected to this tag will be spared deletion.', - 'check_for_updates' => 'Check for updates', - 'liability_direction' => 'Liability in/out', - 'delete_object_group' => 'Delete group ":title"', - 'email' => 'Email address', - 'password' => 'Password', - 'password_confirmation' => 'Password (again)', - 'blocked' => 'Is blocked?', - 'blocked_code' => 'Reason for block', - 'login_name' => 'Login', - 'is_owner' => 'Is admin?', - 'url' => 'URL', - 'bill_end_date' => 'End date', + 'tag_areYouSure' => 'Are you sure you want to delete the tag ":tag"?', + 'journal_link_areYouSure' => 'Are you sure you want to delete the link between :source and :destination?', + 'linkType_areYouSure' => 'Are you sure you want to delete the link type ":name" (":inward" / ":outward")?', + 'permDeleteWarning' => 'Deleting stuff from Firefly III is permanent and cannot be undone.', + 'mass_make_selection' => 'You can still prevent items from being deleted by removing the checkbox.', + 'delete_all_permanently' => 'Delete selected permanently', + 'update_all_journals' => 'Update these transactions', + 'also_delete_transactions' => 'The only transaction connected to this account will be deleted as well.|All :count transactions connected to this account will be deleted as well.', + 'also_delete_transactions_js' => 'No transactions|The only transaction connected to this account will be deleted as well.|All {count} transactions connected to this account will be deleted as well.', + 'also_delete_connections' => 'The only transaction linked with this link type will lose this connection.|All :count transactions linked with this link type will lose their connection.', + 'also_delete_rules' => 'The only rule connected to this rule group will be deleted as well.|All :count rules connected to this rule group will be deleted as well.', + 'also_delete_piggyBanks' => 'The only piggy bank connected to this account will be deleted as well.|All :count piggy bank connected to this account will be deleted as well.', + 'also_delete_piggyBanks_js' => 'No piggy banks|The only piggy bank connected to this account will be deleted as well.|All {count} piggy banks connected to this account will be deleted as well.', + 'not_delete_piggy_banks' => 'The piggy bank connected to this group will not be deleted.|The :count piggy banks connected to this group will not be deleted.', + 'bill_keep_transactions' => 'The only transaction connected to this subscription will not be deleted.|All :count transactions connected to this subscription will be spared deletion.', + 'budget_keep_transactions' => 'The only transaction connected to this budget will not be deleted.|All :count transactions connected to this budget will be spared deletion.', + 'category_keep_transactions' => 'The only transaction connected to this category will not be deleted.|All :count transactions connected to this category will be spared deletion.', + 'recurring_keep_transactions' => 'The only transaction created by this recurring transaction will not be deleted.|All :count transactions created by this recurring transaction will be spared deletion.', + 'tag_keep_transactions' => 'The only transaction connected to this tag will not be deleted.|All :count transactions connected to this tag will be spared deletion.', + 'check_for_updates' => 'Check for updates', + 'liability_direction' => 'Liability in/out', + 'delete_object_group' => 'Delete group ":title"', + 'email' => 'Email address', + 'password' => 'Password', + 'password_confirmation' => 'Password (again)', + 'blocked' => 'Is blocked?', + 'blocked_code' => 'Reason for block', + 'login_name' => 'Login', + 'is_owner' => 'Is admin?', + 'url' => 'URL', + 'bill_end_date' => 'End date', // import - 'apply_rules' => 'Apply rules', - 'artist' => 'Artist', - 'album' => 'Album', - 'song' => 'Song', + 'apply_rules' => 'Apply rules', + 'artist' => 'Artist', + 'album' => 'Album', + 'song' => 'Song', // admin - 'domain' => 'Domain', - 'single_user_mode' => 'Disable user registration', - 'is_demo_site' => 'Is demo site', + 'domain' => 'Domain', + 'single_user_mode' => 'Disable user registration', + 'is_demo_site' => 'Is demo site', // import - 'configuration_file' => 'Configuration file', - 'csv_comma' => 'A comma (,)', - 'csv_semicolon' => 'A semicolon (;)', - 'csv_tab' => 'A tab (invisible)', - 'csv_delimiter' => 'CSV field delimiter', - 'client_id' => 'Client ID', - 'app_id' => 'App ID', - 'secret' => 'Secret', - 'public_key' => 'Public key', - 'country_code' => 'Country code', - 'provider_code' => 'Bank or data-provider', - 'fints_url' => 'FinTS API URL', - 'fints_port' => 'Port', - 'fints_bank_code' => 'Bank code', - 'fints_username' => 'Username', - 'fints_password' => 'PIN / Password', - 'fints_account' => 'FinTS account', - 'local_account' => 'Firefly III account', + 'configuration_file' => 'Configuration file', + 'csv_comma' => 'A comma (,)', + 'csv_semicolon' => 'A semicolon (;)', + 'csv_tab' => 'A tab (invisible)', + 'csv_delimiter' => 'CSV field delimiter', + 'client_id' => 'Client ID', + 'app_id' => 'App ID', + 'secret' => 'Secret', + 'public_key' => 'Public key', + 'country_code' => 'Country code', + 'provider_code' => 'Bank or data-provider', + 'fints_url' => 'FinTS API URL', + 'fints_port' => 'Port', + 'fints_bank_code' => 'Bank code', + 'fints_username' => 'Username', + 'fints_password' => 'PIN / Password', + 'fints_account' => 'FinTS account', + 'local_account' => 'Firefly III account', // Ignore this comment - 'from_date' => 'Date from', - 'to_date' => 'Date to', - 'due_date' => 'Due date', - 'payment_date' => 'Payment date', - 'invoice_date' => 'Invoice date', - 'internal_reference' => 'Internal reference', - 'inward' => 'Inward description', - 'outward' => 'Outward description', - 'rule_group_id' => 'Rule group', - 'transaction_description' => 'Transaction description', - 'first_date' => 'First date', - 'transaction_type' => 'Transaction type', - 'repeat_until' => 'Repeat until', - 'recurring_description' => 'Recurring transaction description', - 'repetition_type' => 'Type of repetition', - 'foreign_currency_id' => 'Foreign currency', - 'repetition_end' => 'Repetition ends', - 'repetitions' => 'Repetitions', - 'calendar' => 'Calendar', - 'weekend' => 'Weekend', - 'client_secret' => 'Client secret', - 'withdrawal_destination_id' => 'Destination account', - 'deposit_source_id' => 'Source account', - 'expected_on' => 'Expected on', - 'paid' => 'Paid', - 'auto_budget_type' => 'Auto-budget', - 'auto_budget_amount' => 'Auto-budget amount', - 'auto_budget_period' => 'Auto-budget period', - 'collected' => 'Collected', - 'submitted' => 'Submitted', - 'key' => 'Key', - 'value' => 'Content of record', - 'webhook_delivery' => 'Delivery', - 'webhook_response' => 'Response', - 'webhook_trigger' => 'Trigger', + 'from_date' => 'Date from', + 'to_date' => 'Date to', + 'due_date' => 'Due date', + 'payment_date' => 'Payment date', + 'invoice_date' => 'Invoice date', + 'internal_reference' => 'Internal reference', + 'inward' => 'Inward description', + 'outward' => 'Outward description', + 'rule_group_id' => 'Rule group', + 'transaction_description' => 'Transaction description', + 'first_date' => 'First date', + 'transaction_type' => 'Transaction type', + 'repeat_until' => 'Repeat until', + 'recurring_description' => 'Recurring transaction description', + 'repetition_type' => 'Type of repetition', + 'foreign_currency_id' => 'Foreign currency', + 'repetition_end' => 'Repetition ends', + 'repetitions' => 'Repetitions', + 'calendar' => 'Calendar', + 'weekend' => 'Weekend', + 'client_secret' => 'Client secret', + 'withdrawal_destination_id' => 'Destination account', + 'deposit_source_id' => 'Source account', + 'expected_on' => 'Expected on', + 'paid' => 'Paid', + 'auto_budget_type' => 'Auto-budget', + 'auto_budget_amount' => 'Auto-budget amount', + 'auto_budget_period' => 'Auto-budget period', + 'collected' => 'Collected', + 'submitted' => 'Submitted', + 'key' => 'Key', + 'value' => 'Content of record', + 'webhook_delivery' => 'Delivery', + 'webhook_response' => 'Response', + 'webhook_trigger' => 'Trigger', + 'pushover_app_token' => 'Pushover app token', + 'pushover_user_token' => 'Pushover user token', + 'ntfy_server' => 'Ntfy server', + 'ntfy_topic' => 'Ntfy topic', + 'ntfy_auth' => 'Ntfy authentication enabled', + 'ntfy_user' => 'Ntfy username', + 'ntfy_pass' => 'Ntfy password', ]; // Ignore this comment diff --git a/resources/lang/en_US/intro.php b/resources/lang/en_US/intro.php index 0f0d60abde..b4ff618784 100644 --- a/resources/lang/en_US/intro.php +++ b/resources/lang/en_US/intro.php @@ -26,128 +26,135 @@ declare(strict_types=1); return [ // index - 'index_intro' => 'Welcome to the index page of Firefly III. Please take the time to walk through this intro to get a feeling of how Firefly III works.', - 'index_accounts-chart' => 'This chart shows the current balance of your asset accounts. You can select the accounts visible here in your preferences.', - 'index_box_out_holder' => 'This little box and the boxes next to this one will give you a quick overview of your financial situation.', - 'index_help' => 'If you ever need help with a page or a form, press this button.', - 'index_outro' => 'Most pages of Firefly III will start with a little tour like this one. Please contact me when you have questions or comments. Enjoy!', - 'index_sidebar-toggle' => 'To create new transactions, accounts or other things, use the menu under this icon.', - 'index_cash_account' => 'These are the accounts created so far. You can use the cash account to track cash expenses but it\'s not mandatory of course.', + 'index_intro' => 'Welcome to the index page of Firefly III. Please take the time to walk through this intro to get a feeling of how Firefly III works.', + 'index_accounts-chart' => 'This chart shows the current balance of your asset accounts. You can select the accounts visible here in your preferences.', + 'index_box_out_holder' => 'This little box and the boxes next to this one will give you a quick overview of your financial situation.', + 'index_help' => 'If you ever need help with a page or a form, press this button.', + 'index_outro' => 'Most pages of Firefly III will start with a little tour like this one. Please contact me when you have questions or comments. Enjoy!', + 'index_sidebar-toggle' => 'To create new transactions, accounts or other things, use the menu under this icon.', + 'index_cash_account' => 'These are the accounts created so far. You can use the cash account to track cash expenses but it\'s not mandatory of course.', // transactions - 'transactions_create_basic_info' => 'Enter the basic information of your transaction. Source, destination, date and description.', - 'transactions_create_amount_info' => 'Enter the amount of the transaction. If necessary the fields will auto-update for foreign amount info.', - 'transactions_create_optional_info' => 'All of these fields are optional. Adding meta-data here will make your transactions better organised.', - 'transactions_create_split' => 'If you want to split a transaction, add more splits with this button', + 'transactions_create_basic_info' => 'Enter the basic information of your transaction. Source, destination, date and description.', + 'transactions_create_amount_info' => 'Enter the amount of the transaction. If necessary the fields will auto-update for foreign amount info.', + 'transactions_create_optional_info' => 'All of these fields are optional. Adding meta-data here will make your transactions better organised.', + 'transactions_create_split' => 'If you want to split a transaction, add more splits with this button', // create account: - 'accounts_create_iban' => 'Give your accounts a valid IBAN. This could make a data import very easy in the future.', - 'accounts_create_asset_opening_balance' => 'Assets accounts may have an "opening balance", indicating the start of this account\'s history in Firefly III.', - 'accounts_create_asset_currency' => 'Firefly III supports multiple currencies. Asset accounts have one main currency, which you must set here.', - 'accounts_create_asset_virtual' => 'It can sometimes help to give your account a virtual balance: an extra amount always added to or removed from the actual balance.', + 'accounts_create_iban' => 'Give your accounts a valid IBAN. This could make a data import very easy in the future.', + 'accounts_create_asset_opening_balance' => 'Assets accounts may have an "opening balance", indicating the start of this account\'s history in Firefly III.', + 'accounts_create_asset_currency' => 'Firefly III supports multiple currencies. Asset accounts have one main currency, which you must set here.', + 'accounts_create_asset_virtual' => 'It can sometimes help to give your account a virtual balance: an extra amount always added to or removed from the actual balance.', // budgets index - 'budgets_index_intro' => 'Budgets are used to manage your finances and form one of the core functions of Firefly III.', - 'budgets_index_see_expenses_bar' => 'Spending money will slowly fill this bar.', - 'budgets_index_navigate_periods' => 'Navigate through periods to easily set budgets ahead of time.', - 'budgets_index_new_budget' => 'Create new budgets as you see fit.', - 'budgets_index_list_of_budgets' => 'Use this table to set the amounts for each budget and see how you are doing.', - 'budgets_index_outro' => 'To learn more about budgeting, checkout the help icon in the top right corner.', + 'budgets_index_intro' => 'Budgets are used to manage your finances and form one of the core functions of Firefly III.', + 'budgets_index_see_expenses_bar' => 'Spending money will slowly fill this bar.', + 'budgets_index_navigate_periods' => 'Navigate through periods to easily set budgets ahead of time.', + 'budgets_index_new_budget' => 'Create new budgets as you see fit.', + 'budgets_index_list_of_budgets' => 'Use this table to set the amounts for each budget and see how you are doing.', + 'budgets_index_outro' => 'To learn more about budgeting, checkout the help icon in the top right corner.', // Ignore this comment // reports (index) - 'reports_index_intro' => 'Use these reports to get detailed insights in your finances.', - 'reports_index_inputReportType' => 'Pick a report type. Check out the help pages to see what each report shows you.', - 'reports_index_inputAccountsSelect' => 'You can exclude or include asset accounts as you see fit.', - 'reports_index_inputDateRange' => 'The selected date range is entirely up to you: from one day to 10 years or more.', - 'reports_index_extra-options-box' => 'Depending on the report you have selected, you can select extra filters and options here. Watch this box when you change report types.', + 'reports_index_intro' => 'Use these reports to get detailed insights in your finances.', + 'reports_index_inputReportType' => 'Pick a report type. Check out the help pages to see what each report shows you.', + 'reports_index_inputAccountsSelect' => 'You can exclude or include asset accounts as you see fit.', + 'reports_index_inputDateRange' => 'The selected date range is entirely up to you: from one day to 10 years or more.', + 'reports_index_extra-options-box' => 'Depending on the report you have selected, you can select extra filters and options here. Watch this box when you change report types.', // reports (reports) - 'reports_report_default_intro' => 'This report will give you a quick and comprehensive overview of your finances. If you wish to see anything else, please don\'t hestitate to contact me!', - 'reports_report_audit_intro' => 'This report will give you detailed insights in your asset accounts.', - 'reports_report_audit_optionsBox' => 'Use these check boxes to show or hide the columns you are interested in.', + 'reports_report_default_intro' => 'This report will give you a quick and comprehensive overview of your finances. If you wish to see anything else, please don\'t hestitate to contact me!', + 'reports_report_audit_intro' => 'This report will give you detailed insights in your asset accounts.', + 'reports_report_audit_optionsBox' => 'Use these check boxes to show or hide the columns you are interested in.', - 'reports_report_category_intro' => 'This report will give you insight in one or multiple categories.', - 'reports_report_category_pieCharts' => 'These charts will give you insight in expenses and income per category or per account.', - 'reports_report_category_incomeAndExpensesChart' => 'This chart shows your expenses and income per category.', + 'reports_report_category_intro' => 'This report will give you insight in one or multiple categories.', + 'reports_report_category_pieCharts' => 'These charts will give you insight in expenses and income per category or per account.', + 'reports_report_category_incomeAndExpensesChart' => 'This chart shows your expenses and income per category.', - 'reports_report_tag_intro' => 'This report will give you insight in one or multiple tags.', - 'reports_report_tag_pieCharts' => 'These charts will give you insight in expenses and income per tag, account, category or budget.', - 'reports_report_tag_incomeAndExpensesChart' => 'This chart shows your expenses and income per tag.', + 'reports_report_tag_intro' => 'This report will give you insight in one or multiple tags.', + 'reports_report_tag_pieCharts' => 'These charts will give you insight in expenses and income per tag, account, category or budget.', + 'reports_report_tag_incomeAndExpensesChart' => 'This chart shows your expenses and income per tag.', - 'reports_report_budget_intro' => 'This report will give you insight in one or multiple budgets.', - 'reports_report_budget_pieCharts' => 'These charts will give you insight in expenses per budget or per account.', - 'reports_report_budget_incomeAndExpensesChart' => 'This chart shows your expenses per budget.', + 'reports_report_budget_intro' => 'This report will give you insight in one or multiple budgets.', + 'reports_report_budget_pieCharts' => 'These charts will give you insight in expenses per budget or per account.', + 'reports_report_budget_incomeAndExpensesChart' => 'This chart shows your expenses per budget.', // create transaction - 'transactions_create_switch_box' => 'Use these buttons to quickly switch the type of transaction you wish to save.', - 'transactions_create_ffInput_category' => 'You can freely type in this field. Previously created categories will be suggested.', - 'transactions_create_withdrawal_ffInput_budget' => 'Link your withdrawal to a budget for better financial control.', - 'transactions_create_withdrawal_currency_dropdown_amount' => 'Use this dropdown when your withdrawal is in another currency.', - 'transactions_create_deposit_currency_dropdown_amount' => 'Use this dropdown when your deposit is in another currency.', - 'transactions_create_transfer_ffInput_piggy_bank_id' => 'Select a piggy bank and link this transfer to your savings.', + 'transactions_create_switch_box' => 'Use these buttons to quickly switch the type of transaction you wish to save.', + 'transactions_create_ffInput_category' => 'You can freely type in this field. Previously created categories will be suggested.', + 'transactions_create_withdrawal_ffInput_budget' => 'Link your withdrawal to a budget for better financial control.', + 'transactions_create_withdrawal_currency_dropdown_amount' => 'Use this dropdown when your withdrawal is in another currency.', + 'transactions_create_deposit_currency_dropdown_amount' => 'Use this dropdown when your deposit is in another currency.', + 'transactions_create_transfer_ffInput_piggy_bank_id' => 'Select a piggy bank and link this transfer to your savings.', // piggy banks index: - 'piggy-banks_index_saved' => 'This field shows you how much you\'ve saved in each piggy bank.', - 'piggy-banks_index_button' => 'Next to this progress bar are two buttons (+ and -) to add or remove money from each piggy bank.', - 'piggy-banks_index_accountStatus' => 'For each asset account with at least one piggy bank the status is listed in this table.', + 'piggy-banks_index_saved' => 'This field shows you how much you\'ve saved in each piggy bank.', + 'piggy-banks_index_button' => 'Next to this progress bar are two buttons (+ and -) to add or remove money from each piggy bank.', + 'piggy-banks_index_accountStatus' => 'For each asset account with at least one piggy bank the status is listed in this table.', // Ignore this comment // create piggy - 'piggy-banks_create_name' => 'What is your goal? A new couch, a camera, money for emergencies?', - 'piggy-banks_create_date' => 'You can set a target date or a deadline for your piggy bank.', + 'piggy-banks_create_name' => 'What is your goal? A new couch, a camera, money for emergencies?', + 'piggy-banks_create_date' => 'You can set a target date or a deadline for your piggy bank.', // show piggy - 'piggy-banks_show_piggyChart' => 'This chart will show the history of this piggy bank.', - 'piggy-banks_show_piggyDetails' => 'Some details about your piggy bank', - 'piggy-banks_show_piggyEvents' => 'Any additions or removals are also listed here.', + 'piggy-banks_show_piggyChart' => 'This chart will show the history of this piggy bank.', + 'piggy-banks_show_piggyDetails' => 'Some details about your piggy bank', + 'piggy-banks_show_piggyEvents' => 'Any additions or removals are also listed here.', // bill index - 'bills_index_rules' => 'Here you see which rules will check if this bill is hit', - 'bills_index_paid_in_period' => 'This field indicates when the bill was last paid.', - 'bills_index_expected_in_period' => 'This field indicates for each bill if and when the next bill is expected to hit.', + 'bills_index_rules' => 'Here you see which rules will check if this subscription is hit', + 'bills_index_paid_in_period' => 'This field indicates when the subscription was last paid.', + 'bills_index_expected_in_period' => 'This field indicates for each subscription if and when the next subscription is expected to hit.', + + 'subscriptions_index_rules' => 'Here you see which rules will check if this subscription is hit', + 'subscriptions_index_paid_in_period' => 'This field indicates when the subscription was last paid.', + 'subscriptions_index_expected_in_period' => 'This field indicates for each subscription if and when the next subscription is expected to hit.', // show bill - 'bills_show_billInfo' => 'This table shows some general information about this bill.', - 'bills_show_billButtons' => 'Use this button to re-scan old transactions so they will be matched to this bill.', - 'bills_show_billChart' => 'This chart shows the transactions linked to this bill.', + 'bills_show_billInfo' => 'This table shows some general information about this subscription.', + 'bills_show_billButtons' => 'Use this button to re-scan old transactions so they will be matched to this subscription.', + 'bills_show_billChart' => 'This chart shows the transactions linked to this subscription.', + 'subscriptions_show_billInfo' => 'This table shows some general information about this subscription.', + 'subscriptions_show_billButtons' => 'Use this button to re-scan old transactions so they will be matched to this subscription.', + 'subscriptions_show_billChart' => 'This chart shows the transactions linked to this subscription.', // create bill - 'bills_create_intro' => 'Use bills to track the amount of money you\'re due every period. Think about expenses like rent, insurance or mortgage payments.', - 'bills_create_name' => 'Use a descriptive name such as "Rent" or "Health insurance".', + 'bills_create_intro' => 'Use subscriptions to track the amount of money you\'re due every period. Think about expenses like rent, insurance or mortgage payments.', + 'bills_create_name' => 'Use a descriptive name such as "Rent" or "Health insurance".', // 'bills_create_match' => 'To match transactions, use terms from those transactions or the expense account involved. All words must match.', - 'bills_create_amount_min_holder' => 'Select a minimum and maximum amount for this bill.', - 'bills_create_repeat_freq_holder' => 'Most bills repeat monthly, but you can set another frequency here.', - 'bills_create_skip_holder' => 'If a bill repeats every 2 weeks, the "skip"-field should be set to "1" to skip every other week.', + 'bills_create_amount_min_holder' => 'Select a minimum and maximum amount for this subscription.', + 'bills_create_repeat_freq_holder' => 'Most subscriptions repeat monthly, but you can set another frequency here.', + 'bills_create_skip_holder' => 'If a subscription repeats every 2 weeks, the "skip"-field should be set to "1" to skip every other week.', // rules index - 'rules_index_intro' => 'Firefly III allows you to manage rules, that will automagically be applied to any transaction you create or edit.', - 'rules_index_new_rule_group' => 'You can combine rules in groups for easier management.', - 'rules_index_new_rule' => 'Create as many rules as you like.', - 'rules_index_prio_buttons' => 'Order them any way you see fit.', - 'rules_index_test_buttons' => 'You can test your rules or apply them to existing transactions.', - 'rules_index_rule-triggers' => 'Rules have "triggers" and "actions" that you can order by drag-and-drop.', - 'rules_index_outro' => 'Be sure to check out the help pages using the (?) icon in the top right!', + 'rules_index_intro' => 'Firefly III allows you to manage rules, that will automagically be applied to any transaction you create or edit.', + 'rules_index_new_rule_group' => 'You can combine rules in groups for easier management.', + 'rules_index_new_rule' => 'Create as many rules as you like.', + 'rules_index_prio_buttons' => 'Order them any way you see fit.', + 'rules_index_test_buttons' => 'You can test your rules or apply them to existing transactions.', + 'rules_index_rule-triggers' => 'Rules have "triggers" and "actions" that you can order by drag-and-drop.', + 'rules_index_outro' => 'Be sure to check out the help pages using the (?) icon in the top right!', // create rule: - 'rules_create_mandatory' => 'Choose a descriptive title, and set when the rule should be fired.', - 'rules_create_ruletriggerholder' => 'Add as many triggers as you like, but remember that ALL triggers must match before any actions are fired.', - 'rules_create_test_rule_triggers' => 'Use this button to see which transactions would match your rule.', - 'rules_create_actions' => 'Set as many actions as you like.', + 'rules_create_mandatory' => 'Choose a descriptive title, and set when the rule should be fired.', + 'rules_create_ruletriggerholder' => 'Add as many triggers as you like, but remember that ALL triggers must match before any actions are fired.', + 'rules_create_test_rule_triggers' => 'Use this button to see which transactions would match your rule.', + 'rules_create_actions' => 'Set as many actions as you like.', // Ignore this comment // preferences - 'preferences_index_tabs' => 'More options are available behind these tabs.', + 'preferences_index_tabs' => 'More options are available behind these tabs.', // currencies - 'currencies_index_intro' => 'Firefly III supports multiple currencies, which you can change on this page.', - 'currencies_index_default' => 'Firefly III has one default currency.', - 'currencies_index_buttons' => 'Use these buttons to change the default currency or enable other currencies.', + 'currencies_index_intro' => 'Firefly III supports multiple currencies, which you can change on this page.', + 'currencies_index_default' => 'Firefly III has one default currency.', + 'currencies_index_buttons' => 'Use these buttons to change the default currency or enable other currencies.', // create currency - 'currencies_create_code' => 'This code should be ISO compliant (Google it for your new currency).', + 'currencies_create_code' => 'This code should be ISO compliant (Google it for your new currency).', ]; // Ignore this comment diff --git a/resources/lang/en_US/list.php b/resources/lang/en_US/list.php index 242234783f..190f9fde91 100644 --- a/resources/lang/en_US/list.php +++ b/resources/lang/en_US/list.php @@ -85,7 +85,7 @@ return [ 'to' => 'To', 'budget' => 'Budget', 'category' => 'Category', - 'bill' => 'Bill', + 'bill' => 'Subscription', 'withdrawal' => 'Withdrawal', 'deposit' => 'Deposit', 'transfer' => 'Transfer', @@ -105,7 +105,7 @@ return [ 'accounts_count' => 'Number of accounts', 'journals_count' => 'Number of transactions', 'attachments_count' => 'Number of attachments', - 'bills_count' => 'Number of bills', + 'bills_count' => 'Number of subscriptions', 'categories_count' => 'Number of categories', 'budget_count' => 'Number of budgets', 'rule_and_groups_count' => 'Number of rules and rule groups', diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index bdf68a07fd..30b7a26a89 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -25,6 +25,9 @@ declare(strict_types=1); return [ + 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', + 'invalid_account_currency' => 'This account does not use the currency you have selected', + 'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".', 'filter_must_be_in' => 'Filter ":filter" must be one of: :values', 'filter_not_string' => 'Filter ":filter" is expected to be a string of text', 'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.', diff --git a/resources/views/accounts/edit.twig b/resources/views/accounts/edit.twig index 9b9ce38834..e5cbdb0266 100644 --- a/resources/views/accounts/edit.twig +++ b/resources/views/accounts/edit.twig @@ -27,9 +27,12 @@
{{ ExpandedForm.text('name', account.name) }} - {% if account.accountType.type == 'Default account' or account.accountType.type == 'Asset account' or objectType == 'liabilities' %} + {% if canEditCurrency and (account.accountType.type == 'Default account' or account.accountType.type == 'Asset account' or objectType == 'liabilities') %} {{ CurrencyForm.currencyList('currency_id', null, {helpText:'account_default_currency'|_}) }} - + {% endif %} + {% if not canEditCurrency and (account.accountType.type == 'Default account' or account.accountType.type == 'Asset account' or objectType == 'liabilities') %} + + {{ ExpandedForm.staticText('currency_id', trans('firefly.account_locked_currency', {name: currency.name})) }} {% endif %} {% if objectType == 'liabilities' %} diff --git a/resources/views/accounts/show.twig b/resources/views/accounts/show.twig index 0210282438..0d4d1c7da8 100644 --- a/resources/views/accounts/show.twig +++ b/resources/views/accounts/show.twig @@ -11,9 +11,15 @@

+ {% if balances.balance %} {{ trans('firefly.chart_account_in_period', { - balance: formatAmountBySymbol(balance, currency.symbol, currency.decimal_places, true), + balance: formatAmountBySymbol(balances.balance, currency.symbol, currency.decimal_places, true), name: account.name|escape, start: start.isoFormat(monthAndDayFormat), end: end.isoFormat(monthAndDayFormat) })|raw }} + {% elseif balances.native_balance %} + {{ trans('firefly.chart_account_in_period', { + balance: formatAmountBySymbol(balances.native_balance, defaultCurrency.symbol, defaultCurrency.decimal_places, true), + name: account.name|escape, start: start.isoFormat(monthAndDayFormat), end: end.isoFormat(monthAndDayFormat) })|raw }} + {% endif %}

@@ -140,7 +146,12 @@

{{ 'transactions'|_ }} - ({{ formatAmountBySymbol(balance, currency.symbol, currency.decimal_places, true)|raw }})

+ {% if balances.balance %} + ({{ formatAmountBySymbol(balances.balance, currency.symbol, currency.decimal_places, true)|raw }}) + {% elseif balances.native_balance %} + ({{ formatAmountBySymbol(balances.native_balance, defaultCurrency.symbol, defaultCurrency.decimal_places, true)|raw }}) + {% endif %} +
{% if account.accountType.type == 'Asset account' %} diff --git a/resources/views/admin/index.twig b/resources/views/admin/index.twig index 2d5b3e44b1..c418c0f273 100644 --- a/resources/views/admin/index.twig +++ b/resources/views/admin/index.twig @@ -17,6 +17,7 @@
  • {{ 'journal_link_configuration'|_ }}
  • {{ 'update_check_title'|_ }}
  • +
  • {{ 'settings_notifications'|_ }}
  • @@ -30,32 +31,6 @@
    -
    - -
    -
    -

    {{ 'admin_notifications'|_ }}

    -
    -
    -

    - {{ 'admin_notifications_expl'|_ }} -

    - {% for notification, value in notifications %} -
    - -
    - {% endfor %} - {{ ExpandedForm.text('slackUrl', slackUrl, {'label' : 'slack_url_label'|_}) }} -
    - -
    -
    diff --git a/resources/views/admin/notifications/index.twig b/resources/views/admin/notifications/index.twig new file mode 100644 index 0000000000..d569f22c49 --- /dev/null +++ b/resources/views/admin/notifications/index.twig @@ -0,0 +1,87 @@ +{% extends './layout/default' %} + +{% block breadcrumbs %} + {{ Breadcrumbs.render }} +{% endblock %} +{% block content %} +
    +
    +
    + +
    +
    +

    {{ 'notification_settings'|_ }}

    +
    +
    +

    + {{ trans('firefly.owner_notifications_expl') }} +

    + {% for notification, value in notifications %} +
    + +
    + {% endfor %} +

    {{ 'channel_settings'|_ }}

    + {{ ExpandedForm.text('slack_webhook_url', slackUrl, {'label' : 'slack_url_label'|_, helpText: trans('firefly.slack_discord_double')}) }} + + {{ ExpandedForm.text('pushover_app_token', pushoverAppToken, {}) }} + {{ ExpandedForm.text('pushover_user_token', pushoverUserToken, {}) }} + + {{ ExpandedForm.text('ntfy_server', ntfyServer, {}) }} + {{ ExpandedForm.text('ntfy_topic', ntfyTopic, {}) }} + {{ ExpandedForm.checkbox('ntfy_auth','1', ntfyAuth, {}) }} + {{ ExpandedForm.text('ntfy_user', ntfyUser, {}) }} + {{ ExpandedForm.passwordWithValue('ntfy_pass', ntfyPass, {}) }} +
    + +
    +
    +
    +
    +
    + +
    +
    +

    {{ 'available_channels_title'|_ }}

    +
    +
    +

    + {{ 'available_channels_expl'|_ }} +

    +
      + {% for name,info in channels %} +
    • + {% if true == info.enabled and true == forcedAvailability[name] %} + ☑️ {{ trans('firefly.notification_channel_name_'~name) }} + {% if 0 == info.ui_configurable %}({{ 'configure_channel_in_env'|_ }}) {% endif %} + {% endif %} + {% if false == info.enabled or false == forcedAvailability[name] %} + ⚠️ {{ trans('firefly.notification_channel_name_'~name) }} ({{ 'channel_not_available'|_ }}) + {% endif %} +
    • + {% endfor %} +
    +
    + +
    +
    +
    +
    + +{% endblock %} diff --git a/resources/views/bills/create.twig b/resources/views/bills/create.twig index 00c823f217..9099d62bca 100644 --- a/resources/views/bills/create.twig +++ b/resources/views/bills/create.twig @@ -6,7 +6,7 @@ {% block content %} -
    +
    diff --git a/resources/views/bills/delete.twig b/resources/views/bills/delete.twig index c5b4dcb034..b0a59c8079 100644 --- a/resources/views/bills/delete.twig +++ b/resources/views/bills/delete.twig @@ -6,7 +6,7 @@ {% block content %} - +
    diff --git a/resources/views/bills/edit.twig b/resources/views/bills/edit.twig index e2e734d1fb..ccedf4b405 100644 --- a/resources/views/bills/edit.twig +++ b/resources/views/bills/edit.twig @@ -6,7 +6,7 @@ {% block content %} - diff --git a/resources/views/bills/index.twig b/resources/views/bills/index.twig index a4bdaaadfd..49a2905d5d 100644 --- a/resources/views/bills/index.twig +++ b/resources/views/bills/index.twig @@ -6,7 +6,7 @@ {% block content %} {% if total == 0 %} - {% include 'partials.empty' with {objectType: 'default', type: 'bills',route: route('bills.create')} %} + {% include 'partials.empty' with {objectType: 'default', type: 'bills',route: route('subscriptions.create')} %} {% else %}
    diff --git a/resources/views/bills/show.twig b/resources/views/bills/show.twig index 7edc47d58d..87a8428b65 100644 --- a/resources/views/bills/show.twig +++ b/resources/views/bills/show.twig @@ -16,8 +16,8 @@
    @@ -27,7 +27,15 @@ @@ -58,7 +66,13 @@ @@ -66,7 +80,12 @@ @@ -74,8 +93,8 @@ @@ -97,7 +116,7 @@ {% endif %} @@ -125,7 +128,13 @@ data-id="{{ budget.id }}">{{ trans('firefly.available_between', {start: budget.start_date.isoFormat(monthAndDayFormat), end: budget.end_date.isoFormat(monthAndDayFormat) }) }} : {{ formatAmountBySymbol(budget.amount, budget.transaction_currency.symbol, budget.transaction_currency.decimal_places, true) }} + data-value="{{ budget.amount }}"> + {{ formatAmountBySymbol(budget.amount, budget.transaction_currency.symbol, budget.transaction_currency.decimal_places, true) }} + {% if(convertToNative and 0 != budget.native_amount) %} + ({{ formatAmountBySymbol(budget.native_amount, defaultCurrency.symbol, defaultCurrency.decimal_places, true) }}) + {% endif %} + + @@ -212,6 +221,7 @@ + {# START OF BUDGET ROW #} {% for budget in budgets %} {% endfor %} + {# END OF BUDGET ROW #}
    - {{ trans('firefly.match_between_amounts', {low: formatAmountByCurrency(object.data.currency,object.data.amount_min), high: formatAmountByCurrency(object.data.currency,object.data.amount_max) })|raw }} + {% set lowAmount = formatAmountByCurrency(object.data.currency,object.data.amount_min) %} + {% set highAmount = formatAmountByCurrency(object.data.currency,object.data.amount_max) %} + {% if(0 != object.data.native_amount_min) %} + {% set lowAmount = lowAmount ~ ' (' ~ formatAmountByCode(object.data.native_amount_min, defaultCurrency.code) ~ ')' %} + {% endif %} + {% if(0 != object.data.native_amount_max) %} + {% set highAmount = highAmount ~ ' (' ~ formatAmountByCode(object.data.native_amount_max, defaultCurrency.code) ~ ')' %} + {% endif %} + {{ trans('firefly.match_between_amounts', {low: lowAmount, high: highAmount })|raw }} {{ 'repeats'|_ }} {{ trans('firefly.repeat_freq_' ~object.data.repeat_freq) }}. {{ trans('firefly.average_bill_amount_year', {year: year}) }} {% for avg in yearAverage %} - {{ formatAmountBySymbol(avg.avg, avg.currency_symbol, avg.currency_decimal_places, true) }}
    + {{ formatAmountBySymbol(avg.avg, avg.currency_symbol, avg.currency_decimal_places, true) }} + {% if convertToNative and 0 != avg.native_avg %} + ({{ formatAmountBySymbol(avg.native_avg, + defaultCurrency.symbol, defaultCurrency.decimal_places, true) }}) + {% endif %} +
    + {% endfor %}
    {{ 'average_bill_amount_overall'|_ }} {% for avg in overallAverage %} - {{ formatAmountBySymbol(avg.avg, avg.currency_symbol, avg.currency_decimal_places, true) }}
    + {{ formatAmountBySymbol(avg.avg, avg.currency_symbol, avg.currency_decimal_places, true) }} + {% if convertToNative and 0 != avg.native_avg %} + ({{ formatAmountBySymbol(avg.native_avg, + defaultCurrency.symbol, defaultCurrency.decimal_places, true) }}) + {% endif %} +
    {% endfor %}
    @@ -444,6 +460,8 @@ var createBudgetLimitUrl = "{{ route('budget-limits.create', ['REPLACEME', start.format('Y-m-d'), end.format('Y-m-d')]) }}"; var storeBudgetLimitUrl = "{{ route('budget-limits.store') }}"; var updateBudgetLimitUrl = "{{ route('budget-limits.update', ['REPLACEME']) }}"; + var showBudgetLimitUrl = "{{ route('budget-limits.show', ['REPLACEME']) }}"; + var editBudgetLimitUrl = "{{ route('budget-limits.edit', ['REPLACEME']) }}"; var deleteBudgetLimitUrl = "{{ route('budget-limits.delete', ['REPLACEME']) }}"; var totalBudgetedUrl = "{{ route('json.budget.total-budgeted', ['REPLACEME', start.format('Y-m-d'), end.format('Y-m-d')]) }}"; diff --git a/resources/views/budgets/show.twig b/resources/views/budgets/show.twig index ca1dbd4995..d0ec49a61f 100644 --- a/resources/views/budgets/show.twig +++ b/resources/views/budgets/show.twig @@ -158,12 +158,19 @@ {{ 'amount'|_ }} {{ formatAmountBySymbol(limit.amount, limit.transactionCurrency.symbol, limit.transactionCurrency.decimal_places) }} + {% if convertToNative and 0 != limit.native_amount %} + ({{ formatAmountBySymbol(limit.native_amount, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {{ 'spent'|_ }} - {{ formatAmountBySymbol(limit.spent, limit.transactionCurrency.symbol, limit.transactionCurrency.decimal_places) }} + {% if convertToNative %} + {{ formatAmountBySymbol(limit.spent, defaultCurrency.symbol, defaultCurrency.decimal_places) }} + {% else %} + {{ formatAmountBySymbol(limit.spent, limit.transactionCurrency.symbol, limit.transactionCurrency.decimal_places) }} + {% endif %} {% if limit.spent > 0 %} diff --git a/resources/views/currencies/index.twig b/resources/views/currencies/index.twig index 93b30071d9..9da52a312b 100644 --- a/resources/views/currencies/index.twig +++ b/resources/views/currencies/index.twig @@ -13,11 +13,10 @@ {{ 'create_currency'|_ }}
    -

    +

    {{ 'currencies_intro'|_ }} -

    -

    {{ 'currencies_default_disabled'|_ }} + {{ 'currencies_switch_default'|_ }}

    {% if currencies|length > 0 %}
    diff --git a/resources/views/emails/invitation-created.blade.php b/resources/views/emails/invitation-created.blade.php index 17aff3684d..020129c6b7 100644 --- a/resources/views/emails/invitation-created.blade.php +++ b/resources/views/emails/invitation-created.blade.php @@ -1,3 +1,9 @@ @component('mail::message') {{ trans('email.invitation_created_body', ['email' => $email,'invitee' => $invitee]) }} + +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/new-ip.blade.php b/resources/views/emails/new-ip.blade.php index 446f1c7ae8..35f4454b08 100644 --- a/resources/views/emails/new-ip.blade.php +++ b/resources/views/emails/new-ip.blade.php @@ -1,13 +1,11 @@ @component('mail::message') {{ trans('email.new_ip_body') }} -{{ trans('email.ip_address') }}: {{ $ipAddress }} - -@if('' !== $host) -{{ trans('email.host_name') }}: {{ $host }} -@endif - -{{ trans('email.date_time') }}: {{ $time }} - {{ trans('email.new_ip_warning') }} + +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/oauth-client-created.blade.php b/resources/views/emails/oauth-client-created.blade.php index 4041c8495e..b83633a72d 100644 --- a/resources/views/emails/oauth-client-created.blade.php +++ b/resources/views/emails/oauth-client-created.blade.php @@ -5,4 +5,9 @@ {{ trans('email.oauth_created_undo', ['url' => route('profile.index')] ) }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/owner/unknown-user.blade.php b/resources/views/emails/owner/unknown-user.blade.php new file mode 100644 index 0000000000..02ede93c41 --- /dev/null +++ b/resources/views/emails/owner/unknown-user.blade.php @@ -0,0 +1,9 @@ +@component('mail::message') +{{ trans('email.unknown_user_body', ['address' => $address]) }} + +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + +@endcomponent diff --git a/resources/views/emails/password.blade.php b/resources/views/emails/password.blade.php index 926ed49e32..1d56bb4de9 100644 --- a/resources/views/emails/password.blade.php +++ b/resources/views/emails/password.blade.php @@ -5,4 +5,9 @@ {{ $url }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/registered.blade.php b/resources/views/emails/registered.blade.php index e4a4032838..b9563a99ce 100644 --- a/resources/views/emails/registered.blade.php +++ b/resources/views/emails/registered.blade.php @@ -1,9 +1,7 @@ @component('mail::message') {{ trans('email.registered_welcome') }} -* {{ trans('email.registered_pw', ['address' => $address]) }} -* {{ trans('email.registered_help') }} -* {{ trans('email.registered_doc_text') }} +{{ trans('email.registered_pw', ['address' => $address]) }} {{ trans('email.registered_help') }} {{ trans('email.registered_closing') }} diff --git a/resources/views/emails/security/disabled-mfa.blade.php b/resources/views/emails/security/disabled-mfa.blade.php index 62f17f6928..28e69299f5 100644 --- a/resources/views/emails/security/disabled-mfa.blade.php +++ b/resources/views/emails/security/disabled-mfa.blade.php @@ -3,4 +3,9 @@ {{ trans('email.disabled_mfa_warning') }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/security/enabled-mfa.blade.php b/resources/views/emails/security/enabled-mfa.blade.php index a6cbb083aa..3045418c6c 100644 --- a/resources/views/emails/security/enabled-mfa.blade.php +++ b/resources/views/emails/security/enabled-mfa.blade.php @@ -3,4 +3,9 @@ {{ trans('email.enabled_mfa_warning') }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/security/failed-login.blade.php b/resources/views/emails/security/failed-login.blade.php new file mode 100644 index 0000000000..122e533fa4 --- /dev/null +++ b/resources/views/emails/security/failed-login.blade.php @@ -0,0 +1,11 @@ +@component('mail::message') +{{ trans('email.failed_login_body', ['email' => $user->email]) }} + +{{ trans('email.failed_login_warning') }} + +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + +@endcomponent diff --git a/resources/views/emails/security/few-backup-codes.blade.php b/resources/views/emails/security/few-backup-codes.blade.php index 84292fe07e..3596df2fb6 100644 --- a/resources/views/emails/security/few-backup-codes.blade.php +++ b/resources/views/emails/security/few-backup-codes.blade.php @@ -3,4 +3,9 @@ {{ trans('email.few_backup_codes_warning') }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/security/many-failed-attempts.blade.php b/resources/views/emails/security/many-failed-attempts.blade.php index 709fe868eb..0eeb074341 100644 --- a/resources/views/emails/security/many-failed-attempts.blade.php +++ b/resources/views/emails/security/many-failed-attempts.blade.php @@ -3,4 +3,9 @@ {{ trans('email.mfa_many_failed_attempts_warning') }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/security/new-backup-codes.blade.php b/resources/views/emails/security/new-backup-codes.blade.php index dc151a6ee4..8ef185d570 100644 --- a/resources/views/emails/security/new-backup-codes.blade.php +++ b/resources/views/emails/security/new-backup-codes.blade.php @@ -3,4 +3,9 @@ {{ trans('email.new_backup_codes_warning') }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/security/no-backup-codes.blade.php b/resources/views/emails/security/no-backup-codes.blade.php index ca785c5b3a..e8c4e3b468 100644 --- a/resources/views/emails/security/no-backup-codes.blade.php +++ b/resources/views/emails/security/no-backup-codes.blade.php @@ -3,4 +3,9 @@ {{ trans('email.no_backup_codes_warning') }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/security/used-backup-code.blade.php b/resources/views/emails/security/used-backup-code.blade.php index 9631cd3345..5367d38c49 100644 --- a/resources/views/emails/security/used-backup-code.blade.php +++ b/resources/views/emails/security/used-backup-code.blade.php @@ -3,4 +3,9 @@ {{ trans('email.used_backup_code_warning') }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + @endcomponent diff --git a/resources/views/emails/token-created.blade.php b/resources/views/emails/token-created.blade.php index c42ce35a4c..7d113cddca 100644 --- a/resources/views/emails/token-created.blade.php +++ b/resources/views/emails/token-created.blade.php @@ -4,4 +4,11 @@ {{ trans('email.access_token_created_explanation') }} {{ trans('email.access_token_created_revoke', ['url' => route('profile.index')]) }} + +- {{ trans('email.ip_address') }}: {{ $ip }} +- {{ trans('email.host_name') }}: {{ $host }} +- {{ trans('email.date_time') }}: {{ $time }} +- {{ trans('email.user_agent') }}: {{ $userAgent }} + + @endcomponent diff --git a/resources/views/exchange-rates/index.twig b/resources/views/exchange-rates/index.twig new file mode 100644 index 0000000000..8d51135202 --- /dev/null +++ b/resources/views/exchange-rates/index.twig @@ -0,0 +1,12 @@ +{% set VUE_SCRIPT_NAME = 'exchange-rates/index' %} +{% extends './layout/default' %} +{% block breadcrumbs %} + {{ Breadcrumbs.render(Route.getCurrentRoute.getName, objectType) }} +{% endblock %} + +{% block content %} +
    +{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/resources/views/exchange-rates/rates.twig b/resources/views/exchange-rates/rates.twig new file mode 100644 index 0000000000..0155a2962a --- /dev/null +++ b/resources/views/exchange-rates/rates.twig @@ -0,0 +1,13 @@ +{% set VUE_SCRIPT_NAME = 'exchange-rates/rates' %} +{% extends './layout/default' %} +{% block breadcrumbs %} + {{ Breadcrumbs.render(Route.getCurrentRoute.getName, from, to) }} +{% endblock %} + +{% block content %} +
    + +{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/resources/views/form/multi-select.twig b/resources/views/form/multi-select.twig new file mode 100644 index 0000000000..acdfc38695 --- /dev/null +++ b/resources/views/form/multi-select.twig @@ -0,0 +1,10 @@ +
    + + +
    + {{ Html.multiselect(name~"[]", list, selected).id(options.id).class('form-control').attribute('autocomplete','off').attribute('spellcheck','false').attribute('placeholder', options.placeholder) }} + {% include 'form.help' %} + {% include 'form.feedback' %} + +
    +
    diff --git a/resources/views/index.twig b/resources/views/index.twig index e9334c9370..ec0655f809 100644 --- a/resources/views/index.twig +++ b/resources/views/index.twig @@ -132,7 +132,7 @@ {# BILLS #}
    diff --git a/resources/views/list/accounts.twig b/resources/views/list/accounts.twig index 75624856aa..7dfe78ed67 100644 --- a/resources/views/list/accounts.twig +++ b/resources/views/list/accounts.twig @@ -66,14 +66,26 @@ {% if objectType != 'liabilities' %} - {{ formatAmountByAccount(account, account.endBalance) }} + {% for key, balance in account.endBalances %} + + {% if 'balance' == key %} + {{ formatAmountBySymbol(balance, account.currency.symbol, account.currency.decimal_places) }} + {% elseif 'native_balance' == key %} + {{ formatAmountBySymbol(balance, defaultCurrency.symbol, defaultCurrency.decimal_places) }} + {% else %} + ({{ formatAmountByCode(balance, key) }}) + {% endif %} + + {% endfor %} {% endif %} {% if objectType == 'liabilities' %} {% if '-' != account.current_debt %} - {{ formatAmountByAccount(account, account.current_debt, false) }} + + {{ formatAmountBySymbol(account.current_debt, account.currency.symbol, account.currency.decimal_places, false) }} + {% endif %} {% endif %} @@ -99,7 +111,16 @@ {% endif %} - {{ formatAmountByAccount(account, account.difference) }} + {% for key, balance in account.differences %} + + {% if 'balance' == key or 'native_balance' == key %} + {{ formatAmountBySymbol(balance, account.currency.symbol, account.currency.currency.decimal_places) }} + + {% else %} + ({{ formatAmountByCode(balance, key) }}) + {% endif %} + + {% endfor %} diff --git a/resources/views/list/bills.twig b/resources/views/list/bills.twig index b32ca256cd..d6200e830e 100644 --- a/resources/views/list/bills.twig +++ b/resources/views/list/bills.twig @@ -31,17 +31,17 @@ -
    {% if not entry.active %} {% endif %} - {{ entry.name }} + {{ entry.name }} {# count attachments #} {% if entry.attachments.count() > 0 %} @@ -67,6 +67,10 @@ title="{{ formatAmountBySymbol(entry.amount_min, entry.currency_symbol, entry.currency_decimal_places, false)|escape }} -- {{ formatAmountBySymbol(entry.amount_max, entry.currency_symbol, entry.currency_decimal_places, false)|escape }}" > ~ {{ formatAmountBySymbol((entry.amount_max + entry.amount_min)/2, entry.currency_symbol, entry.currency_decimal_places) }} + + {% if '0' != entry.native_amount_max %} + (~ {{ formatAmountBySymbol((entry.native_amount_max + entry.native_amount_min)/2, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} diff --git a/resources/views/list/groups-tiny.twig b/resources/views/list/groups-tiny.twig index 3ea7672958..a606dc66e1 100644 --- a/resources/views/list/groups-tiny.twig +++ b/resources/views/list/groups-tiny.twig @@ -12,6 +12,10 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} + {% elseif transaction.transaction_type_type == 'Transfer' %} {# transfer away: #} @@ -20,13 +24,18 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places, false) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} {% else %} {{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_decimal_places, false) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places, false) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} {% endif %} - {# transfer to #} {% elseif transaction.transaction_type_type == 'Opening balance' %} @@ -35,11 +44,17 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} {% else %} {{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} {% endif %} {% elseif transaction.transaction_type_type == 'Reconciliation' %} {% if transaction.source_account_type == 'Reconciliation account' %} @@ -47,17 +62,26 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} {% else %} {{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} {% endif %} {% else %} {{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and null != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount, defaultCurrency.symbol, foreign_currency_.decimal_places) }}) + {% endif %} {% endif %} diff --git a/resources/views/list/groups.twig b/resources/views/list/groups.twig index cd210e0a07..3830e007c9 100644 --- a/resources/views/list/groups.twig +++ b/resources/views/list/groups.twig @@ -59,13 +59,14 @@ {% for sum in group.sums %} {% if group.transaction_type == 'Deposit' %} - {{ formatAmountBySymbol(sum.amount*-1, sum.currency_symbol, sum.currency_decimal_places) }}{% if loop.index != group.sums|length %},{% endif %} + {{ formatAmountBySymbol(sum.amount*-1, sum.currency_symbol, sum.currency_decimal_places) }}(TODO NATIVE group 1){% if loop.index != group.sums|length %},{% endif %} + {% elseif group.transaction_type == 'Transfer' %} - {{ formatAmountBySymbol(sum.amount*-1, sum.currency_symbol, sum.currency_decimal_places, false) }}{% if loop.index != group.sums|length %},{% endif %} + {{ formatAmountBySymbol(sum.amount*-1, sum.currency_symbol, sum.currency_decimal_places, false) }}(TODO group 2 ){% if loop.index != group.sums|length %},{% endif %} {% else %} - {{ formatAmountBySymbol(sum.amount, sum.currency_symbol, sum.currency_decimal_places) }}{% if loop.index != group.sums|length %},{% endif %} + {{ formatAmountBySymbol(sum.amount, sum.currency_symbol, sum.currency_decimal_places) }}(TODO NATIVE group 3){% if loop.index != group.sums|length %},{% endif %} {% endif %} {% endfor %} @@ -153,6 +154,9 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {# transfer #} {% elseif transaction.transaction_type_type == 'Transfer' %} @@ -160,6 +164,9 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places, false) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {# opening balance #} {% elseif transaction.transaction_type_type == 'Opening balance' %} @@ -168,11 +175,17 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {% else %} {{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {% endif %} {# reconciliation #} {% elseif transaction.transaction_type_type == 'Reconciliation' %} @@ -181,11 +194,17 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {% else %} {{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {% endif %} {# liability credit #} {% elseif transaction.transaction_type_type == 'Liability credit' %} @@ -194,11 +213,17 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {% else %} {{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_decimal_places) }} {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount*-1, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {% endif %} @@ -208,6 +233,9 @@ {% if null != transaction.foreign_amount %} ({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_decimal_places) }}) {% endif %} + {% if convertToNative and 0 != transaction.native_amount %} + ({{ formatAmountBySymbol(transaction.native_amount, defaultCurrency.symbol, defaultCurrency.decimal_places) }}) + {% endif %} {% endif %} diff --git a/resources/views/list/piggy-bank-events.twig b/resources/views/list/piggy-bank-events.twig index eb916e9161..bc7eca413a 100644 --- a/resources/views/list/piggy-bank-events.twig +++ b/resources/views/list/piggy-bank-events.twig @@ -25,9 +25,9 @@ {% if event.amount < 0 %} - {{ trans('firefly.removed_amount', {amount: formatAmountByAccount(event.piggyBank.account, event.amount, false)})|raw }} + {{ trans('firefly.removed_amount', {amount: formatAmountBySymbol(event.amount,event.piggyBank.transactionCurrency.symbol, false)})|raw }} {% else %} - {{ trans('firefly.added_amount', {amount: formatAmountByAccount(event.piggyBank.account, event.amount, false)})|raw }} + {{ trans('firefly.added_amount', {amount: formatAmountBySymbol(event.amount, event.piggyBank.transactionCurrency.symbol, false)})|raw }} {% endif %} diff --git a/resources/views/object-groups/index.twig b/resources/views/object-groups/index.twig index b6dad95f11..80402c7423 100644 --- a/resources/views/object-groups/index.twig +++ b/resources/views/object-groups/index.twig @@ -46,7 +46,7 @@ - {{ 'piggy_bank'|_ }}: {{ piggyBank.name }}
    {% endfor %} {% for bill in objectGroup.bills %} - - {{ 'bill'|_ }}: {{ bill.name }}
    + - {{ 'bill'|_ }}: {{ bill.name }}
    {% endfor %} diff --git a/resources/views/partials/boxes.twig b/resources/views/partials/boxes.twig index 29acbb518c..27385c5697 100644 --- a/resources/views/partials/boxes.twig +++ b/resources/views/partials/boxes.twig @@ -25,7 +25,7 @@
    - {{ 'bills_to_pay'|_ }} + {{ 'bills_to_pay'|_ }}
    diff --git a/resources/views/partials/control-bar.twig b/resources/views/partials/control-bar.twig index 0ca560f745..e587a1a453 100644 --- a/resources/views/partials/control-bar.twig +++ b/resources/views/partials/control-bar.twig @@ -97,7 +97,7 @@
  • - + diff --git a/resources/views/partials/menu-sidebar.twig b/resources/views/partials/menu-sidebar.twig index f3487f6e8c..32fff07238 100644 --- a/resources/views/partials/menu-sidebar.twig +++ b/resources/views/partials/menu-sidebar.twig @@ -16,7 +16,7 @@
  • - + {{ 'bills'|_ }} @@ -189,7 +189,7 @@
  • {% endif %} -
  • @@ -219,6 +219,14 @@ {{ 'currencies'|_ }}
  • + {% if config('cer.enabled') %} +
  • + + + {{ 'menu_exchange_rates_index'|_ }} + +
  • + {% endif %} {% if hasRole('owner') %}
  • diff --git a/resources/views/piggy-banks/add-mobile.twig b/resources/views/piggy-banks/add-mobile.twig index c66c09078d..79aeec0cd2 100644 --- a/resources/views/piggy-banks/add-mobile.twig +++ b/resources/views/piggy-banks/add-mobile.twig @@ -14,16 +14,16 @@

    {{ trans('firefly.add_money_to_piggy', {name: piggyBank.name}) }}

  • - {% if maxAmount > 0 %} -

    - {{ 'max_amount_add'|_ }}: {{ formatAmountByCurrency(currency,maxAmount) }}. -

    + {% if total > 0 %} -
    -
    {{ currency.symbol|raw }}
    - -
    + + {% for account in accounts %} + {{ account.account.name }} ({{ 'max_amount_add'|_ }}: {{ formatAmountByCurrency(piggyBank.transactionCurrency, account.max_amount) }}) +
    +
    {{ piggyBank.transactionCurrency.symbol|raw }}
    + +
    + {% endfor %}

     

    diff --git a/resources/views/piggy-banks/add.twig b/resources/views/piggy-banks/add.twig index 9c4da8d832..3f07365ed8 100644 --- a/resources/views/piggy-banks/add.twig +++ b/resources/views/piggy-banks/add.twig @@ -5,19 +5,17 @@
    - {% if maxAmount > 0 %} + {% if total > 0 %}
    -

    - {{ 'max_amount_remove'|_ }}: {{ formatAmountByCurrency(currency, repetition.currentamount) }}. -

    + {% for account in accounts %} +

    + {{ account.account.name }}: {{ 'max_amount_remove'|_ }}: {{ formatAmountByCurrency(piggyBank.transactionCurrency, account.saved_so_far) }}. +

    +
    +
    {{ piggyBank.transactionCurrency.symbol|raw }}
    + +
    + {% endfor %} -
    -
    {{ currency.symbol|raw }}
    - -

     

    diff --git a/resources/views/piggy-banks/remove.twig b/resources/views/piggy-banks/remove.twig index 7527b7d798..e4b381aa8d 100644 --- a/resources/views/piggy-banks/remove.twig +++ b/resources/views/piggy-banks/remove.twig @@ -10,15 +10,16 @@
    + {% endfor %}