From 4ba1c5bcfcb444a9d078d23122a13f5df56b95f6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 26 Nov 2024 18:04:32 +0100 Subject: [PATCH 001/167] New set of PHP 8.4 files --- app/Http/Controllers/Controller.php | 3 + app/Http/Controllers/HomeController.php | 1 + app/Models/AccountType.php | 28 ++-- app/Models/AutoBudget.php | 6 +- app/Models/RecurrenceRepetition.php | 8 +- app/Models/TransactionType.php | 14 +- composer.json | 2 +- composer.lock | 206 ++++++++++++------------ 8 files changed, 137 insertions(+), 131 deletions(-) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 772020f672..c5db5b85f7 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -44,6 +44,9 @@ 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 string $monthFormat; diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 72fc6ba722..0370161be8 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -43,6 +43,7 @@ use Illuminate\Support\Facades\Log; */ class HomeController extends Controller { + /** * HomeController constructor. */ diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index 013b7269bd..6517f16dee 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -35,46 +35,46 @@ class AccountType extends Model { use ReturnsIntegerIdTrait; - /** @deprecated */ + #[\Deprecated] public const string ASSET = 'Asset account'; - /** @deprecated */ + #[\Deprecated] public const string BENEFICIARY = 'Beneficiary account'; - /** @deprecated */ + #[\Deprecated] public const string CASH = 'Cash account'; - /** @deprecated */ + #[\Deprecated] public const string CREDITCARD = 'Credit card'; - /** @deprecated */ + #[\Deprecated] public const string DEBT = 'Debt'; - /** @deprecated */ + #[\Deprecated] public const string DEFAULT = 'Default account'; - /** @deprecated */ + #[\Deprecated] public const string EXPENSE = 'Expense account'; - /** @deprecated */ + #[\Deprecated] public const string IMPORT = 'Import account'; - /** @deprecated */ + #[\Deprecated] public const string INITIAL_BALANCE = 'Initial balance account'; - /** @deprecated */ + #[\Deprecated] public const string LIABILITY_CREDIT = 'Liability credit account'; - /** @deprecated */ + #[\Deprecated] public const string LOAN = 'Loan'; - /** @deprecated */ + #[\Deprecated] public const string MORTGAGE = 'Mortgage'; - /** @deprecated */ + #[\Deprecated] public const string RECONCILIATION = 'Reconciliation account'; - /** @deprecated */ + #[\Deprecated] public const string REVENUE = 'Revenue account'; protected $casts diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index b67ca3be23..4504522edf 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -39,13 +39,13 @@ class AutoBudget extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - /** @deprecated */ + #[\Deprecated] public const int AUTO_BUDGET_ADJUSTED = 3; - /** @deprecated */ + #[\Deprecated] public const int AUTO_BUDGET_RESET = 1; - /** @deprecated */ + #[\Deprecated] public const int AUTO_BUDGET_ROLLOVER = 2; protected $fillable = ['budget_id', 'amount', 'period']; diff --git a/app/Models/RecurrenceRepetition.php b/app/Models/RecurrenceRepetition.php index f3daa1657b..5680b09c51 100644 --- a/app/Models/RecurrenceRepetition.php +++ b/app/Models/RecurrenceRepetition.php @@ -39,16 +39,16 @@ class RecurrenceRepetition extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - /** @deprecated */ + #[\Deprecated] public const int WEEKEND_DO_NOTHING = 1; - /** @deprecated */ + #[\Deprecated] public const int WEEKEND_SKIP_CREATION = 2; - /** @deprecated */ + #[\Deprecated] public const int WEEKEND_TO_FRIDAY = 3; - /** @deprecated */ + #[\Deprecated] public const int WEEKEND_TO_MONDAY = 4; protected $casts diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php index dfc466cf3a..d0b948a469 100644 --- a/app/Models/TransactionType.php +++ b/app/Models/TransactionType.php @@ -38,25 +38,25 @@ class TransactionType extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - /** @deprecated */ + #[\Deprecated] public const string DEPOSIT = 'Deposit'; - /** @deprecated */ + #[\Deprecated] public const string INVALID = 'Invalid'; - /** @deprecated */ + #[\Deprecated] public const string LIABILITY_CREDIT = 'Liability credit'; - /** @deprecated */ + #[\Deprecated] public const string OPENING_BALANCE = 'Opening balance'; - /** @deprecated */ + #[\Deprecated] public const string RECONCILIATION = 'Reconciliation'; - /** @deprecated */ + #[\Deprecated] public const string TRANSFER = 'Transfer'; - /** @deprecated */ + #[\Deprecated] public const string WITHDRAWAL = 'Withdrawal'; protected $casts diff --git a/composer.json b/composer.json index b28b135953..c85437acdc 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,7 @@ } ], "require": { - "php": ">=8.3", + "php": ">=8.4", "ext-bcmath": "*", "ext-curl": "*", "ext-fileinfo": "*", diff --git a/composer.lock b/composer.lock index 761703e201..79b33dcb29 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": "eb0a48bb5142f68837c2ca1f9b82aa0d", + "content-hash": "f813653aac7be9e344fb4ca91513df61", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1936,20 +1936,21 @@ }, { "name": "laravel-json-api/core", - "version": "v4.2.0", + "version": "v4.3.0", "source": { "type": "git", "url": "https://github.com/laravel-json-api/core.git", - "reference": "5a3d1771a63e222d902ccd7d57c9323c8aac8d32" + "reference": "37c4734dbd5c9fd7f2d5cca490553a0a664b2a69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/core/zipball/5a3d1771a63e222d902ccd7d57c9323c8aac8d32", - "reference": "5a3d1771a63e222d902ccd7d57c9323c8aac8d32", + "url": "https://api.github.com/repos/laravel-json-api/core/zipball/37c4734dbd5c9fd7f2d5cca490553a0a664b2a69", + "reference": "37c4734dbd5c9fd7f2d5cca490553a0a664b2a69", "shasum": "" }, "require": { "ext-json": "*", + "illuminate/auth": "^11.33", "illuminate/contracts": "^11.0", "illuminate/http": "^11.0", "illuminate/support": "^11.0", @@ -1994,9 +1995,9 @@ ], "support": { "issues": "https://github.com/laravel-json-api/core/issues", - "source": "https://github.com/laravel-json-api/core/tree/v4.2.0" + "source": "https://github.com/laravel-json-api/core/tree/v4.3.0" }, - "time": "2024-08-21T19:29:20+00:00" + "time": "2024-11-26T16:37:40+00:00" }, { "name": "laravel-json-api/eloquent", @@ -2547,23 +2548,23 @@ }, { "name": "laravel/framework", - "version": "v11.33.2", + "version": "v11.34.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "6b9832751cf8eed18b3c73df5071f78f0682aa5d" + "reference": "858184e8def3f20f588f9ab88355003750845a6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/6b9832751cf8eed18b3c73df5071f78f0682aa5d", - "reference": "6b9832751cf8eed18b3c73df5071f78f0682aa5d", + "url": "https://api.github.com/repos/laravel/framework/zipball/858184e8def3f20f588f9ab88355003750845a6c", + "reference": "858184e8def3f20f588f9ab88355003750845a6c", "shasum": "" }, "require": { "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", - "dragonmantank/cron-expression": "^3.3.2", + "dragonmantank/cron-expression": "^3.4", "egulias/email-validator": "^3.2.1|^4.0", "ext-ctype": "*", "ext-filter": "*", @@ -2573,35 +2574,36 @@ "ext-session": "*", "ext-tokenizer": "*", "fruitcake/php-cors": "^1.3", - "guzzlehttp/guzzle": "^7.8", + "guzzlehttp/guzzle": "^7.8.2", "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", "league/commonmark": "^2.2.1", - "league/flysystem": "^3.8.0", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.72.2|^3.0", + "nesbot/carbon": "^2.72.2|^3.4", "nunomaduro/termwind": "^2.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^7.0", - "symfony/error-handler": "^7.0", - "symfony/finder": "^7.0", - "symfony/http-foundation": "^7.0", - "symfony/http-kernel": "^7.0", - "symfony/mailer": "^7.0", - "symfony/mime": "^7.0", - "symfony/polyfill-php83": "^1.28", - "symfony/process": "^7.0", - "symfony/routing": "^7.0", - "symfony/uid": "^7.0", - "symfony/var-dumper": "^7.0", + "symfony/console": "^7.0.3", + "symfony/error-handler": "^7.0.3", + "symfony/finder": "^7.0.3", + "symfony/http-foundation": "^7.0.3", + "symfony/http-kernel": "^7.0.3", + "symfony/mailer": "^7.0.3", + "symfony/mime": "^7.0.3", + "symfony/polyfill-php83": "^1.31", + "symfony/process": "^7.0.3", + "symfony/routing": "^7.0.3", + "symfony/uid": "^7.0.3", + "symfony/var-dumper": "^7.0.3", "tijsverkoyen/css-to-inline-styles": "^2.2.5", - "vlucas/phpdotenv": "^5.4.1", - "voku/portable-ascii": "^2.0" + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" }, "conflict": { "mockery/mockery": "1.6.8", @@ -2651,29 +2653,32 @@ }, "require-dev": { "ably/ably-php": "^1.0", - "aws/aws-sdk-php": "^3.235.5", + "aws/aws-sdk-php": "^3.322.9", "ext-gmp": "*", - "fakerphp/faker": "^1.23", - "league/flysystem-aws-s3-v3": "^3.0", - "league/flysystem-ftp": "^3.0", - "league/flysystem-path-prefixing": "^3.3", - "league/flysystem-read-only": "^3.3", - "league/flysystem-sftp-v3": "^3.0", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "nyholm/psr7": "^1.2", "orchestra/testbench-core": "^9.6", - "pda/pheanstalk": "^5.0", + "pda/pheanstalk": "^5.0.6", "phpstan/phpstan": "^1.11.5", - "phpunit/phpunit": "^10.5|^11.0", - "predis/predis": "^2.0.2", + "phpunit/phpunit": "^10.5.35|^11.3.6", + "predis/predis": "^2.3", "resend/resend-php": "^0.10.0", - "symfony/cache": "^7.0", - "symfony/http-client": "^7.0", - "symfony/psr-http-message-bridge": "^7.0" + "symfony/cache": "^7.0.3", + "symfony/http-client": "^7.0.3", + "symfony/psr-http-message-bridge": "^7.0.3", + "symfony/translation": "^7.0.3" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", - "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", "ext-apcu": "Required to use the APC cache driver.", "ext-fileinfo": "Required to use the Filesystem class.", @@ -2687,16 +2692,16 @@ "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", - "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", - "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", - "league/flysystem-read-only": "Required to use read-only disks (^3.3)", - "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", "mockery/mockery": "Required to use mocking (^1.6).", "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", - "predis/predis": "Required to use the predis connector (^2.0.2).", + "predis/predis": "Required to use the predis connector (^2.3).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", @@ -2752,7 +2757,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-11-19T22:47:13+00:00" + "time": "2024-11-26T15:11:52+00:00" }, { "name": "laravel/passport", @@ -2891,16 +2896,16 @@ }, { "name": "laravel/sanctum", - "version": "v4.0.4", + "version": "v4.0.5", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "819782c75aaf2b08da1765503893bd2b8023d3b3" + "reference": "fe361b9a63407a228f884eb78d7217f680b50140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/819782c75aaf2b08da1765503893bd2b8023d3b3", - "reference": "819782c75aaf2b08da1765503893bd2b8023d3b3", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fe361b9a63407a228f884eb78d7217f680b50140", + "reference": "fe361b9a63407a228f884eb78d7217f680b50140", "shasum": "" }, "require": { @@ -2951,7 +2956,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2024-11-15T14:47:23+00:00" + "time": "2024-11-26T14:36:23+00:00" }, { "name": "laravel/serializable-closure", @@ -3016,16 +3021,16 @@ }, { "name": "laravel/slack-notification-channel", - "version": "v3.4.0", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/laravel/slack-notification-channel.git", - "reference": "8ffbb9f0578956cc192bffc8d75f5b07beb35aa3" + "reference": "f43f63f1e0d22de1ded93425e4a9a5f977bfe34c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/slack-notification-channel/zipball/8ffbb9f0578956cc192bffc8d75f5b07beb35aa3", - "reference": "8ffbb9f0578956cc192bffc8d75f5b07beb35aa3", + "url": "https://api.github.com/repos/laravel/slack-notification-channel/zipball/f43f63f1e0d22de1ded93425e4a9a5f977bfe34c", + "reference": "f43f63f1e0d22de1ded93425e4a9a5f977bfe34c", "shasum": "" }, "require": { @@ -3075,22 +3080,22 @@ ], "support": { "issues": "https://github.com/laravel/slack-notification-channel/issues", - "source": "https://github.com/laravel/slack-notification-channel/tree/v3.4.0" + "source": "https://github.com/laravel/slack-notification-channel/tree/v3.4.1" }, - "time": "2024-10-24T15:06:08+00:00" + "time": "2024-11-21T15:06:30+00:00" }, { "name": "laravel/ui", - "version": "v4.5.2", + "version": "v4.6.0", "source": { "type": "git", "url": "https://github.com/laravel/ui.git", - "reference": "c75396f63268c95b053c8e4814eb70e0875e9628" + "reference": "a34609b15ae0c0512a0cf47a21695a2729cb7f93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/ui/zipball/c75396f63268c95b053c8e4814eb70e0875e9628", - "reference": "c75396f63268c95b053c8e4814eb70e0875e9628", + "url": "https://api.github.com/repos/laravel/ui/zipball/a34609b15ae0c0512a0cf47a21695a2729cb7f93", + "reference": "a34609b15ae0c0512a0cf47a21695a2729cb7f93", "shasum": "" }, "require": { @@ -3138,9 +3143,9 @@ "ui" ], "support": { - "source": "https://github.com/laravel/ui/tree/v4.5.2" + "source": "https://github.com/laravel/ui/tree/v4.6.0" }, - "time": "2024-05-08T18:07:10+00:00" + "time": "2024-11-21T15:06:41+00:00" }, { "name": "lcobucci/clock", @@ -5150,21 +5155,21 @@ }, { "name": "php-http/guzzle7-adapter", - "version": "1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-http/guzzle7-adapter.git", - "reference": "fb075a71dbfa4847cf0c2938c4e5a9c478ef8b01" + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/guzzle7-adapter/zipball/fb075a71dbfa4847cf0c2938c4e5a9c478ef8b01", - "reference": "fb075a71dbfa4847cf0c2938c4e5a9c478ef8b01", + "url": "https://api.github.com/repos/php-http/guzzle7-adapter/zipball/03a415fde709c2f25539790fecf4d9a31bc3d0eb", + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.0", - "php": "^7.2 | ^8.0", + "php": "^7.3 | ^8.0", "php-http/httplug": "^2.0", "psr/http-client": "^1.0" }, @@ -5175,14 +5180,11 @@ }, "require-dev": { "php-http/client-integration-tests": "^3.0", + "php-http/message-factory": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^8.0|^9.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.2.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Adapter\\Guzzle7\\": "src/" @@ -5206,9 +5208,9 @@ ], "support": { "issues": "https://github.com/php-http/guzzle7-adapter/issues", - "source": "https://github.com/php-http/guzzle7-adapter/tree/1.0.0" + "source": "https://github.com/php-http/guzzle7-adapter/tree/1.1.0" }, - "time": "2021-03-09T07:35:15+00:00" + "time": "2024-11-26T11:14:36+00:00" }, { "name": "php-http/httplug", @@ -10443,16 +10445,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.14.7", + "version": "v3.14.9", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "f484b8c9124de0b163da39958331098ffcd4a65e" + "reference": "2e805a6bd4e1aa83774316bb062703c65d0691ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f484b8c9124de0b163da39958331098ffcd4a65e", - "reference": "f484b8c9124de0b163da39958331098ffcd4a65e", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/2e805a6bd4e1aa83774316bb062703c65d0691ef", + "reference": "2e805a6bd4e1aa83774316bb062703c65d0691ef", "shasum": "" }, "require": { @@ -10511,7 +10513,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.7" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.9" }, "funding": [ { @@ -10523,7 +10525,7 @@ "type": "github" } ], - "time": "2024-11-14T09:12:35+00:00" + "time": "2024-11-25T14:51:20+00:00" }, { "name": "barryvdh/laravel-ide-helper", @@ -10731,16 +10733,16 @@ }, { "name": "composer/class-map-generator", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "98bbf6780e56e0fd2404fe4b82eb665a0f93b783" + "reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/98bbf6780e56e0fd2404fe4b82eb665a0f93b783", - "reference": "98bbf6780e56e0fd2404fe4b82eb665a0f93b783", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915", + "reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915", "shasum": "" }, "require": { @@ -10749,10 +10751,10 @@ "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { - "phpstan/phpstan": "^1.6", - "phpstan/phpstan-deprecation-rules": "^1", - "phpstan/phpstan-phpunit": "^1", - "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", "phpunit/phpunit": "^8", "symfony/filesystem": "^5.4 || ^6" }, @@ -10784,7 +10786,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.4.0" + "source": "https://github.com/composer/class-map-generator/tree/1.5.0" }, "funding": [ { @@ -10800,7 +10802,7 @@ "type": "tidelift" } ], - "time": "2024-10-03T18:14:00+00:00" + "time": "2024-11-25T16:11:06+00:00" }, { "name": "composer/pcre", @@ -11218,16 +11220,16 @@ }, { "name": "laravel-json-api/testing", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/laravel-json-api/testing.git", - "reference": "1ada998d2087479351e01dd22ca13a00a96b4118" + "reference": "5ec2a84e725f93b6e0f79091b92c30bec88fe639" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel-json-api/testing/zipball/1ada998d2087479351e01dd22ca13a00a96b4118", - "reference": "1ada998d2087479351e01dd22ca13a00a96b4118", + "url": "https://api.github.com/repos/laravel-json-api/testing/zipball/5ec2a84e725f93b6e0f79091b92c30bec88fe639", + "reference": "5ec2a84e725f93b6e0f79091b92c30bec88fe639", "shasum": "" }, "require": { @@ -11277,9 +11279,9 @@ ], "support": { "issues": "https://github.com/laravel-json-api/testing/issues", - "source": "https://github.com/laravel-json-api/testing/tree/v3.0.0" + "source": "https://github.com/laravel-json-api/testing/tree/v3.0.1" }, - "time": "2024-03-12T20:30:38+00:00" + "time": "2024-11-26T16:49:53+00:00" }, { "name": "maximebf/debugbar", @@ -13565,7 +13567,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3", + "php": ">=8.4", "ext-bcmath": "*", "ext-curl": "*", "ext-fileinfo": "*", From c25c0d37c5969ac1caf9bcc00232fb862a85eb6d Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 27 Nov 2024 08:08:52 +0100 Subject: [PATCH 002/167] Replace constants with enums. --- app/Models/AccountType.php | 2 +- config/firefly.php | 632 ++++++++++++++++++------------------- 2 files changed, 317 insertions(+), 317 deletions(-) diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index 6517f16dee..0ef77adcc2 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -44,7 +44,7 @@ class AccountType extends Model #[\Deprecated] public const string CASH = 'Cash account'; - #[\Deprecated] + #[\Deprecated] /** @deprecated */ public const string CREDITCARD = 'Credit card'; #[\Deprecated] diff --git a/config/firefly.php b/config/firefly.php index 1cce0a20e0..2b651de7d9 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -22,9 +22,9 @@ 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; @@ -226,14 +226,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 +324,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 +351,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,7 +430,7 @@ return [ 'transfers' => 'fa-exchange', ], - 'bindables' => [ + 'bindables' => [ // models 'account' => Account::class, 'attachment' => Attachment::class, @@ -488,7 +488,7 @@ return [ 'userGroupBill' => UserGroupBill::class, 'userGroup' => UserGroup::class, ], - 'rule-actions' => [ + 'rule-actions' => [ 'set_category' => SetCategory::class, 'clear_category' => ClearCategory::class, 'set_budget' => SetBudget::class, @@ -522,7 +522,7 @@ return [ // 'set_foreign_amount' => SetForeignAmount::class, // 'set_foreign_currency' => SetForeignCurrency::class, ], - 'context-rule-actions' => [ + 'context-rule-actions' => [ 'set_category', 'set_budget', 'add_tag', @@ -541,321 +541,321 @@ return [ 'convert_transfer', ], - 'test-triggers' => [ + 'test-triggers' => [ 'limit' => 10, 'range' => 200, ], // expected source types for each transaction type, in order of preference. - 'expected_source_types' => [ + '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, + 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' => [ + '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' => [ + '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, + 'account_to_transaction' => [ + 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], + 'source_dests' => [ + 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], + 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). - 'journal_meta_fields' => [ + 'journal_meta_fields' => [ // sepa 'sepa_cc', 'sepa_ct_op', @@ -889,28 +889,28 @@ return [ 'recurrence_count', 'recurrence_date', ], - 'webhooks' => [ + '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], - 'dynamic_creation_allowed' => [ - AccountType::EXPENSE, - AccountType::REVENUE, - AccountType::INITIAL_BALANCE, - AccountType::RECONCILIATION, - AccountType::LIABILITY_CREDIT, + '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' => [ + 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'], - 'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'], + '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'], + 'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'], // dynamic date ranges are as follows: - 'dynamic_date_ranges' => ['last7', 'last30', 'last90', 'last365', 'MTD', 'QTD', 'YTD'], + 'dynamic_date_ranges' => ['last7', 'last30', 'last90', 'last365', 'MTD', 'QTD', 'YTD'], // only used in v1 - 'allowed_sort_parameters' => ['order', 'name', 'iban'], + 'allowed_sort_parameters' => ['order', 'name', 'iban'], // preselected account lists possibilities: - 'preselected_accounts' => ['all', 'assets', 'liabilities'], + 'preselected_accounts' => ['all', 'assets', 'liabilities'], ]; From f5c56e02da25dc178f4f691907cb2e0af0403b84 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 30 Nov 2024 05:42:59 +0100 Subject: [PATCH 003/167] API allows update/set of budget limit notes. https://github.com/firefly-iii/firefly-iii/issues/5523 --- .../Models/BudgetLimit/StoreController.php | 9 +- .../Models/BudgetLimit/StoreRequest.php | 2 + .../Models/BudgetLimit/UpdateRequest.php | 9 +- app/Models/Account.php | 2 +- app/Models/BudgetLimit.php | 9 + .../Budget/BudgetLimitRepository.php | 280 ++++++++++-------- .../Budget/BudgetLimitRepositoryInterface.php | 3 + app/Transformers/BudgetLimitTransformer.php | 17 +- 8 files changed, 187 insertions(+), 144 deletions(-) diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php index 6f899b19ce..6f4cd02427 100644 --- a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php +++ b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php @@ -69,16 +69,17 @@ 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); - $manager = $this->getManager(); + $budgetLimit = $this->blRepository->store($data); + $manager = $this->getManager(); /** @var BudgetLimitTransformer $transformer */ - $transformer = app(BudgetLimitTransformer::class); + $transformer = app(BudgetLimitTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($budgetLimit, $transformer, 'budget_limits'); + $resource = new Item($budgetLimit, $transformer, 'budget_limits'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } 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..08f2a160be 100644 --- a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php @@ -51,8 +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 +71,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 +89,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/Models/Account.php b/app/Models/Account.php index 39b5e1f8ed..fe523e2fe4 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -144,7 +144,7 @@ class Account extends Model } /** - * Get all of the notes. + * Get all the notes. */ public function notes(): MorphMany { diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index 22f3c0de2d..1f20c69482 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; /** @@ -90,6 +91,14 @@ class BudgetLimit extends Model return $this->belongsTo(TransactionCurrency::class); } + /** + * Get all the notes. + */ + public function notes(): MorphMany + { + return $this->morphMany(Note::class, 'noteable'); + } + /** * Get the amount */ diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php index 6921968dc3..f98b3da334 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; @@ -49,10 +50,10 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface */ public function budgeted(Carbon $start, Carbon $end, TransactionCurrency $currency, ?Collection $budgets = null): string { - $query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') + $query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') // same complex where query as below. - ->where( + ->where( static function (Builder $q5) use ($start, $end): void { $q5->where( static function (Builder $q1) use ($start, $end): void { @@ -62,30 +63,27 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d')); } ) - ->orWhere( - static function (Builder $q3) use ($start, $end): void { - $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d')); - $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d')); - } - ) - ; + ->orWhere( + static function (Builder $q3) use ($start, $end): void { + $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d')); + $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d')); + } + ); } ) - ->orWhere( - static function (Builder $q4) use ($start, $end): void { - // or start is before start AND end is after end. - $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d')); - $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d')); - } - ) - ; + ->orWhere( + static function (Builder $q4) use ($start, $end): void { + // or start is before start AND end is after end. + $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d')); + $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d')); + } + ); } ) - ->where('budget_limits.transaction_currency_id', $currency->id) - ->whereNull('budgets.deleted_at') - ->where('budgets.active', true) - ->where('budgets.user_id', $this->user->id) - ; + ->where('budget_limits.transaction_currency_id', $currency->id) + ->whereNull('budgets.deleted_at') + ->where('budgets.active', true) + ->where('budgets.user_id', $this->user->id); if (null !== $budgets && $budgets->count() > 0) { $query->whereIn('budget_limits.budget_id', $budgets->pluck('id')->toArray()); } @@ -137,19 +135,17 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface // both are NULL: if (null === $start && null === $end) { return BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->with(['budget']) - ->where('budgets.user_id', $this->user->id) - ->whereNull('budgets.deleted_at') - ->get(['budget_limits.*']) - ; + ->with(['budget']) + ->where('budgets.user_id', $this->user->id) + ->whereNull('budgets.deleted_at') + ->get(['budget_limits.*']); } // one of the two is NULL. if (null === $start xor null === $end) { $query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->with(['budget']) - ->whereNull('budgets.deleted_at') - ->where('budgets.user_id', $this->user->id) - ; + ->with(['budget']) + ->whereNull('budgets.deleted_at') + ->where('budgets.user_id', $this->user->id); if (null !== $end) { // end date must be before $end. $query->where('end_date', '<=', $end->format('Y-m-d 00:00:00')); @@ -164,39 +160,36 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface // neither are NULL: return BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->with(['budget']) - ->where('budgets.user_id', $this->user->id) - ->whereNull('budgets.deleted_at') - ->where( - static function (Builder $q5) use ($start, $end): void { - $q5->where( - static function (Builder $q1) use ($start, $end): void { - $q1->where( - static function (Builder $q2) use ($start, $end): void { - $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d')); - $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d')); - } - ) - ->orWhere( - static function (Builder $q3) use ($start, $end): void { - $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d')); - $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d')); - } - ) - ; - } - ) - ->orWhere( - static function (Builder $q4) use ($start, $end): void { - // or start is before start AND end is after end. - $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d')); - $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d')); - } - ) - ; - } - )->get(['budget_limits.*']) - ; + ->with(['budget']) + ->where('budgets.user_id', $this->user->id) + ->whereNull('budgets.deleted_at') + ->where( + static function (Builder $q5) use ($start, $end): void { + $q5->where( + static function (Builder $q1) use ($start, $end): void { + $q1->where( + static function (Builder $q2) use ($start, $end): void { + $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d')); + $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d')); + } + ) + ->orWhere( + static function (Builder $q3) use ($start, $end): void { + $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d')); + $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d')); + } + ); + } + ) + ->orWhere( + static function (Builder $q4) use ($start, $end): void { + // or start is before start AND end is after end. + $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d')); + $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d')); + } + ); + } + )->get(['budget_limits.*']); } public function getBudgetLimits(Budget $budget, ?Carbon $start = null, ?Carbon $end = null): Collection @@ -221,41 +214,38 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface // when both dates are set: return $budget->budgetlimits() - ->where( - static function (Builder $q5) use ($start, $end): void { // @phpstan-ignore-line - $q5->where( - static function (Builder $q1) use ($start, $end): void { - // budget limit ends within period - $q1->where( - static function (Builder $q2) use ($start, $end): void { - $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00')); - $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 23:59:59')); - } - ) - // budget limit start within period - ->orWhere( - static function (Builder $q3) use ($start, $end): void { - $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00')); - $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 23:59:59')); - } - ) - ; - } - ) - ->orWhere( - static function (Builder $q4) use ($start, $end): void { - // or start is before start AND end is after end. - $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 23:59:59')); - $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00')); - } - ) - ; - } - )->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']) - ; + ->where( + static function (Builder $q5) use ($start, $end): void { // @phpstan-ignore-line + $q5->where( + static function (Builder $q1) use ($start, $end): void { + // budget limit ends within period + $q1->where( + static function (Builder $q2) use ($start, $end): void { + $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00')); + $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 23:59:59')); + } + ) + // budget limit start within period + ->orWhere( + static function (Builder $q3) use ($start, $end): void { + $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00')); + $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 23:59:59')); + } + ); + } + ) + ->orWhere( + static function (Builder $q4) use ($start, $end): void { + // or start is before start AND end is after end. + $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 23:59:59')); + $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00')); + } + ); + } + )->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']); } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { if ($user instanceof User) { $this->user = $user; @@ -269,52 +259,57 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface { // if no currency has been provided, use the user's default currency: /** @var TransactionCurrencyFactory $factory */ - $factory = app(TransactionCurrencyFactory::class); - $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); + $factory = app(TransactionCurrencyFactory::class); + $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); if (null === $currency) { $currency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); } - $currency->enabled = true; + $currency->enabled = true; $currency->save(); // find the budget: - $budget = $this->user->budgets()->find((int) $data['budget_id']); + $budget = $this->user->budgets()->find((int) $data['budget_id']); if (null === $budget) { throw new FireflyException('200004: Budget does not exist.'); } // find limit with same date range and currency. - $limit = $budget->budgetlimits() - ->where('budget_limits.start_date', $data['start_date']->format('Y-m-d')) - ->where('budget_limits.end_date', $data['end_date']->format('Y-m-d')) - ->where('budget_limits.transaction_currency_id', $currency->id) - ->first(['budget_limits.*']) - ; + $limit = $budget->budgetlimits() + ->where('budget_limits.start_date', $data['start_date']->format('Y-m-d')) + ->where('budget_limits.end_date', $data['end_date']->format('Y-m-d')) + ->where('budget_limits.transaction_currency_id', $currency->id) + ->first(['budget_limits.*']); if (null !== $limit) { throw new FireflyException('200027: Budget limit already exists.'); } app('log')->debug('No existing budget limit, create a new one'); // or create one and return it. - $limit = new BudgetLimit(); + $limit = new BudgetLimit(); $limit->budget()->associate($budget); $limit->start_date = $data['start_date']->format('Y-m-d'); $limit->end_date = $data['end_date']->format('Y-m-d'); $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; } + public function find(Budget $budget, TransactionCurrency $currency, Carbon $start, Carbon $end): ?BudgetLimit { return $budget->budgetlimits() - ->where('transaction_currency_id', $currency->id) - ->where('start_date', $start->format('Y-m-d')) - ->where('end_date', $end->format('Y-m-d'))->first() - ; + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); } /** @@ -322,8 +317,8 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface */ public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit { - $budgetLimit->amount = array_key_exists('amount', $data) ? $data['amount'] : $budgetLimit->amount; - $budgetLimit->budget_id = array_key_exists('budget_id', $data) ? $data['budget_id'] : $budgetLimit->budget_id; + $budgetLimit->amount = array_key_exists('amount', $data) ? $data['amount'] : $budgetLimit->amount; + $budgetLimit->budget_id = array_key_exists('budget_id', $data) ? $data['budget_id'] : $budgetLimit->budget_id; if (array_key_exists('start', $data)) { $budgetLimit->start_date = $data['start']->startOfDay(); @@ -335,7 +330,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface } // if no currency has been provided, use the user's default currency: - $currency = null; + $currency = null; // update if relevant: if (array_key_exists('currency_id', $data) || array_key_exists('currency_code', $data)) { @@ -347,41 +342,43 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface if (null === $currency) { $currency = $budgetLimit->transactionCurrency ?? app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); } - $currency->enabled = true; + $currency->enabled = true; $currency->save(); $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; } public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit { // count the limits: - $limits = $budget->budgetlimits() - ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) - ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) - ->count('budget_limits.*') - ; + $limits = $budget->budgetlimits() + ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) + ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) + ->count('budget_limits.*'); app('log')->debug(sprintf('Found %d budget limits.', $limits)); // there might be a budget limit for these dates: /** @var null|BudgetLimit $limit */ - $limit = $budget->budgetlimits() - ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) - ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) - ->first(['budget_limits.*']) - ; + $limit = $budget->budgetlimits() + ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) + ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) + ->first(['budget_limits.*']); // if more than 1 limit found, delete the others: if ($limits > 1 && null !== $limit) { app('log')->debug(sprintf('Found more than 1, delete all except #%d', $limit->id)); $budget->budgetlimits() - ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) - ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) - ->where('budget_limits.id', '!=', $limit->id)->delete() - ; + ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) + ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) + ->where('budget_limits.id', '!=', $limit->id)->delete(); } // delete if amount is zero. @@ -403,7 +400,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface } app('log')->debug('No existing budget limit, create a new one'); // or create one and return it. - $limit = new BudgetLimit(); + $limit = new BudgetLimit(); $limit->budget()->associate($budget); $limit->start_date = $start->startOfDay(); $limit->start_date_tz = $start->format('e'); @@ -415,4 +412,25 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface return $limit; } + + #[\Override] public function getNoteText(BudgetLimit $budgetLimit): string + { + return (string) $budgetLimit->notes()->first()?->text; + } + + #[\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(); + } } diff --git a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php index 9bb52abb15..12cbcd9da4 100644 --- a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php @@ -48,6 +48,9 @@ interface BudgetLimitRepositoryInterface */ public function destroyAll(): void; + public function getNoteText(BudgetLimit $budgetLimit): string; + public function setNoteText(BudgetLimit $budgetLimit, string $text): void; + /** * Destroy a budget limit. */ diff --git a/app/Transformers/BudgetLimitTransformer.php b/app/Transformers/BudgetLimitTransformer.php index ad8225ceec..d75440a8fd 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; @@ -54,8 +55,10 @@ class BudgetLimitTransformer extends AbstractTransformer */ public function transform(BudgetLimit $budgetLimit): array { - $repository = app(OperationsRepository::class); + $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; @@ -78,16 +82,16 @@ class BudgetLimitTransformer extends AbstractTransformer $currencySymbol = $currency->symbol; $currencyDecimalPlaces = $currency->decimal_places; } - $amount = app('steam')->bcround($amount, $currencyDecimalPlaces); + $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,10 +99,11 @@ class BudgetLimitTransformer extends AbstractTransformer 'amount' => $amount, 'period' => $budgetLimit->period, 'spent' => $expenses[$currencyId]['sum'] ?? '0', + 'notes' => '' === $notes ? null : $notes, 'links' => [ [ 'rel' => 'self', - 'uri' => '/budgets/limits/'.$budgetLimit->id, + 'uri' => '/budgets/limits/' . $budgetLimit->id, ], ], ]; From 9ad005e31fcabca44840c927c2772a1fbcc09937 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 30 Nov 2024 06:19:21 +0100 Subject: [PATCH 004/167] Add edit button for notes https://github.com/firefly-iii/firefly-iii/issues/5523 --- .../Budget/BudgetLimitController.php | 95 ++++++++++++------- .../Controllers/Budget/IndexController.php | 1 + public/v1/js/ff/budgets/index.js | 20 ++++ resources/lang/en_US/firefly.php | 4 + .../views/budgets/budget-limits/create.twig | 4 + .../views/budgets/budget-limits/edit.twig | 28 ++++++ .../views/budgets/budget-limits/show.twig | 20 ++++ resources/views/budgets/index.twig | 11 ++- routes/web.php | 8 +- 9 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 resources/views/budgets/budget-limits/edit.twig create mode 100644 resources/views/budgets/budget-limits/show.twig diff --git a/app/Http/Controllers/Budget/BudgetLimitController.php b/app/Http/Controllers/Budget/BudgetLimitController.php index effd7ac2cb..c1dbe62020 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); @@ -84,7 +84,7 @@ class BudgetLimitController extends Controller $budgetLimits = $this->blRepository->getBudgetLimits($budget, $start, $end); // remove already budgeted currencies with the same date range - $currencies = $collection->filter( + $currencies = $collection->filter( static function (TransactionCurrency $currency) use ($budgetLimits, $start, $end) { /** @var BudgetLimit $limit */ foreach ($budgetLimits as $limit) { @@ -101,6 +101,24 @@ class BudgetLimitController extends Controller return view('budgets.budget-limits.create', compact('start', 'end', 'currencies', 'budget')); } + /** + * @return Factory|View + */ + public function show(BudgetLimit $budgetLimit) + { + $notes = $this->blRepository->getNoteText($budgetLimit); + return view('budgets.budget-limits.show', compact('budgetLimit', 'notes')); + } + + /** + * @return Factory|View + */ + public function edit(BudgetLimit $budgetLimit) + { + $notes = $this->blRepository->getNoteText($budgetLimit); + return view('budgets.budget-limits.edit', compact('budgetLimit', 'notes')); + } + /** * @return Redirector|RedirectResponse */ @@ -117,23 +135,23 @@ class BudgetLimitController extends Controller * * @throws FireflyException */ - public function store(Request $request): JsonResponse|RedirectResponse + public function store(Request $request): JsonResponse | RedirectResponse { 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.'); } - $start = Carbon::createFromFormat('Y-m-d', $request->get('start')); - $end = Carbon::createFromFormat('Y-m-d', $request->get('end')); + $start = Carbon::createFromFormat('Y-m-d', $request->get('start')); + $end = Carbon::createFromFormat('Y-m-d', $request->get('end')); if (null === $start || null === $end) { return response()->json([]); } - $amount = (string)$request->get('amount'); + $amount = (string) $request->get('amount'); $start->startOfDay(); $end->startOfDay(); @@ -143,7 +161,7 @@ class BudgetLimitController extends Controller app('log')->debug(sprintf('Start: %s, end: %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); - $limit = $this->blRepository->find($budget, $currency, $start, $end); + $limit = $this->blRepository->find($budget, $currency, $start, $end); // sanity check on amount: if (0 === bccomp($amount, '0')) { @@ -154,7 +172,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,41 +187,47 @@ 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(); + $array = $limit->toArray(); // add some extra metadata: - $spentArr = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection([$budget]), $currency); - $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); + $spentArr = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection([$budget]), $currency); + $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); // 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']); + $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: @@ -211,7 +235,7 @@ class BudgetLimitController extends Controller $budgetId = $budgetLimit->budget_id; $currency = $budgetLimit->transactionCurrency; $this->blRepository->destroyBudgetLimit($budgetLimit); - $array = [ + $array = [ 'budget_id' => $budgetId, 'left_formatted' => app('amount')->formatAnything($currency, '0'), 'left_per_day_formatted' => app('amount')->formatAnything($currency, '0'), @@ -224,29 +248,36 @@ 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(); + $array = $limit->toArray(); - $spentArr = $this->opsRepository->sumExpenses( + $spentArr = $this->opsRepository->sumExpenses( $limit->start_date, $limit->end_date, null, new Collection([$budgetLimit->budget]), $budgetLimit->transactionCurrency ); - $daysLeft = $this->activeDaysLeft($limit->start_date, $limit->end_date); - $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['left_per_day'] = 0 === $daysLeft ? bcadd($array['spent'], $array['amount']) : bcdiv(bcadd($array['spent'], $array['amount']), $array['days_left']); + $daysLeft = $this->activeDaysLeft($limit->start_date, $limit->end_date); + $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['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/IndexController.php b/app/Http/Controllers/Budget/IndexController.php index fd182e9212..5fde689d96 100644 --- a/app/Http/Controllers/Budget/IndexController.php +++ b/app/Http/Controllers/Budget/IndexController.php @@ -213,6 +213,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), 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/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index e472b7484a..aa395cd3da 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -2068,6 +2068,10 @@ return [ '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 bill :name. To remove the connection, uncheck the checkbox. Use rules to connect it to another bill.', diff --git a/resources/views/budgets/budget-limits/create.twig b/resources/views/budgets/budget-limits/create.twig index ce0c05d06a..112bd739e7 100644 --- a/resources/views/budgets/budget-limits/create.twig +++ b/resources/views/budgets/budget-limits/create.twig @@ -25,6 +25,10 @@
+
+ + {{ trans('firefly.field_supports_markdown')|raw }} +
{% endfor %} + {# END OF BUDGET ROW #} @@ -444,6 +451,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/routes/web.php b/routes/web.php index e5a9b8c84f..a702e49163 100644 --- a/routes/web.php +++ b/routes/web.php @@ -304,10 +304,10 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'budget-limits', 'as' => 'budget-limits.'], static function (): void { - Route::get('create/{budget}/{start_date}/{end_date}', ['uses' => 'Budget\BudgetLimitController@create', 'as' => 'create']) - ->where(['start_date' => DATEFORMAT]) - ->where(['end_date' => DATEFORMAT]) - ; + Route::get('create/{budget}/{start_date}/{end_date}', ['uses' => 'Budget\BudgetLimitController@create', 'as' => 'create'])->where(['start_date' => DATEFORMAT])->where(['end_date' => DATEFORMAT]); + Route::get('edit/{budgetLimit}', ['uses' => 'Budget\BudgetLimitController@edit', 'as' => 'edit']); + Route::get('show/{budgetLimit}', ['uses' => 'Budget\BudgetLimitController@show', 'as' => 'show']); + Route::post('store', ['uses' => 'Budget\BudgetLimitController@store', 'as' => 'store']); Route::post('delete/{budgetLimit}', ['uses' => 'Budget\BudgetLimitController@delete', 'as' => 'delete']); From 92190bbc54bf681891d5d739da678e1bf38311f8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 30 Nov 2024 15:57:11 +0100 Subject: [PATCH 005/167] Rename fields in piggy bank. --- .../Autocomplete/PiggyBankController.php | 2 +- .../Commands/Correction/CorrectAmounts.php | 4 +- app/Handlers/Observer/PiggyBankObserver.php | 10 +- .../Controllers/PiggyBank/IndexController.php | 1 + app/Models/PiggyBank.php | 22 ++-- app/Models/PiggyBankRepetition.php | 2 +- .../2021_08_28_073733_user_groups.php | 1 + .../2024_11_30_075826_multi_piggy.php | 103 ++++++++++++++++++ 8 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 database/migrations/2024_11_30_075826_multi_piggy.php diff --git a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php index 3373d6c708..1c29c51bec 100644 --- a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php +++ b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php @@ -105,7 +105,7 @@ class PiggyBankController extends Controller /** @var PiggyBank $piggy */ foreach ($piggies as $piggy) { $currency = $this->accountRepository->getAccountCurrency($piggy->account) ?? $defaultCurrency; - $currentAmount = $this->piggyRepository->getRepetition($piggy)->currentamount ?? '0'; + $currentAmount = $this->piggyRepository->getRepetition($piggy)->current_amount ?? '0'; $objectGroup = $piggy->objectGroups()->first(); $response[] = [ 'id' => (string)$piggy->id, diff --git a/app/Console/Commands/Correction/CorrectAmounts.php b/app/Console/Commands/Correction/CorrectAmounts.php index ad30463120..73ff3b3309 100644 --- a/app/Console/Commands/Correction/CorrectAmounts.php +++ b/app/Console/Commands/Correction/CorrectAmounts.php @@ -173,7 +173,7 @@ class CorrectAmounts extends Command /** @var PiggyBankRepetition $item */ foreach ($set as $item) { - $item->currentamount = app('steam')->positive($item->currentamount); + $item->currentamount = app('steam')->positive($item->current_amount); $item->save(); } $this->friendlyInfo(sprintf('Corrected %d piggy bank repetition amount(s).', $count)); @@ -191,7 +191,7 @@ class CorrectAmounts extends Command /** @var PiggyBank $item */ foreach ($set as $item) { - $item->targetamount = app('steam')->positive($item->targetamount); + $item->targetamount = app('steam')->positive($item->target_amount); $item->save(); } $this->friendlyInfo(sprintf('Corrected %d piggy bank amount(s).', $count)); diff --git a/app/Handlers/Observer/PiggyBankObserver.php b/app/Handlers/Observer/PiggyBankObserver.php index 5bd961f0e5..2857f7c2b7 100644 --- a/app/Handlers/Observer/PiggyBankObserver.php +++ b/app/Handlers/Observer/PiggyBankObserver.php @@ -37,11 +37,11 @@ class PiggyBankObserver 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->start_date = $piggyBank->startdate; + $repetition->start_date_tz = $piggyBank->startdate->format('e'); + $repetition->target_date = $piggyBank->targetdate; + $repetition->target_date_tz = $piggyBank->targetdate?->format('e'); + $repetition->current_amount = '0'; $repetition->save(); } diff --git a/app/Http/Controllers/PiggyBank/IndexController.php b/app/Http/Controllers/PiggyBank/IndexController.php index 72396eb20d..9c3578550e 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; diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index abef05764f..0edfa05b9e 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; @@ -46,17 +47,15 @@ class PiggyBank extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', - 'startdate' => 'date', - 'targetdate' => 'date', + 'start_date' => 'date', + 'target_date' => 'date', 'order' => 'int', 'active' => 'boolean', 'encrypted' => 'boolean', - 'targetamount' => 'string', + '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', 'account_id', 'order', 'target_amount', 'start_date', 'start_date_tz', 'target_date', 'target_date_tz', 'active']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -110,6 +109,11 @@ class PiggyBank extends Model return $this->hasMany(PiggyBankEvent::class); } + public function accounts(): BelongsToMany + { + return $this->belongsToMany(Account::class); + } + public function piggyBankRepetitions(): HasMany { return $this->hasMany(PiggyBankRepetition::class); @@ -118,9 +122,9 @@ 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; } protected function accountId(): Attribute @@ -140,7 +144,7 @@ class PiggyBank extends Model /** * Get the max amount */ - protected function targetamount(): Attribute + protected function targetAmount(): Attribute { return Attribute::make( get: static fn ($value) => (string)$value, diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index 38008361dd..eaa42bed7d 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -47,7 +47,7 @@ class PiggyBankRepetition extends Model '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 { 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..d1675663f2 --- /dev/null +++ b/database/migrations/2024_11_30_075826_multi_piggy.php @@ -0,0 +1,103 @@ +dropForeign('piggy_banks_account_id_foreign'); + }); + Schema::table('piggy_banks', static function (Blueprint $table) { + // 2. make column nullable. + $table->unsignedInteger('account_id')->nullable()->change(); + }); + Schema::table('piggy_banks', static function (Blueprint $table) { + // 3. add currency + $table->integer('transaction_currency_id', false, true)->after('account_id'); + $table->foreign('transaction_currency_id','unique_currency')->references('id')->on('transaction_currencies')->onDelete('cascade'); + }); + Schema::table('piggy_banks', static function (Blueprint $table) { + // 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) { + // 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) { + // 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) { + $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) { + // 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) { + // 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'); + } +}; From 21a6927279f25ab41b3462d10c7e9d74e4325da1 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 30 Nov 2024 16:02:30 +0100 Subject: [PATCH 006/167] Rename fields for piggy bank --- .../Autocomplete/PiggyBankController.php | 2 +- .../Commands/Correction/CorrectAmounts.php | 4 +- app/Console/Commands/Tools/ApplyRules.php | 4 +- app/Handlers/Observer/PiggyBankObserver.php | 8 ++-- .../Controllers/Chart/PiggyBankController.php | 2 +- .../Controllers/Json/FrontpageController.php | 6 +-- .../PiggyBank/AmountController.php | 8 ++-- .../Controllers/PiggyBank/EditController.php | 8 ++-- app/Models/PiggyBankRepetition.php | 20 ++++----- .../PiggyBank/ModifiesPiggyBanks.php | 42 +++++++++---------- .../PiggyBank/PiggyBankRepository.php | 22 +++++----- app/Support/Export/ExportDataGenerator.php | 8 ++-- .../Actions/UpdatePiggybank.php | 6 +-- app/Transformers/PiggyBankTransformer.php | 8 ++-- app/Transformers/V2/PiggyBankTransformer.php | 10 ++--- 15 files changed, 79 insertions(+), 79 deletions(-) diff --git a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php index 1c29c51bec..cf29aa037a 100644 --- a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php +++ b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php @@ -114,7 +114,7 @@ class PiggyBankController extends Controller '%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_name' => $currency->name, diff --git a/app/Console/Commands/Correction/CorrectAmounts.php b/app/Console/Commands/Correction/CorrectAmounts.php index 73ff3b3309..639c96edec 100644 --- a/app/Console/Commands/Correction/CorrectAmounts.php +++ b/app/Console/Commands/Correction/CorrectAmounts.php @@ -173,7 +173,7 @@ class CorrectAmounts extends Command /** @var PiggyBankRepetition $item */ foreach ($set as $item) { - $item->currentamount = app('steam')->positive($item->current_amount); + $item->current_amount = app('steam')->positive($item->current_amount); $item->save(); } $this->friendlyInfo(sprintf('Corrected %d piggy bank repetition amount(s).', $count)); @@ -191,7 +191,7 @@ class CorrectAmounts extends Command /** @var PiggyBank $item */ foreach ($set as $item) { - $item->targetamount = app('steam')->positive($item->target_amount); + $item->target_amount = app('steam')->positive($item->target_amount); $item->save(); } $this->friendlyInfo(sprintf('Corrected %d piggy bank amount(s).', $count)); diff --git a/app/Console/Commands/Tools/ApplyRules.php b/app/Console/Commands/Tools/ApplyRules.php index 5e6e64552f..023864e1aa 100644 --- a/app/Console/Commands/Tools/ApplyRules.php +++ b/app/Console/Commands/Tools/ApplyRules.php @@ -128,7 +128,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. @@ -296,7 +296,7 @@ class ApplyRules extends Command [$inputEnd, $inputStart] = [$inputStart, $inputEnd]; } - $this->startDate = $inputStart; + $this->start_date = $inputStart; $this->endDate = $inputEnd; } diff --git a/app/Handlers/Observer/PiggyBankObserver.php b/app/Handlers/Observer/PiggyBankObserver.php index 2857f7c2b7..40bde63a1a 100644 --- a/app/Handlers/Observer/PiggyBankObserver.php +++ b/app/Handlers/Observer/PiggyBankObserver.php @@ -37,10 +37,10 @@ class PiggyBankObserver app('log')->debug('Observe "created" of a piggy bank.'); $repetition = new PiggyBankRepetition(); $repetition->piggyBank()->associate($piggyBank); - $repetition->start_date = $piggyBank->startdate; - $repetition->start_date_tz = $piggyBank->startdate->format('e'); - $repetition->target_date = $piggyBank->targetdate; - $repetition->target_date_tz = $piggyBank->targetdate?->format('e'); + $repetition->start_date = $piggyBank->start_date; + $repetition->start_date_tz = $piggyBank->start_date->format('e'); + $repetition->target_date = $piggyBank->target_date; + $repetition->target_date_tz = $piggyBank->target_date?->format('e'); $repetition->current_amount = '0'; $repetition->save(); } diff --git a/app/Http/Controllers/Chart/PiggyBankController.php b/app/Http/Controllers/Chart/PiggyBankController.php index 4bf27e2748..7bbfd0aae0 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(); diff --git a/app/Http/Controllers/Json/FrontpageController.php b/app/Http/Controllers/Json/FrontpageController.php index a8ab134e96..959c1ce4e6 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/PiggyBank/AmountController.php b/app/Http/Controllers/PiggyBank/AmountController.php index 28085daa06..d14446d291 100644 --- a/app/Http/Controllers/PiggyBank/AmountController.php +++ b/app/Http/Controllers/PiggyBank/AmountController.php @@ -72,8 +72,8 @@ class AmountController extends Controller $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); + if (0 !== bccomp($piggyBank->target_amount, '0')) { + $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); $maxAmount = min($leftOnAccount, $leftToSave); } $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); @@ -94,8 +94,8 @@ class AmountController extends Controller $savedSoFar = $this->piggyRepos->getCurrentAmount($piggyBank); $maxAmount = $leftOnAccount; - if (0 !== bccomp($piggyBank->targetamount, '0')) { - $leftToSave = bcsub($piggyBank->targetamount, $savedSoFar); + if (0 !== bccomp($piggyBank->target_amount, '0')) { + $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); $maxAmount = min($leftOnAccount, $leftToSave); } $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); diff --git a/app/Http/Controllers/PiggyBank/EditController.php b/app/Http/Controllers/PiggyBank/EditController.php index c0a2de083e..6df3ec3101 100644 --- a/app/Http/Controllers/PiggyBank/EditController.php +++ b/app/Http/Controllers/PiggyBank/EditController.php @@ -77,8 +77,8 @@ class EditController extends Controller $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'); + $targetDate = $piggyBank->target_date?->format('Y-m-d'); + $startDate = $piggyBank->start_date?->format('Y-m-d'); $currency = $this->accountRepository->getAccountCurrency($piggyBank->account); if (null === $currency) { $currency = app('amount')->getDefaultCurrency(); @@ -87,13 +87,13 @@ class EditController extends Controller $preFilled = [ 'name' => $piggyBank->name, 'account_id' => $piggyBank->account_id, - 'targetamount' => app('steam')->bcround($piggyBank->targetamount, $currency->decimal_places), + 'targetamount' => app('steam')->bcround($piggyBank->target_amount, $currency->decimal_places), 'targetdate' => $targetDate, 'startdate' => $startDate, 'object_group' => null !== $piggyBank->objectGroups->first() ? $piggyBank->objectGroups->first()->title : '', 'notes' => null === $note ? '' : $note->text, ]; - if (0 === bccomp($piggyBank->targetamount, '0')) { + if (0 === bccomp($piggyBank->target_amount, '0')) { $preFilled['targetamount'] = ''; } session()->flash('preFilled', $preFilled); diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index eaa42bed7d..378af8edb7 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -42,8 +42,8 @@ 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', ]; @@ -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,15 +82,15 @@ 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, diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index df3e98c10e..c5f9293c2e 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -59,7 +59,7 @@ trait ModifiesPiggyBanks if (null === $repetition) { return false; } - $repetition->currentamount = bcsub($repetition->currentamount, $amount); + $repetition->current_amount = bcsub($repetition->current_amount, $amount); $repetition->save(); app('log')->debug('addAmount [a]: Trigger change for negative amount.'); @@ -74,8 +74,8 @@ trait ModifiesPiggyBanks if (null === $repetition) { return false; } - $currentAmount = $repetition->currentamount ?? '0'; - $repetition->currentamount = bcadd($currentAmount, $amount); + $currentAmount = $repetition->current_amount ?? '0'; + $repetition->current_amount = bcadd($currentAmount, $amount); $repetition->save(); app('log')->debug('addAmount [b]: Trigger change for positive amount.'); @@ -88,11 +88,11 @@ trait ModifiesPiggyBanks { $today = today(config('app.timezone')); $leftOnAccount = $this->leftOnAccount($piggyBank, $today); - $savedSoFar = $this->getRepetition($piggyBank)->currentamount; + $savedSoFar = $this->getRepetition($piggyBank)->current_amount; $maxAmount = $leftOnAccount; $leftToSave = null; - if (0 !== bccomp($piggyBank->targetamount, '0')) { - $leftToSave = bcsub($piggyBank->targetamount, $savedSoFar); + if (0 !== bccomp($piggyBank->target_amount, '0')) { + $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); $maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount; } @@ -114,7 +114,7 @@ trait ModifiesPiggyBanks if (null === $repetition) { return false; } - $savedSoFar = $repetition->currentamount; + $savedSoFar = $repetition->current_amount; return bccomp($amount, $savedSoFar) <= 0; } @@ -143,12 +143,12 @@ trait ModifiesPiggyBanks 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')) { @@ -213,7 +213,7 @@ trait ModifiesPiggyBanks // 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->current_amount = $data['current_amount']; $repetition->save(); } @@ -318,13 +318,13 @@ trait ModifiesPiggyBanks // 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); + if (null !== $repetition && $repetition->current_amount > $piggyBank->target_amount && 0 !== bccomp($piggyBank->target_amount, '0')) { + $difference = bcsub($piggyBank->target_amount, $repetition->current_amount); // an amount will be removed, create "negative" event: event(new ChangedAmount($piggyBank, $difference, null, null)); - $repetition->currentamount = $piggyBank->targetamount; + $repetition->current_amount = $piggyBank->target_amount; $repetition->save(); } @@ -370,18 +370,18 @@ trait ModifiesPiggyBanks $piggyBank->account_id = (int)$data['account_id']; } if (array_key_exists('targetamount', $data) && '' !== $data['targetamount']) { - $piggyBank->targetamount = $data['targetamount']; + $piggyBank->target_amount = $data['targetamount']; } if (array_key_exists('targetamount', $data) && '' === $data['targetamount']) { - $piggyBank->targetamount = '0'; + $piggyBank->target_amount = '0'; } if (array_key_exists('targetdate', $data) && '' !== $data['targetdate']) { - $piggyBank->targetdate = $data['targetdate']; - $piggyBank->targetdate_tz = $data['targetdate']?->format('e'); + $piggyBank->target_date = $data['targetdate']; + $piggyBank->target_date_tz = $data['targetdate']?->format('e'); } if (array_key_exists('startdate', $data)) { - $piggyBank->startdate = $data['startdate']; - $piggyBank->startdate_tz = $data['targetdate']?->format('e'); + $piggyBank->start_date = $data['startdate']; + $piggyBank->start_date_tz = $data['targetdate']?->format('e'); } $piggyBank->save(); diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 99d19353a9..b31571b043 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -120,7 +120,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return '0'; } - return $rep->currentamount; + return $rep->current_amount; } public function getRepetition(PiggyBank $piggyBank): ?PiggyBankRepetition @@ -200,10 +200,10 @@ 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'); + $room = bcsub($piggyBank->target_amount, $repetition->current_amount); + $compare = bcmul($repetition->current_amount, '-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)); @@ -223,7 +223,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface // 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', $repetition->current_amount)); 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)); @@ -267,7 +267,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface /** @var PiggyBank $piggy */ foreach ($set as $piggy) { - $currentAmount = $this->getRepetition($piggy)->currentamount ?? '0'; + $currentAmount = $this->getRepetition($piggy)->current_amount ?? '0'; $piggy->name = $piggy->name.' ('.app('amount')->formatAnything($currency, $currentAmount, false).')'; } @@ -298,11 +298,11 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface if (null === $repetition) { return $savePerMonth; } - if (null !== $piggyBank->targetdate && $repetition->currentamount < $piggyBank->targetamount) { + if (null !== $piggyBank->target_date && $repetition->current_amount < $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, $repetition->current_amount); // more than 1 month to go and still need money to save: if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) { @@ -332,7 +332,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface foreach ($piggies as $current) { $repetition = $this->getRepetition($current); if (null !== $repetition) { - $balance = bcsub($balance, $repetition->currentamount); + $balance = bcsub($balance, $repetition->current_amount); } } 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/TransactionRules/Actions/UpdatePiggybank.php b/app/TransactionRules/Actions/UpdatePiggybank.php index 3e90a79ad9..1e748c1926 100644 --- a/app/TransactionRules/Actions/UpdatePiggybank.php +++ b/app/TransactionRules/Actions/UpdatePiggybank.php @@ -178,15 +178,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.'); } diff --git a/app/Transformers/PiggyBankTransformer.php b/app/Transformers/PiggyBankTransformer.php index aaccf20c63..4cc10cc466 100644 --- a/app/Transformers/PiggyBankTransformer.php +++ b/app/Transformers/PiggyBankTransformer.php @@ -84,18 +84,18 @@ class PiggyBankTransformer extends AbstractTransformer // 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); + $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, diff --git a/app/Transformers/V2/PiggyBankTransformer.php b/app/Transformers/V2/PiggyBankTransformer.php index b1d3abada7..30ebe367a7 100644 --- a/app/Transformers/V2/PiggyBankTransformer.php +++ b/app/Transformers/V2/PiggyBankTransformer.php @@ -119,7 +119,7 @@ class PiggyBankTransformer extends AbstractTransformer /** @var PiggyBankRepetition $repetition */ foreach ($repetitions as $repetition) { $this->repetitions[$repetition->piggy_bank_id] = [ - 'amount' => $repetition->currentamount, + 'amount' => $repetition->current_amount, ]; } @@ -178,14 +178,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; @@ -194,7 +194,7 @@ class PiggyBankTransformer extends AbstractTransformer $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); + $savePerMonth = $this->getSuggestedMonthlyAmount($currentAmount, $targetAmount, $piggyBank->start_date, $piggyBank->target_date); $nativeSavePerMonth = $this->converter->convert($this->default, $currency, today(), $savePerMonth); } $this->converter->summarize(); From f2fab5d4eef9040f0145f5a330f3576e1be8eb11 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 1 Dec 2024 06:48:15 +0100 Subject: [PATCH 007/167] Update code for piggy banks. --- .../Commands/Correction/CorrectAmounts.php | 4 +- .../Commands/System/ForceDecimalSize.php | 4 +- .../Upgrade/UpgradeMultiPiggyBanks.php | 118 ++++++++++++++++++ .../Commands/Upgrade/UpgradeSkeleton.php.stub | 1 - app/Handlers/Observer/PiggyBankObserver.php | 19 +-- 5 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php diff --git a/app/Console/Commands/Correction/CorrectAmounts.php b/app/Console/Commands/Correction/CorrectAmounts.php index 639c96edec..10e65e07be 100644 --- a/app/Console/Commands/Correction/CorrectAmounts.php +++ b/app/Console/Commands/Correction/CorrectAmounts.php @@ -163,7 +163,7 @@ class CorrectAmounts extends Command private function fixRepetitions(): void { - $set = PiggyBankRepetition::where('currentamount', '<', 0)->get(); + $set = PiggyBankRepetition::where('current_amount', '<', 0)->get(); $count = $set->count(); if (0 === $count) { $this->friendlyPositive('All piggy bank repetition amounts are positive.'); @@ -181,7 +181,7 @@ class CorrectAmounts extends Command private function fixPiggyBanks(): void { - $set = PiggyBank::where('targetamount', '<', 0)->get(); + $set = PiggyBank::where('target_amount', '<', 0)->get(); $count = $set->count(); if (0 === $count) { $this->friendlyPositive('All piggy bank amounts are positive.'); diff --git a/app/Console/Commands/System/ForceDecimalSize.php b/app/Console/Commands/System/ForceDecimalSize.php index 426e7fbcbd..9f3c42a12c 100644 --- a/app/Console/Commands/System/ForceDecimalSize.php +++ b/app/Console/Commands/System/ForceDecimalSize.php @@ -83,8 +83,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'], ]; diff --git a/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php b/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php new file mode 100644 index 0000000000..f987a0b3d5 --- /dev/null +++ b/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php @@ -0,0 +1,118 @@ +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; + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool) $configVar->data; + } + + return false; + } + + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + + 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); + $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->save(); + + // store current amount in account association. + $piggyBank->accounts()->sync([$piggyBank->account->id => ['current_amount' => $repetition->current_amount]]); + $piggyBank->account_id = null; + $piggyBank->save(); + + // remove all repetitions (no longer used) + $piggyBank->piggyBankRepetitions()->delete(); + + } +} diff --git a/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub b/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub index d2f5b627c0..5d2dba01f5 100644 --- a/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub +++ b/app/Console/Commands/Upgrade/UpgradeSkeleton.php.stub @@ -6,7 +6,6 @@ use Illuminate\Console\Command; /** * Class UpgradeSkeleton. - * TODO DONT FORGET TO ADD THIS TO THE DOCKER BUILD */ class UpgradeSkeleton extends Command { diff --git a/app/Handlers/Observer/PiggyBankObserver.php b/app/Handlers/Observer/PiggyBankObserver.php index 40bde63a1a..74eb37b211 100644 --- a/app/Handlers/Observer/PiggyBankObserver.php +++ b/app/Handlers/Observer/PiggyBankObserver.php @@ -34,15 +34,16 @@ 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->start_date = $piggyBank->start_date; - $repetition->start_date_tz = $piggyBank->start_date->format('e'); - $repetition->target_date = $piggyBank->target_date; - $repetition->target_date_tz = $piggyBank->target_date?->format('e'); - $repetition->current_amount = '0'; - $repetition->save(); + app('log')->debug('Observe "created" of a piggy bank. DO NOTHING.'); + +// $repetition = new PiggyBankRepetition(); +// $repetition->piggyBank()->associate($piggyBank); +// $repetition->start_date = $piggyBank->start_date; +// $repetition->start_date_tz = $piggyBank->start_date->format('e'); +// $repetition->target_date = $piggyBank->target_date; +// $repetition->target_date_tz = $piggyBank->target_date?->format('e'); +// $repetition->current_amount = '0'; +// $repetition->save(); } /** From cdf1ebf3f709d29ddc9319ca74eebe1039a21d26 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 1 Dec 2024 18:16:48 +0100 Subject: [PATCH 008/167] Better ability to store piggy banks. --- .../Autocomplete/PiggyBankController.php | 6 +- .../V1/Controllers/Data/PurgeController.php | 20 +-- .../Models/Account/ListController.php | 2 +- .../Models/PiggyBank/ListController.php | 33 +++++ .../Models/PiggyBank/StoreRequest.php | 96 ++++++++++--- app/Factory/PiggyBankFactory.php | 129 ++++++++++++++++++ .../Controllers/PiggyBank/IndexController.php | 2 +- app/Models/PiggyBank.php | 8 +- .../Currency/CurrencyRepository.php | 5 + .../Currency/CurrencyRepositoryInterface.php | 2 + .../PiggyBank/ModifiesPiggyBanks.php | 87 ++---------- .../PiggyBank/PiggyBankRepository.php | 9 +- .../PiggyBankRepositoryInterface.php | 17 ++- app/Validation/FireflyValidator.php | 11 +- config/firefly.php | 3 + resources/lang/en_US/validation.php | 1 + routes/api.php | 1 + 17 files changed, 297 insertions(+), 135 deletions(-) diff --git a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php index cf29aa037a..5a757dd35f 100644 --- a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php +++ b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php @@ -68,12 +68,11 @@ class PiggyBankController extends Controller { $data = $request->getData(); $piggies = $this->piggyRepository->searchPiggyBank($data['query'], $this->parameters->get('limit')); - $defaultCurrency = app('amount')->getDefaultCurrency(); $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, @@ -99,12 +98,11 @@ class PiggyBankController extends Controller { $data = $request->getData(); $piggies = $this->piggyRepository->searchPiggyBank($data['query'], $this->parameters->get('limit')); - $defaultCurrency = app('amount')->getDefaultCurrency(); $response = []; /** @var PiggyBank $piggy */ foreach ($piggies as $piggy) { - $currency = $this->accountRepository->getAccountCurrency($piggy->account) ?? $defaultCurrency; + $currency = $piggy->transactionCurrency; $currentAmount = $this->piggyRepository->getRepetition($piggy)->current_amount ?? '0'; $objectGroup = $piggy->objectGroups()->first(); $response[] = [ diff --git a/app/Api/V1/Controllers/Data/PurgeController.php b/app/Api/V1/Controllers/Data/PurgeController.php index 8d076b62cf..d80b60315b 100644 --- a/app/Api/V1/Controllers/Data/PurgeController.php +++ b/app/Api/V1/Controllers/Data/PurgeController.php @@ -36,6 +36,7 @@ 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; @@ -63,14 +64,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/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/PiggyBank/ListController.php b/app/Api/V1/Controllers/Models/PiggyBank/ListController.php index 3c3ac1672b..8fda63be8e 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; @@ -118,4 +119,36 @@ class ListController extends Controller 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/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); + } } diff --git a/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php b/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php index 8adc656a65..d73abc7b8c 100644 --- a/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php +++ b/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php @@ -24,10 +24,13 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Models\PiggyBank; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Rules\IsValidPositiveAmount; 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,19 +45,20 @@ 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; } @@ -65,15 +69,67 @@ class StoreRequest extends FormRequest 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 IsValidPositiveAmount()], + 'object_group_id' => 'numeric|belongsToUser:object_groups,id', + 'object_group_title' => ['min:1', 'max:255'], + 'target_amount' => ['required', new IsValidPositiveAmount()], + 'start_date' => 'date|nullable', + 'transaction_currency_id' => 'exists:transaction_currencies,id', + 'transaction_currency_code' => 'exists:transaction_currencies,code', + 'target_date' => 'date|nullable|after:start_date', + 'notes' => 'max:65000', ]; } + + /** + * Can only store money on liabilities and asset accouns. + */ + public function withValidator(Validator $validator): void + { + $validator->after( + function (Validator $validator): void { + // validate start before end only if both are there. + $data = $validator->getData(); + 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) { + $type = $account->accountType->type; + if (!in_array($type, $types, true)) { + $validator->errors()->add(sprintf('accounts.%d', $index), trans('validation.invalid_account_type')); + } + } + } + } + } + ); + if ($validator->fails()) { + Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); + } + } + + 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($entry['current_amount'] ?? '0'), + ]; + } + return $return; + } } diff --git a/app/Factory/PiggyBankFactory.php b/app/Factory/PiggyBankFactory.php index 505c92d0da..df9b54bc46 100644 --- a/app/Factory/PiggyBankFactory.php +++ b/app/Factory/PiggyBankFactory.php @@ -23,8 +23,14 @@ 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\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\User; +use Illuminate\Database\QueryException; /** * Class PiggyBankFactory @@ -32,6 +38,72 @@ use FireflyIII\User; class PiggyBankFactory { private User $user; + private CurrencyRepositoryInterface $currencyRepository; + private AccountRepositoryInterface $accountRepository; + private PiggyBankRepositoryInterface $piggyBankRepository; + + /** + * Store a piggy bank or come back with an exception. + * + * @param array $data + * + * @return PiggyBank + */ + public function store(array $data): PiggyBank { + $this->currencyRepository = app(CurrencyRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->piggyBankRepository = app(PiggyBankRepositoryInterface::class); + $this->currencyRepository->setUser($this->user); + $this->accountRepository->setUser($this->user); + $this->piggyBankRepository->setUser($this->user); + $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::create($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]); + $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; + } public function find(?int $piggyBankId, ?string $piggyBankName): ?PiggyBank { @@ -70,4 +142,61 @@ class PiggyBankFactory { $this->user = $user; } + + 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; + } + + private function setOrder(PiggyBank $piggyBank, array $data): PiggyBank { + $this->resetOrder(); + $order = $this->getMaxOrder() + 1; + if (array_key_exists('order', $data)) { + $order = $data['order']; + } + $piggyBank->order = $order; + $piggyBank->save(); + return $piggyBank; + + } + + private 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; + } + } + + + private function getMaxOrder(): int + { + return (int)$this->user->piggyBanks()->max('piggy_banks.order'); + } + + private function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void { + /** @var array $info */ + foreach($accounts as $info) { + $account = $this->accountRepository->find((int)($info['account_id'] ?? 0)); + if(null === $account) { + continue; + } + $piggyBank->accounts()->syncWithoutDetaching([$account->id, ['current_amount' => $info['current_amount'] ?? '0']]); + } + } } diff --git a/app/Http/Controllers/PiggyBank/IndexController.php b/app/Http/Controllers/PiggyBank/IndexController.php index 9c3578550e..9c08ebdf25 100644 --- a/app/Http/Controllers/PiggyBank/IndexController.php +++ b/app/Http/Controllers/PiggyBank/IndexController.php @@ -79,7 +79,7 @@ class IndexController extends Controller public function index() { $this->cleanupObjectGroups(); - $this->piggyRepos->resetOrder(); + //$this->piggyRepos->resetOrder(); $collection = $this->piggyRepos->getPiggyBanks(); $accounts = []; diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 0edfa05b9e..911566e628 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -55,7 +55,7 @@ class PiggyBank extends Model 'target_amount' => 'string', ]; - protected $fillable = ['name', 'account_id', 'order', 'target_amount', 'start_date', 'start_date_tz', 'target_date', 'target_date_tz', 'active']; + protected $fillable = ['name', 'account_id', 'order', 'target_amount', 'start_date', 'start_date_tz', 'target_date', 'target_date_tz', 'active','transaction_currency_id']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -67,9 +67,9 @@ class PiggyBank extends Model if (auth()->check()) { $piggyBankId = (int)$value; $piggyBank = self::where('piggy_banks.id', $piggyBankId) - ->leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id') - ->where('accounts.user_id', auth()->user()->id)->first(['piggy_banks.*']) - ; + ->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) { return $piggyBank; } diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index 5ca4924769..97cb7ba79f 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -105,4 +105,9 @@ class CurrencyRepository implements CurrencyRepositoryInterface $this->user = $user; } } + + #[\Override] public function find(int $currencyId): ?TransactionCurrency + { + return TransactionCurrency::find($currencyId); + } } diff --git a/app/Repositories/Currency/CurrencyRepositoryInterface.php b/app/Repositories/Currency/CurrencyRepositoryInterface.php index e714977abb..b5fb7d7acb 100644 --- a/app/Repositories/Currency/CurrencyRepositoryInterface.php +++ b/app/Repositories/Currency/CurrencyRepositoryInterface.php @@ -42,6 +42,8 @@ interface CurrencyRepositoryInterface */ public function findByCode(string $currencyCode): ?TransactionCurrency; + public function find(int $currencyId): ?TransactionCurrency; + /** * Returns the complete set of transactions but needs * no user object. diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index c5f9293c2e..998fed9b23 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -26,11 +26,14 @@ namespace FireflyIII\Repositories\PiggyBank; use FireflyIII\Events\Model\PiggyBank\ChangedAmount; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\PiggyBankFactory; use FireflyIII\Models\Note; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; +use FireflyIII\Support\Facades\Amount; use Illuminate\Database\QueryException; /** @@ -178,82 +181,11 @@ 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']); - - // 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->current_amount = $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; + $factory = new PiggyBankFactory(); + $factory->setUser($this->user); + return $factory->store($data); } - /** - * 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 { @@ -282,13 +214,12 @@ trait ModifiesPiggyBanks return true; } - private function updateNote(PiggyBank $piggyBank, string $note): bool + public function updateNote(PiggyBank $piggyBank, string $note): void { if ('' === $note) { $dbNote = $piggyBank->notes()->first(); $dbNote?->delete(); - - return true; + return ; } $dbNote = $piggyBank->notes()->first(); if (null === $dbNote) { @@ -297,8 +228,6 @@ trait ModifiesPiggyBanks } $dbNote->text = trim($note); $dbNote->save(); - - return true; } public function update(PiggyBank $piggyBank, array $data): PiggyBank diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index b31571b043..1e9658d290 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -240,10 +240,6 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } } - public function getMaxOrder(): int - { - return (int)$this->user->piggyBanks()->max('piggy_banks.order'); - } /** * Return note for piggy bank. @@ -351,4 +347,9 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return $search->take($limit)->get(); } + + #[\Override] public function purgeAll(): void + { + throw new FireflyException('TODO Not implemented'); + } } diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index 7c1e12becc..dfe534f4cf 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -52,6 +52,8 @@ interface PiggyBankRepositoryInterface public function destroyAll(): void; + public function purgeAll(): void; + public function find(int $piggyBankId): ?PiggyBank; /** @@ -78,10 +80,7 @@ interface PiggyBankRepositoryInterface */ public function getExactAmount(PiggyBank $piggyBank, PiggyBankRepetition $repetition, TransactionJournal $journal): string; - /** - * Highest order of all piggy banks. - */ - public function getMaxOrder(): int; + public function updateNote(PiggyBank $piggyBank, string $note): void; /** * Return note for piggy bank. @@ -114,10 +113,10 @@ interface PiggyBankRepositoryInterface public function removeObjectGroup(PiggyBank $piggyBank): PiggyBank; - /** - * Correct order of piggies in case of issues. - */ - public function resetOrder(): void; +// /** +// * Correct order of piggies in case of issues. +// */ +// public function resetOrder(): void; /** * Search for piggy banks. @@ -133,7 +132,7 @@ interface PiggyBankRepositoryInterface */ public function setOrder(PiggyBank $piggyBank, int $newOrder): bool; - public function setUser(null|Authenticatable|User $user): void; + public function setUser(null | Authenticatable | User $user): void; /** * Store new piggy bank. diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 95553306ff..136bc2f9a5 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; @@ -812,15 +813,15 @@ 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/config/firefly.php b/config/firefly.php index 2b651de7d9..c13ef5ecbe 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -913,4 +913,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/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index bdf68a07fd..9f6dc2fe54 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -25,6 +25,7 @@ declare(strict_types=1); return [ + 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', '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/routes/api.php b/routes/api.php index 750450b257..ec16affdd9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -615,6 +615,7 @@ Route::group( Route::get('{piggyBank}/events', ['uses' => 'ListController@piggyBankEvents', 'as' => 'events']); Route::get('{piggyBank}/attachments', ['uses' => 'ListController@attachments', 'as' => 'attachments']); + Route::get('{piggyBank}/accounts', ['uses' => 'ListController@accounts', 'as' => 'accounts']); } ); From d740814f8845c9007479602bcffa0912770b05f3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 1 Dec 2024 18:32:05 +0100 Subject: [PATCH 009/167] API works for multi-account piggies, the rest throws an exception --- .../Models/PiggyBank/ShowController.php | 2 +- .../Integrity/AddTimezonesToDates.php | 4 +- app/Models/PiggyBank.php | 5 ++ .../PiggyBank/ModifiesPiggyBanks.php | 1 + .../PiggyBank/PiggyBankRepository.php | 74 +++++++++---------- app/Transformers/PiggyBankTransformer.php | 58 +++++++++------ app/Transformers/V2/PiggyBankTransformer.php | 1 + app/User.php | 8 -- 8 files changed, 82 insertions(+), 71 deletions(-) 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/Console/Commands/Integrity/AddTimezonesToDates.php b/app/Console/Commands/Integrity/AddTimezonesToDates.php index 7dafa505c4..cd347bd274 100644 --- a/app/Console/Commands/Integrity/AddTimezonesToDates.php +++ b/app/Console/Commands/Integrity/AddTimezonesToDates.php @@ -67,8 +67,8 @@ class AddTimezonesToDates extends Command CurrencyExchangeRate::class => ['date'], // done InvitedUser::class => ['expires'], PiggyBankEvent::class => ['date'], - PiggyBankRepetition::class => ['startdate', 'targetdate'], - PiggyBank::class => ['startdate', 'targetdate'], // done + 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'], diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 911566e628..d5a936cf31 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -78,6 +78,11 @@ class PiggyBank extends Model throw new NotFoundHttpException(); } + public function transactionCurrency(): BelongsTo + { + return $this->belongsTo(TransactionCurrency::class); + } + public function account(): BelongsTo { return $this->belongsTo(Account::class); diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index 998fed9b23..09169ef898 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -45,6 +45,7 @@ trait ModifiesPiggyBanks public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void { + throw new FireflyException('[a] Piggy bank repetitions are EOL.'); app('log')->debug(sprintf('addAmountToRepetition: %s', $amount)); if (-1 === bccomp($amount, '0')) { app('log')->debug('Remove amount.'); diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 1e9658d290..ce32ca3269 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -94,7 +94,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function getAttachments(PiggyBank $piggyBank): Collection { - $set = $piggyBank->attachments()->get(); + $set = $piggyBank->attachments()->get(); /** @var \Storage $disk */ $disk = \Storage::disk('upload'); @@ -115,16 +115,18 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function getCurrentAmount(PiggyBank $piggyBank): string { - $rep = $this->getRepetition($piggyBank); - if (null === $rep) { - return '0'; + $sum = '0'; + foreach ($piggyBank->accounts as $account) { + $amount = (string) $account->pivot->current_amount; + $amount = '' === $amount ? '0' : $amount; + $sum = bcadd($sum, $amount); } - - return $rep->current_amount; + return $sum; } public function getRepetition(PiggyBank $piggyBank): ?PiggyBankRepetition { + throw new FireflyException('[b] Piggy bank repetitions are EOL.'); return $piggyBank->piggyBankRepetitions()->first(); } @@ -140,17 +142,18 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function getExactAmount(PiggyBank $piggyBank, PiggyBankRepetition $repetition, TransactionJournal $journal): string { + throw new FireflyException('[c] Piggy bank repetitions are EOL.'); app('log')->debug(sprintf('Now in getExactAmount(%d, %d, %d)', $piggyBank->id, $repetition->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); @@ -159,10 +162,10 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBankCurrency->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 +187,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,8 +203,8 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } app('log')->debug(sprintf('The currency is %s and the amount is %s', $currency->code, $amount)); - $room = bcsub($piggyBank->target_amount, $repetition->current_amount); - $compare = bcmul($repetition->current_amount, '-1'); + $room = bcsub($piggyBank->target_amount, $repetition->current_amount); + $compare = bcmul($repetition->current_amount, '-1'); if (0 === bccomp($piggyBank->target_amount, '0')) { // amount is zero? then the "room" is positive amount of we wish to add or remove. @@ -230,10 +233,10 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return $compare; } - return (string)$amount; + return (string) $amount; } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { if ($user instanceof User) { $this->user = $user; @@ -249,7 +252,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface /** @var null|Note $note */ $note = $piggyBank->notes()->first(); - return (string)$note?->text; + return (string) $note?->text; } /** @@ -259,12 +262,12 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface { $currency = app('amount')->getDefaultCurrency(); - $set = $this->getPiggyBanks(); + $set = $this->getPiggyBanks(); /** @var PiggyBank $piggy */ foreach ($set as $piggy) { $currentAmount = $this->getRepetition($piggy)->current_amount ?? '0'; - $piggy->name = $piggy->name.' ('.app('amount')->formatAnything($currency, $currentAmount, false).')'; + $piggy->name = $piggy->name . ' (' . app('amount')->formatAnything($currency, $currentAmount, false) . ')'; } return $set; @@ -272,16 +275,17 @@ 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', auth()->user()->id) ->with( [ 'account', 'objectGroups', ] ) - ->orderBy('order', 'ASC')->get() - ; + ->orderBy('piggy_banks.order', 'ASC')->get(); } /** @@ -289,20 +293,17 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function getSuggestedMonthlyAmount(PiggyBank $piggyBank): string { - $savePerMonth = '0'; - $repetition = $this->getRepetition($piggyBank); - if (null === $repetition) { - return $savePerMonth; - } - if (null !== $piggyBank->target_date && $repetition->current_amount < $piggyBank->target_amount) { + $savePerMonth = '0'; + $currentAmount = $this->getCurrentAmount($piggyBank); + if (null !== $piggyBank->target_date && $currentAmount < $piggyBank->target_amount) { $now = today(config('app.timezone')); $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, $repetition->current_amount); + $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: @@ -342,8 +343,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface $search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query)); } $search->orderBy('piggy_banks.order', 'ASC') - ->orderBy('piggy_banks.name', 'ASC') - ; + ->orderBy('piggy_banks.name', 'ASC'); return $search->take($limit)->get(); } diff --git a/app/Transformers/PiggyBankTransformer.php b/app/Transformers/PiggyBankTransformer.php index 4cc10cc466..701c816257 100644 --- a/app/Transformers/PiggyBankTransformer.php +++ b/app/Transformers/PiggyBankTransformer.php @@ -54,25 +54,22 @@ 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); - $notes = '' === $notes ? null : $notes; + $notes = $this->piggyRepos->getNoteText($piggyBank); + $notes = '' === $notes ? null : $notes; $objectGroupId = null; $objectGroupOrder = null; $objectGroupTitle = null; /** @var null|ObjectGroup $objectGroup */ - $objectGroup = $piggyBank->objectGroups->first(); + $objectGroup = $piggyBank->objectGroups->first(); if (null !== $objectGroup) { $objectGroupId = $objectGroup->id; $objectGroupOrder = $objectGroup->order; @@ -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 = app('steam')->bcround($this->piggyRepos->getCurrentAmount($piggyBank), $currency->decimal_places); // Amounts, depending on 0.0 state of target amount - $percentage = null; - $targetAmount = $piggyBank->target_amount; - $leftToSave = null; - $savePerMonth = null; + $percentage = null; + $targetAmount = $piggyBank->target_amount; + $leftToSave = null; + $savePerMonth = null; if (0 !== bccomp($targetAmount, '0')) { // target amount is not 0.00 $leftToSave = bcsub($piggyBank->target_amount, $currentAmount); - $percentage = (int)bcmul(bcdiv($currentAmount, $targetAmount), '100'); + $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->start_date?->format('Y-m-d'); - $targetDate = $piggyBank->target_date?->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,15 +117,28 @@ 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' => [ [ 'rel' => 'self', - 'uri' => '/piggy_banks/'.$piggyBank->id, + 'uri' => '/piggy_banks/' . $piggyBank->id, ], ], ]; } + + private function renderAccounts(PiggyBank $piggyBank): array + { + $return = []; + foreach ($piggyBank->accounts as $account) { + $return[] = [ + 'id' => $account->id, + 'name' => $account->name, + // TODO add balance, add left to save. + ]; + } + return $return; + } } diff --git a/app/Transformers/V2/PiggyBankTransformer.php b/app/Transformers/V2/PiggyBankTransformer.php index 30ebe367a7..8dfb88b69c 100644 --- a/app/Transformers/V2/PiggyBankTransformer.php +++ b/app/Transformers/V2/PiggyBankTransformer.php @@ -115,6 +115,7 @@ 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) { diff --git a/app/User.php b/app/User.php index deb0b896ab..3e44cb1e43 100644 --- a/app/User.php +++ b/app/User.php @@ -332,14 +332,6 @@ class User extends Authenticatable return $this->hasMany(ObjectGroup::class); } - /** - * Link to piggy banks. - */ - public function piggyBanks(): HasManyThrough - { - return $this->hasManyThrough(PiggyBank::class, Account::class); - } - /** * Link to preferences. */ From 4819b5ac5d08e6461d522f47a5f4ec049fbd2237 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 4 Dec 2024 06:38:47 +0100 Subject: [PATCH 010/167] More code for multi account piggy banks. --- app/Factory/PiggyBankFactory.php | 15 +- .../Controllers/PiggyBank/IndexController.php | 159 ++++++++++++------ app/Models/PiggyBank.php | 2 +- .../PiggyBank/PiggyBankRepository.php | 10 +- .../PiggyBankRepositoryInterface.php | 5 +- app/Transformers/PiggyBankTransformer.php | 3 +- 6 files changed, 131 insertions(+), 63 deletions(-) diff --git a/app/Factory/PiggyBankFactory.php b/app/Factory/PiggyBankFactory.php index df9b54bc46..7f9def7600 100644 --- a/app/Factory/PiggyBankFactory.php +++ b/app/Factory/PiggyBankFactory.php @@ -169,9 +169,20 @@ class PiggyBankFactory } - private function resetOrder(): void + public function resetOrder(): void { - $set = $this->user->piggyBanks()->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']); + // 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( + [ + 'account', + 'objectGroups', + ] + ) + ->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']); $current = 1; foreach ($set as $piggyBank) { if ($piggyBank->order !== $current) { diff --git a/app/Http/Controllers/PiggyBank/IndexController.php b/app/Http/Controllers/PiggyBank/IndexController.php index 9c08ebdf25..eca160f505 100644 --- a/app/Http/Controllers/PiggyBank/IndexController.php +++ b/app/Http/Controllers/PiggyBank/IndexController.php @@ -36,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; @@ -57,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); @@ -79,63 +80,26 @@ class IndexController extends Controller public function index() { $this->cleanupObjectGroups(); - //$this->piggyRepos->resetOrder(); - $collection = $this->piggyRepos->getPiggyBanks(); - $accounts = []; + $this->piggyRepos->resetOrder(); + $collection = $this->piggyRepos->getPiggyBanks(); /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); + $end = session('end', today(config('app.timezone'))->endOfMonth()); // transform piggies using the transformer: - $parameters = new ParameterBag(); + $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. - $piggyBanks = $this->makeSums($piggyBanks); + // data + $piggyBanks = $this->groupPiggyBanks($collection); + $accounts = $this->collectAccounts($collection); + $accounts = $this->mergeAccountsAndPiggies($piggyBanks, $accounts); + $piggyBanks = $this->makeSums($piggyBanks); ksort($piggyBanks); @@ -148,7 +112,7 @@ class IndexController extends Controller foreach ($piggyBanks as $groupOrder => $group) { $groupId = $group['object_group_id']; foreach ($group['piggy_banks'] as $piggy) { - $currencyId = $piggy['currency_id']; + $currencyId = $piggy['currency_id']; $sums[$groupId][$currencyId] ??= [ 'target' => '0', 'saved' => '0', @@ -163,10 +127,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) { @@ -182,8 +146,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); @@ -194,4 +158,91 @@ class IndexController extends Controller return response()->json(['data' => 'OK']); } + + 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; + } } diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index d5a936cf31..fa797977fc 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -116,7 +116,7 @@ class PiggyBank extends Model public function accounts(): BelongsToMany { - return $this->belongsToMany(Account::class); + return $this->belongsToMany(Account::class)->withPivot('current_amount'); } public function piggyBankRepetitions(): HasMany diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index ce32ca3269..cf73589b94 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\PiggyBank; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\PiggyBankFactory; use FireflyIII\Models\Attachment; use FireflyIII\Models\Note; use FireflyIII\Models\PiggyBank; @@ -285,7 +286,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface 'objectGroups', ] ) - ->orderBy('piggy_banks.order', 'ASC')->get(); + ->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']); } /** @@ -352,4 +353,11 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface { throw new FireflyException('TODO Not implemented'); } + + #[\Override] public function resetOrder(): void + { + $factory = new PiggyBankFactory(); + $factory->setUser($this->user); + $factory->resetOrder(); + } } diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index dfe534f4cf..b2c13d5764 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -113,10 +113,7 @@ interface PiggyBankRepositoryInterface public function removeObjectGroup(PiggyBank $piggyBank): PiggyBank; -// /** -// * Correct order of piggies in case of issues. -// */ -// public function resetOrder(): void; + public function resetOrder(): void; /** * Search for piggy banks. diff --git a/app/Transformers/PiggyBankTransformer.php b/app/Transformers/PiggyBankTransformer.php index 701c816257..ae15958fbe 100644 --- a/app/Transformers/PiggyBankTransformer.php +++ b/app/Transformers/PiggyBankTransformer.php @@ -132,10 +132,11 @@ class PiggyBankTransformer extends AbstractTransformer private function renderAccounts(PiggyBank $piggyBank): array { $return = []; - foreach ($piggyBank->accounts as $account) { + foreach ($piggyBank->accounts()->get() as $account) { $return[] = [ 'id' => $account->id, 'name' => $account->name, + 'current_amount' => $account->pivot->current_amount, // TODO add balance, add left to save. ]; } From ea4be9dd0cc4927e1551df9c729779af414a6702 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 6 Dec 2024 08:10:31 +0100 Subject: [PATCH 011/167] Expand forms and improve validation for multi-account piggy banks --- .../Models/PiggyBank/StoreRequest.php | 49 ++++++++++++++++--- app/Factory/PiggyBankFactory.php | 16 +++--- .../PiggyBank/CreateController.php | 5 +- app/Http/Requests/PiggyBankStoreRequest.php | 29 +++++++---- app/Models/AccountType.php | 26 +++++----- .../PiggyBank/ModifiesPiggyBanks.php | 4 +- .../PiggyBank/PiggyBankRepository.php | 4 +- .../Support/RecurringTransactionTrait.php | 6 +-- app/Support/Form/AccountForm.php | 18 ++++++- app/Support/Form/FormSupport.php | 19 +++++++ config/twigbridge.php | 1 + resources/lang/en_US/firefly.php | 1 + resources/lang/en_US/form.php | 7 +-- resources/lang/en_US/validation.php | 2 + resources/views/form/multi-select.twig | 10 ++++ resources/views/piggy-banks/create.twig | 9 ++-- 16 files changed, 149 insertions(+), 57 deletions(-) create mode 100644 resources/views/form/multi-select.twig diff --git a/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php b/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php index d73abc7b8c..2566cfe8ba 100644 --- a/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php +++ b/app/Api/V1/Requests/Models/PiggyBank/StoreRequest.php @@ -24,8 +24,11 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Models\PiggyBank; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Rules\IsValidPositiveAmount; +use FireflyIII\Rules\IsValidZeroOrMoreAmount; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; @@ -73,27 +76,30 @@ class StoreRequest extends FormRequest 'accounts' => 'required', 'accounts.*' => 'array|required', 'accounts.*.account_id' => 'required|numeric|belongsToUser:accounts,id', - 'accounts.*.current_amount' => ['numeric', new IsValidPositiveAmount()], + '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 IsValidPositiveAmount()], + 'target_amount' => ['required', new IsValidZeroOrMoreAmount()], 'start_date' => 'date|nullable', - 'transaction_currency_id' => 'exists:transaction_currencies,id', - 'transaction_currency_code' => 'exists:transaction_currencies,code', + '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 accouns. + * 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(); + $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'); @@ -101,6 +107,13 @@ class StoreRequest extends FormRequest $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')); @@ -108,6 +121,9 @@ class StoreRequest extends FormRequest } } } + if(bccomp($targetAmount, $currentAmount) === -1 && bccomp($targetAmount, '0') === 1) { + $validator->errors()->add('target_amount', trans('validation.current_amount_too_much')); + } } ); if ($validator->fails()) { @@ -126,10 +142,27 @@ class StoreRequest extends FormRequest continue; } $return[] = [ - 'account_id' => $this->integerFromValue((string)($entry['account_id'] ?? '0')), - 'current_amount' => $this->clearString($entry['current_amount'] ?? '0'), + 'account_id' => $this->integerFromValue((string) ($entry['account_id'] ?? '0')), + 'current_amount' => $this->clearString((string) ($entry['current_amount'] ?? '0')), ]; } return $return; } + + 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/Factory/PiggyBankFactory.php b/app/Factory/PiggyBankFactory.php index 7f9def7600..cd41ee1deb 100644 --- a/app/Factory/PiggyBankFactory.php +++ b/app/Factory/PiggyBankFactory.php @@ -37,7 +37,11 @@ use Illuminate\Database\QueryException; */ class PiggyBankFactory { - private User $user; + public User $user { + set(User $value) { + $this->user = $value; + } + } private CurrencyRepositoryInterface $currencyRepository; private AccountRepositoryInterface $accountRepository; private PiggyBankRepositoryInterface $piggyBankRepository; @@ -138,11 +142,6 @@ class PiggyBankFactory return $this->user->piggyBanks()->where('piggy_banks.name', $name)->first(); } - public function setUser(User $user): void - { - $this->user = $user; - } - private function getCurrency(array $data): TransactionCurrency { // currency: $defaultCurrency = app('amount')->getDefaultCurrency(); @@ -197,7 +196,8 @@ class PiggyBankFactory private function getMaxOrder(): int { - return (int)$this->user->piggyBanks()->max('piggy_banks.order'); + return (int) $this->piggyBankRepository->getPiggyBanks()->max('order'); + } private function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void { @@ -207,7 +207,7 @@ class PiggyBankFactory if(null === $account) { continue; } - $piggyBank->accounts()->syncWithoutDetaching([$account->id, ['current_amount' => $info['current_amount'] ?? '0']]); + $piggyBank->accounts()->syncWithoutDetaching([$account->id => ['current_amount' => $info['current_amount'] ?? '0']]); } } } diff --git a/app/Http/Controllers/PiggyBank/CreateController.php b/app/Http/Controllers/PiggyBank/CreateController.php index 92aac833d9..a0626b5c3b 100644 --- a/app/Http/Controllers/PiggyBank/CreateController.php +++ b/app/Http/Controllers/PiggyBank/CreateController.php @@ -92,10 +92,11 @@ 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); + var_dump($data);exit; session()->flash('success', (string)trans('firefly.stored_piggy_bank', ['name' => $piggyBank->name])); app('preferences')->mark(); diff --git a/app/Http/Requests/PiggyBankStoreRequest.php b/app/Http/Requests/PiggyBankStoreRequest.php index 94087fedb8..467cfe607e 100644 --- a/app/Http/Requests/PiggyBankStoreRequest.php +++ b/app/Http/Requests/PiggyBankStoreRequest.php @@ -43,15 +43,21 @@ class PiggyBankStoreRequest extends FormRequest */ public function getPiggyBankData(): array { - return [ + $data = [ 'name' => $this->convertString('name'), - 'startdate' => $this->getCarbonDate('startdate'), - 'account_id' => $this->convertInteger('account_id'), - 'targetamount' => $this->convertString('targetamount'), - 'targetdate' => $this->getCarbonDate('targetdate'), + 'start_date' => $this->getCarbonDate('start_date'), + //'account_id' => $this->convertInteger('account_id'), + 'accounts' => $this->get('accounts'), + 'target_amount' => $this->convertString('target_amount'), + 'target_date' => $this->getCarbonDate('target_date'), 'notes' => $this->stringWithNewlines('notes'), 'object_group_title' => $this->convertString('object_group'), ]; + if(!is_array($data['accounts'])) { + $data['accounts'] = []; + } + + return $data; } /** @@ -61,10 +67,11 @@ class PiggyBankStoreRequest extends FormRequest { return [ 'name' => 'required|min:1|max:255|uniquePiggyBankForUser', - 'account_id' => 'required|belongsToUser:accounts', - 'targetamount' => ['nullable', new IsValidPositiveAmount()], - 'startdate' => 'date', - 'targetdate' => 'date|nullable', + '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', @@ -73,6 +80,10 @@ class PiggyBankStoreRequest extends FormRequest public function withValidator(Validator $validator): void { + // need to have more than one account. + // accounts need to have the same currency or be multi-currency(?). + + if ($validator->fails()) { Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); } diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index 0ef77adcc2..c8bc76b399 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -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 */ 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/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index 09169ef898..f37bc9e1a2 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -182,8 +182,8 @@ trait ModifiesPiggyBanks */ public function store(array $data): PiggyBank { - $factory = new PiggyBankFactory(); - $factory->setUser($this->user); + $factory = new PiggyBankFactory(); + $factory->user = $this->user; return $factory->store($data); } diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index cf73589b94..bd8c78de52 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -356,8 +356,8 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface #[\Override] public function resetOrder(): void { - $factory = new PiggyBankFactory(); - $factory->setUser($this->user); + $factory = new PiggyBankFactory(); + $factory->user = $this->user; $factory->resetOrder(); } } diff --git a/app/Services/Internal/Support/RecurringTransactionTrait.php b/app/Services/Internal/Support/RecurringTransactionTrait.php index 438563c98b..1ca7f701c2 100644 --- a/app/Services/Internal/Support/RecurringTransactionTrait.php +++ b/app/Services/Internal/Support/RecurringTransactionTrait.php @@ -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/Support/Form/AccountForm.php b/app/Support/Form/AccountForm.php index b7bc140a79..8034bf93e6 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; @@ -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/FormSupport.php b/app/Support/Form/FormSupport.php index f91931cd60..09c8de4910 100644 --- a/app/Support/Form/FormSupport.php +++ b/app/Support/Form/FormSupport.php @@ -56,6 +56,25 @@ trait FormSupport return $html; } + 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.multi-select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); + } catch (\Throwable $e) { + app('log')->debug(sprintf('Could not render multi-select(): %s', $e->getMessage())); + $html = 'Could not render multi-select.'; + } + + return $html; + } + protected function label(string $name, ?array $options = null): string { $options ??= []; diff --git a/config/twigbridge.php b/config/twigbridge.php index da8d491d82..60d1e1023d 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -197,6 +197,7 @@ return [ 'assetAccountCheckList', 'assetAccountList', 'longAccountList', + 'assetLiabilityMultiAccountList', ], ], 'CurrencyForm' => [ diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index aa395cd3da..1f6c37af8e 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -2197,6 +2197,7 @@ return [ 'amount' => 'Amount', 'overview' => 'Overview', 'saveOnAccount' => 'Save on account', + 'saveOnAccounts' => 'Save on account(s)', 'unknown' => 'Unknown', 'monthly' => 'Monthly', 'profile' => 'Profile', diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 5e6ab8d583..a83bbd764e 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -69,6 +69,7 @@ return [ // Ignore this comment 'targetamount' => 'Target amount', + 'target_amount' => 'Target amount', 'account_role' => 'Account role', 'opening_balance_date' => 'Opening balance date', 'cc_type' => 'Credit card payment plan', @@ -106,7 +107,9 @@ return [ 'deletePermanently' => 'Delete permanently', 'cancel' => 'Cancel', 'targetdate' => 'Target date', + 'target_date' => 'Target date', 'startdate' => 'Start date', + 'start_date' => 'Start date', 'tag' => 'Tag', 'under' => 'Under', 'symbol' => 'Symbol', @@ -116,7 +119,6 @@ return [ 'creditCardNumber' => 'Credit card number', 'has_headers' => 'Headers', 'date_format' => 'Date format', - 'specifix' => 'Bank- or file specific fixes', 'attachments[]' => 'Attachments', 'title' => 'Title', 'notes' => 'Notes', @@ -125,8 +127,7 @@ return [ 'size' => 'Size', 'trigger' => 'Trigger', 'stop_processing' => 'Stop processing', - 'start_date' => 'Start of range', - 'end_date' => 'End of range', + 'end_date' => 'End date', 'enddate' => 'End date', 'move_rules_before_delete' => 'Rule group', 'start' => 'Start of range', diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 9f6dc2fe54..587db8cb29 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -26,6 +26,8 @@ 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/form/multi-select.twig b/resources/views/form/multi-select.twig new file mode 100644 index 0000000000..b27359e41e --- /dev/null +++ b/resources/views/form/multi-select.twig @@ -0,0 +1,10 @@ +
+ + +
+ {{ Html.select(name~"[]", list, selected).id(options.id).class('form-control').attribute('multiple').attribute('autocomplete','off').attribute('spellcheck','false').attribute('placeholder', options.placeholder) }} + {% include 'form.help' %} + {% include 'form.feedback' %} + +
+
diff --git a/resources/views/piggy-banks/create.twig b/resources/views/piggy-banks/create.twig index f25dd211a8..84662207a4 100644 --- a/resources/views/piggy-banks/create.twig +++ b/resources/views/piggy-banks/create.twig @@ -8,7 +8,6 @@
-
@@ -16,10 +15,10 @@

{{ 'mandatoryFields'|_ }}

- {{ ExpandedForm.text('name') }} - {{ AccountForm.assetAccountList('account_id', null, {label: 'saveOnAccount'|_ }) }} - {{ ExpandedForm.amountNoCurrency('targetamount') }} + {{ AccountForm.assetLiabilityMultiAccountList('accounts', null, {label: 'saveOnAccounts'|_ }) }} + + {{ ExpandedForm.amountNoCurrency('target_amount') }}
@@ -30,7 +29,7 @@

{{ 'optionalFields'|_ }}

- {{ ExpandedForm.date('targetdate') }} + {{ ExpandedForm.date('target_date') }} {{ ExpandedForm.textarea('notes', null, {helpText: trans('firefly.field_supports_markdown')} ) }} {{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }} {{ ExpandedForm.objectGroup() }} From 1220564f309f5692be80833878e15d4fc6d723c2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 7 Dec 2024 08:05:29 +0100 Subject: [PATCH 012/167] Fix creating piggies --- app/Factory/PiggyBankFactory.php | 2 + .../PiggyBank/CreateController.php | 26 ++++--- app/Http/Requests/PiggyBankStoreRequest.php | 77 ++++++++++++++----- app/Support/Form/AccountForm.php | 1 - app/Support/Form/FormSupport.php | 1 + resources/views/piggy-banks/create.twig | 4 +- 6 files changed, 79 insertions(+), 32 deletions(-) diff --git a/app/Factory/PiggyBankFactory.php b/app/Factory/PiggyBankFactory.php index cd41ee1deb..84dbab3853 100644 --- a/app/Factory/PiggyBankFactory.php +++ b/app/Factory/PiggyBankFactory.php @@ -28,6 +28,7 @@ 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; @@ -37,6 +38,7 @@ use Illuminate\Database\QueryException; */ class PiggyBankFactory { + use CreatesObjectGroups; public User $user { set(User $value) { $this->user = $value; diff --git a/app/Http/Controllers/PiggyBank/CreateController.php b/app/Http/Controllers/PiggyBank/CreateController.php index a0626b5c3b..d3af975cc2 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')); } /** @@ -91,33 +94,34 @@ class CreateController extends Controller */ public function store(PiggyBankStoreRequest $request) { - $data = $request->getPiggyBankData(); + $data = $request->getPiggyBankData(); + if (null === $data['start_date']) { $data['start_date'] = today(config('app.timezone')); } $piggyBank = $this->piggyRepos->store($data); - var_dump($data);exit; - 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): /** @var null|array $files */ - $files = $request->hasFile('attachments') ? $request->file('attachments') : null; + $files = $request->hasFile('attachments') ? $request->file('attachments') : null; if (null !== $files && !auth()->user()->hasRole('demo')) { $this->attachments->saveAttachmentsForModel($piggyBank, $files); } 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')); } - $redirect = redirect($this->getPreviousUrl('piggy-banks.create.url')); + $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/Requests/PiggyBankStoreRequest.php b/app/Http/Requests/PiggyBankStoreRequest.php index 467cfe607e..bc796e74a1 100644 --- a/app/Http/Requests/PiggyBankStoreRequest.php +++ b/app/Http/Requests/PiggyBankStoreRequest.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; @@ -43,18 +45,21 @@ class PiggyBankStoreRequest extends FormRequest */ public function getPiggyBankData(): array { - $data = [ - 'name' => $this->convertString('name'), - 'start_date' => $this->getCarbonDate('start_date'), - //'account_id' => $this->convertInteger('account_id'), - 'accounts' => $this->get('accounts'), - 'target_amount' => $this->convertString('target_amount'), - 'target_date' => $this->getCarbonDate('target_date'), - '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($data['accounts'])) { - $data['accounts'] = []; + if (!is_array($accounts)) { + $accounts = []; + } + foreach ($accounts as $item) { + $data['accounts'][] = ['account_id' => (int) ($item)]; } return $data; @@ -66,15 +71,15 @@ class PiggyBankStoreRequest extends FormRequest public function rules(): array { return [ - 'name' => 'required|min:1|max:255|uniquePiggyBankForUser', - 'accounts' => 'required|array', - 'accounts.*' => 'required|belongsToUser:accounts', + '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', + 'order' => 'integer|min:1', + 'object_group' => 'min:0|max:255', + 'notes' => 'min:1|max:32768|nullable', ]; } @@ -82,10 +87,46 @@ class PiggyBankStoreRequest extends FormRequest { // 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 app('amount')->getDefaultCurrency(); + } + return $currency; + } } diff --git a/app/Support/Form/AccountForm.php b/app/Support/Form/AccountForm.php index 8034bf93e6..63f4bb4130 100644 --- a/app/Support/Form/AccountForm.php +++ b/app/Support/Form/AccountForm.php @@ -157,7 +157,6 @@ class AccountForm { $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); } diff --git a/app/Support/Form/FormSupport.php b/app/Support/Form/FormSupport.php index 09c8de4910..4ba51bb046 100644 --- a/app/Support/Form/FormSupport.php +++ b/app/Support/Form/FormSupport.php @@ -63,6 +63,7 @@ trait FormSupport $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); $selected = $this->fillFieldValue($name, $selected); + unset($options['autocomplete'], $options['placeholder']); try { diff --git a/resources/views/piggy-banks/create.twig b/resources/views/piggy-banks/create.twig index 84662207a4..ad944792fa 100644 --- a/resources/views/piggy-banks/create.twig +++ b/resources/views/piggy-banks/create.twig @@ -16,9 +16,9 @@
{{ ExpandedForm.text('name') }} - {{ AccountForm.assetLiabilityMultiAccountList('accounts', null, {label: 'saveOnAccounts'|_ }) }} - {{ ExpandedForm.amountNoCurrency('target_amount') }} + {{ CurrencyForm.currencyList('transaction_ currency_id', null, {helpText:'piggy_default_currency'|_}) }} + {{ AccountForm.assetLiabilityMultiAccountList('accounts', preFilled.accounts, {label: 'saveOnAccounts'|_, helpText: 'piggy_account_currency_match'|_ }) }}
From 26948a058a801ab57fd57a54e86409bc24bea3c5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 7 Dec 2024 08:05:51 +0100 Subject: [PATCH 013/167] Expand UI for notifications. --- app/Http/Controllers/Admin/HomeController.php | 16 +- .../Admin/NotificationController.php | 44 + .../Controllers/PreferencesController.php | 2 + config/firefly.php | 4 +- config/notifications.php | 54 + resources/lang/en_US/breadcrumbs.php | 3 + resources/lang/en_US/firefly.php | 5234 +++++++++-------- resources/views/admin/index.twig | 9 +- .../views/admin/notifications/index.twig | 68 + resources/views/form/multi-select.twig | 2 +- resources/views/partials/flashes.twig | 6 + routes/breadcrumbs.php | 9 + routes/web.php | 5 + 13 files changed, 2834 insertions(+), 2622 deletions(-) create mode 100644 app/Http/Controllers/Admin/NotificationController.php create mode 100644 config/notifications.php create mode 100644 resources/views/admin/notifications/index.twig diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index a641b2f4a5..eabcb1ec30 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -67,22 +67,24 @@ class HomeController extends Controller // admin notification settings: $notifications = []; - foreach (config('firefly.admin_notifications') as $item) { - $notifications[$item] = app('fireflyconfig')->get(sprintf('notification_%s', $item), true)->data; + foreach (config('notifications.notifications.owner') as $key => $info) { + if($info['enabled']) { + $notifications[$key] = app('fireflyconfig')->get(sprintf('notification_%s', $key), true)->data; + } } - $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; + // - return view('admin.index', compact('title', 'mainTitleIcon', 'email', 'notifications', 'slackUrl')); + return view('admin.index', compact('title', 'mainTitleIcon', 'email', 'notifications')); } public function notifications(Request $request): RedirectResponse { - foreach (config('firefly.admin_notifications') as $item) { + foreach (config('notifications.notifications.owner') as $key => $info) { $value = false; - if ($request->has(sprintf('notification_%s', $item))) { + if ($request->has(sprintf('notification_%s', $key))) { $value = true; } - app('fireflyconfig')->set(sprintf('notification_%s', $item), $value); + app('fireflyconfig')->set(sprintf('notification_%s', $key), $value); } $url = (string)$request->get('slackUrl'); if ('' === $url) { diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php new file mode 100644 index 0000000000..a19e849b62 --- /dev/null +++ b/app/Http/Controllers/Admin/NotificationController.php @@ -0,0 +1,44 @@ +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'; + $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; + $discordUrl = app('fireflyconfig')->get('discord_webhook_url', '')->data; + $channels = config('notifications.channels'); + + return view('admin.notifications.index', compact('title', 'subTitle', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl','discordUrl')); + } +} diff --git a/app/Http/Controllers/PreferencesController.php b/app/Http/Controllers/PreferencesController.php index 1206cde540..e326325471 100644 --- a/app/Http/Controllers/PreferencesController.php +++ b/app/Http/Controllers/PreferencesController.php @@ -111,6 +111,7 @@ class PreferencesController extends Controller // notification preferences (single value for each): $notifications = []; + die('fix the reference to the available notifications.'); foreach (config('firefly.available_notifications') as $notification) { $notifications[$notification] = app('preferences')->get(sprintf('notification_%s', $notification), true)->data; } @@ -165,6 +166,7 @@ class PreferencesController extends Controller // extract notifications: $all = $request->all(); + die('fix the reference to the available notifications.'); foreach (config('firefly.available_notifications') as $option) { $key = sprintf('notification_%s', $option); if (array_key_exists($key, $all)) { diff --git a/config/firefly.php b/config/firefly.php index c13ef5ecbe..17253ada99 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -147,9 +147,7 @@ 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' => [ diff --git a/config/notifications.php b/config/notifications.php new file mode 100644 index 0000000000..1253567f8c --- /dev/null +++ b/config/notifications.php @@ -0,0 +1,54 @@ + [ + 'email' => ['enabled' => true, 'ui_configurable' => 0,], + 'slack' => ['enabled' => true, 'ui_configurable' => 1,], + 'discord' => ['enabled' => true, 'ui_configurable' => 1,], + 'nfty' => ['enabled' => false, 'ui_configurable' => 0,], + 'pushover' => ['enabled' => false, 'ui_configurable' => 0,], + 'gotify' => ['enabled' => false, 'ui_configurable' => 0,], + 'pushbullet' => ['enabled' => false, 'ui_configurable' => 0,], + ], + 'notifications' => [ + 'user' => [ + 'some_notification' => [ + 'enabled' => true, + 'email' => '', + 'slack' => '', + ], + ], + '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], + ], + ], + // // 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/resources/lang/en_US/breadcrumbs.php b/resources/lang/en_US/breadcrumbs.php index 97e6bf9530..a2ed05e525 100644 --- a/resources/lang/en_US/breadcrumbs.php +++ b/resources/lang/en_US/breadcrumbs.php @@ -86,4 +86,7 @@ return [ 'mfa_enableMFA' => 'Enable multi-factor authentication', 'mfa_backup_codes' => 'Backup codes', 'mfa_disableMFA' => 'Disable multi-factor authentication', + + // notifications + 'notification_index' => 'Owner notifications', ]; diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 1f6c37af8e..1b3b7f9d6e 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -26,2749 +26,2769 @@ 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 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', // 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 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"', // 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' => '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)', // 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.', + '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 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.', // 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_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 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"', // 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' => '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', // 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' => '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', // 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 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".', - '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', + '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', + 'discord_url_label' => 'Discord webhook URL', // 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 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.', // 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 bill', + '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 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?', // 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 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}', // 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', + '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', - '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 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 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', + '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 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}', // 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' => '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', // 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', - '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', + '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', + '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 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)', // 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 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', + '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 in your notifications settings.', + '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', + '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 confiuration, use the buttons below. Please note that the buttons have no spam control.', + 'notification_channel_name_email' => 'Email', + 'notification_channel_name_slack' => 'Slack', + 'notification_channel_name_discord' => 'Discord', + 'notification_channel_name_nfty' => 'Nfty', + 'notification_channel_name_pushover' => 'Pushover', + 'notification_channel_name_gotify' => 'Gotify', + 'notification_channel_name_pushbullet' => 'Pushbullet', + 'channel_not_available' => 'not available yet', + '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_discord' => 'Test Discord', - '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 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', // 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' => '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', // 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 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', // 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/views/admin/index.twig b/resources/views/admin/index.twig index 2d5b3e44b1..8ae31a35ac 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'|_ }}
  • @@ -34,20 +35,20 @@
    -

    {{ 'admin_notifications'|_ }}

    +

    {{ 'owner_notifications'|_ }}

    - {{ 'admin_notifications_expl'|_ }} + {{ 'owner_notifications_expl'|_ }}

    {% for notification, value in notifications %}
    {% endfor %} - {{ ExpandedForm.text('slackUrl', slackUrl, {'label' : 'slack_url_label'|_}) }} + {# {{ ExpandedForm.text('slackUrl', slackUrl, {'label' : 'slack_url_label'|_}) }} #}
    +
    + + +
    +
    + +
    +
    +

    {{ 'available_channels_title'|_ }}

    +
    +
    +

    + {{ 'available_channels_expl'|_ }} +

    +
      + {% for name,info in channels %} +
    • + {% if info.enabled %} + ☑️ {{ trans('firefly.notification_channel_name_'~name) }} + {% if 0 == info.ui_configurable %}({{ 'configure_channel_in_env'|_ }}) {% endif %} + {% endif %} + {% if not info.enabled %} + ⚠️ {{ trans('firefly.notification_channel_name_'~name) }} ({{ 'channel_not_available'|_ }}) + {% endif %} +
    • + {% endfor %} +
    +
    + +
    +
    +
    + + +{% endblock %} diff --git a/resources/views/form/multi-select.twig b/resources/views/form/multi-select.twig index b27359e41e..acdfc38695 100644 --- a/resources/views/form/multi-select.twig +++ b/resources/views/form/multi-select.twig @@ -2,7 +2,7 @@
    - {{ Html.select(name~"[]", list, selected).id(options.id).class('form-control').attribute('multiple').attribute('autocomplete','off').attribute('spellcheck','false').attribute('placeholder', options.placeholder) }} + {{ 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/partials/flashes.twig b/resources/views/partials/flashes.twig index cd795a2a01..62e4ee7c5f 100644 --- a/resources/views/partials/flashes.twig +++ b/resources/views/partials/flashes.twig @@ -56,7 +56,13 @@ {% endif %} {# SINGLE INFO MESSAGE #} {% if session('info') is not iterable %} + {% if session_has('info_url') %} + + {% endif %} {{ 'flash_info'|_ }}: {{ session('info')|raw }} + {% if session_has('info_url') %} + + {% endif %} {% endif %}
    diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index fdde0ad18f..dde259dfb7 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -179,6 +179,15 @@ Breadcrumbs::for( } ); +Breadcrumbs::for( + 'admin.notification.index', + static function (Generator $breadcrumbs): void { + $breadcrumbs->parent('home'); + $breadcrumbs->push(trans('firefly.administration'), route('admin.index')); + $breadcrumbs->push(trans('breadcrumbs.notification_index'), route('admin.notification.index')); + } +); + Breadcrumbs::for( 'admin.users', static function (Generator $breadcrumbs): void { diff --git a/routes/web.php b/routes/web.php index a702e49163..04b98d3afe 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1398,6 +1398,11 @@ Route::group( // FF configuration: Route::get('configuration', ['uses' => 'ConfigurationController@index', 'as' => 'configuration.index']); Route::post('configuration', ['uses' => 'ConfigurationController@postIndex', 'as' => 'configuration.index.post']); + + // routes for notifications settings. + Route::get('notifications', ['uses' => 'NotificationController@index', 'as' => 'notification.index']); + Route::post('notifications', ['uses' => 'NotificationController@postIndex', 'as' => 'notification.post']); + Route::post('notifications/test', ['uses' => 'NotificationController@testNotification', 'as' => 'notification.test']); } ); From c06fb8daf68fcad7ecac538ac5e47a200bff3b19 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 7 Dec 2024 09:41:09 +0100 Subject: [PATCH 014/167] Expand notifications settings. --- app/Http/Controllers/Admin/HomeController.php | 34 +-------- .../Admin/NotificationController.php | 25 ++++++- app/Http/Requests/NotificationRequest.php | 70 +++++++++++++++++++ resources/lang/en_US/firefly.php | 3 +- resources/views/admin/index.twig | 28 +------- .../views/admin/notifications/index.twig | 13 +++- 6 files changed, 111 insertions(+), 62 deletions(-) create mode 100644 app/Http/Requests/NotificationRequest.php diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index eabcb1ec30..4788f21eda 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -65,38 +65,7 @@ class HomeController extends Controller $email = $pref->data; } - // 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; - } - } - // - - return view('admin.index', compact('title', 'mainTitleIcon', 'email', 'notifications')); - } - - public function notifications(Request $request): RedirectResponse - { - foreach (config('notifications.notifications.owner') as $key => $info) { - $value = false; - if ($request->has(sprintf('notification_%s', $key))) { - $value = true; - } - app('fireflyconfig')->set(sprintf('notification_%s', $key), $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')); + return view('admin.index', compact('title', 'mainTitleIcon', 'email')); } /** @@ -106,6 +75,7 @@ class HomeController extends Controller */ public function testMessage() { + die('disabled.'); Log::channel('audit')->info('User sends test message.'); /** @var User $user */ diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php index a19e849b62..75dc522d91 100644 --- a/app/Http/Controllers/Admin/NotificationController.php +++ b/app/Http/Controllers/Admin/NotificationController.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Admin; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Http\Requests\NotificationRequest; +use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Log; class NotificationController extends Controller @@ -39,6 +41,27 @@ class NotificationController extends Controller $discordUrl = app('fireflyconfig')->get('discord_webhook_url', '')->data; $channels = config('notifications.channels'); - return view('admin.notifications.index', compact('title', 'subTitle', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl','discordUrl')); + + // 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; + } + } + + + return view('admin.notifications.index', compact('title', 'subTitle', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl','discordUrl','notifications')); + } + + public function postIndex(NotificationRequest $request): RedirectResponse { + + var_dump($request->getAll()); + exit; + // app('fireflyconfig')->set(sprintf('notification_%s', $key), $value);; + + session()->flash('success', (string)trans('firefly.notification_settings_saved')); + + return redirect(route('admin.index')); } } diff --git a/app/Http/Requests/NotificationRequest.php b/app/Http/Requests/NotificationRequest.php new file mode 100644 index 0000000000..2f94e047bf --- /dev/null +++ b/app/Http/Requests/NotificationRequest.php @@ -0,0 +1,70 @@ + $info) { + $value = false; + if ($this->has(sprintf('notification_%s', $key))) { + $value = true; + } + $return[$key] = $value; + } + $return['discord_url'] = $this->convertString('discordUrl'); + $return['slack_url'] = $this->convertString('slackUrl'); + return $return; +// if (UrlValidator::isValidWebhookURL($url)) { +// app('fireflyconfig')->set('slack_webhook_url', $url); +// } +// } +// +// +// var_dump($this->all()); +// exit; +// return []; + } + + /** + * Rules for this request. + */ + public function rules(): array + { + // fixed + return [ + //'password' => 'required', + ]; + } + +} diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 1b3b7f9d6e..5ce9ee368b 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -2481,7 +2481,8 @@ return [ '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 in your notifications settings.', + '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', 'settings_notifications' => 'Settings for notifications', 'title_owner_notifications' => 'Owner notifications', 'owner_notification_check_user_new_reg' => 'User gets post-registration welcome message', diff --git a/resources/views/admin/index.twig b/resources/views/admin/index.twig index 8ae31a35ac..c418c0f273 100644 --- a/resources/views/admin/index.twig +++ b/resources/views/admin/index.twig @@ -17,7 +17,7 @@
  • {{ 'journal_link_configuration'|_ }}
  • {{ 'update_check_title'|_ }}
  • -
  • {{ 'settings_notifications'|_ }}
  • +
  • {{ 'settings_notifications'|_ }}
  • @@ -31,32 +31,6 @@ -
    - -
    -
    -

    {{ 'owner_notifications'|_ }}

    -
    -
    -

    - {{ 'owner_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 index 76f0ebcac1..52e5085eb1 100644 --- a/resources/views/admin/notifications/index.twig +++ b/resources/views/admin/notifications/index.twig @@ -4,7 +4,7 @@ {{ Breadcrumbs.render }} {% endblock %} {% block content %} -
    +
    @@ -13,6 +13,17 @@

    {{ 'notification_settings'|_ }}

    +

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

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

    {{ 'channel_settings'|_ }}

    {{ ExpandedForm.text('slackUrl', slackUrl, {'label' : 'slack_url_label'|_}) }} {{ ExpandedForm.text('discordUrl', discordUrl, {'label' : 'discord_url_label'|_}) }}
    From 2f7a1c941e8c06be3f749a407b7118fb4fcc80c9 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 8 Dec 2024 16:28:22 +0100 Subject: [PATCH 015/167] Expand test notification framework. --- .../TestNotificationChannel.php} | 26 ++-- app/Handlers/Events/AdminEventHandler.php | 31 ++++- app/Http/Controllers/Admin/HomeController.php | 19 --- .../Admin/NotificationController.php | 60 ++++++++-- app/Http/Requests/NotificationRequest.php | 25 ++-- app/Models/TransactionJournal.php | 4 - .../Test/TestNotificationDiscord.php | 108 +++++++++++++++++ .../Test/TestNotificationEmail.php | 102 ++++++++++++++++ .../Test/TestNotificationSlack.php | 113 ++++++++++++++++++ app/Providers/EventServiceProvider.php | 5 +- app/Rules/Admin/IsValidDiscordUrl.php | 32 +++++ app/Rules/Admin/IsValidSlackUrl.php | 32 +++++ app/User.php | 16 ++- resources/lang/en_US/firefly.php | 2 + .../views/admin/notifications/index.twig | 4 +- 15 files changed, 509 insertions(+), 70 deletions(-) rename app/Events/{AdminRequestedTestMessage.php => Test/TestNotificationChannel.php} (62%) create mode 100644 app/Notifications/Test/TestNotificationDiscord.php create mode 100644 app/Notifications/Test/TestNotificationEmail.php create mode 100644 app/Notifications/Test/TestNotificationSlack.php create mode 100644 app/Rules/Admin/IsValidDiscordUrl.php create mode 100644 app/Rules/Admin/IsValidSlackUrl.php diff --git a/app/Events/AdminRequestedTestMessage.php b/app/Events/Test/TestNotificationChannel.php similarity index 62% rename from app/Events/AdminRequestedTestMessage.php rename to app/Events/Test/TestNotificationChannel.php index 4f32c7841c..5ad625e64a 100644 --- a/app/Events/AdminRequestedTestMessage.php +++ b/app/Events/Test/TestNotificationChannel.php @@ -1,8 +1,7 @@ . + * 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 TestNotificationChannel { use SerializesModels; - public User $user; + public User $user; + public string $channel; /** * 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 TestNotificationChannel("%s") for user #%d (%s)', $channel, $user->id, $user->email)); + $this->user = $user; + $this->channel = $channel; } } diff --git a/app/Handlers/Events/AdminEventHandler.php b/app/Handlers/Events/AdminEventHandler.php index fb8565b137..9c450a537d 100644 --- a/app/Handlers/Events/AdminEventHandler.php +++ b/app/Handlers/Events/AdminEventHandler.php @@ -26,10 +26,15 @@ namespace FireflyIII\Handlers\Events; use FireflyIII\Events\Admin\InvitationCreated; use FireflyIII\Events\AdminRequestedTestMessage; use FireflyIII\Events\NewVersionAvailable; +use FireflyIII\Events\Test\TestNotificationChannel; use FireflyIII\Notifications\Admin\TestNotification; use FireflyIII\Notifications\Admin\UserInvitation; use FireflyIII\Notifications\Admin\VersionCheckResult; +use FireflyIII\Notifications\Test\TestNotificationDiscord; +use FireflyIII\Notifications\Test\TestNotificationEmail; +use FireflyIII\Notifications\Test\TestNotificationSlack; use FireflyIII\Repositories\User\UserRepositoryInterface; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Notification; /** @@ -39,7 +44,7 @@ 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; } @@ -75,7 +80,7 @@ class AdminEventHandler */ public function sendNewVersion(NewVersionAvailable $event): void { - $sendMail = app('fireflyconfig')->get('notification_new_version', true)->data; + $sendMail = app('fireflyconfig')->get('notification_new_version', true)->data; if (false === $sendMail) { return; } @@ -109,17 +114,34 @@ class AdminEventHandler /** * Sends a test message to an administrator. */ - public function sendTestMessage(AdminRequestedTestMessage $event): void + public function sendTestNotification(TestNotificationChannel $event): void { + Log::debug(sprintf('Now in sendTestNotification(#%d, "%s")', $event->user->id, $event->channel)); /** @var UserRepositoryInterface $repository */ $repository = app(UserRepositoryInterface::class); if (!$repository->hasRole($event->user, 'owner')) { + Log::error(sprintf('User #%d is not an owner.', $event->user->id)); return; } + switch($event->channel) { + case 'email': + $class = TestNotificationEmail::class; + break; + case 'slack': + $class = TestNotificationSlack::class; + break; + case 'discord': + $class = TestNotificationDiscord::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->user, new TestNotification($event->user->email)); + Notification::send($event->user, new $class($event->user->email)); } catch (\Exception $e) { // @phpstan-ignore-line $message = $e->getMessage(); if (str_contains($message, 'Bcc')) { @@ -135,5 +157,6 @@ class AdminEventHandler 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/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index 4788f21eda..8c8b3bf2b0 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -67,23 +67,4 @@ class HomeController extends Controller return view('admin.index', compact('title', 'mainTitleIcon', 'email')); } - - /** - * Send a test message to the admin. - * - * @return Redirector|RedirectResponse - */ - public function testMessage() - { - die('disabled.'); - 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')); - } } diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php index 75dc522d91..429481333b 100644 --- a/app/Http/Controllers/Admin/NotificationController.php +++ b/app/Http/Controllers/Admin/NotificationController.php @@ -23,9 +23,12 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Admin; +use FireflyIII\Events\Test\TestNotificationChannel; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\NotificationRequest; +use FireflyIII\User; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class NotificationController extends Controller @@ -38,30 +41,69 @@ class NotificationController extends Controller $subTitle = (string) trans('firefly.title_owner_notifications'); $subTitleIcon = 'envelope-o'; $slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data; - $discordUrl = app('fireflyconfig')->get('discord_webhook_url', '')->data; + $discordUrl = app('fireflyconfig')->get('discord_webhook_url', '')->data; $channels = config('notifications.channels'); // admin notification settings: $notifications = []; foreach (config('notifications.notifications.owner') as $key => $info) { - if($info['enabled']) { + if ($info['enabled']) { $notifications[$key] = app('fireflyconfig')->get(sprintf('notification_%s', $key), true)->data; } } - return view('admin.notifications.index', compact('title', 'subTitle', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl','discordUrl','notifications')); + return view('admin.notifications.index', compact('title', 'subTitle', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl', 'discordUrl', 'notifications')); } - public function postIndex(NotificationRequest $request): RedirectResponse { + public function postIndex(NotificationRequest $request): RedirectResponse + { + $all = $request->getAll(); - var_dump($request->getAll()); - exit; - // app('fireflyconfig')->set(sprintf('notification_%s', $key), $value);; + foreach (config('notifications.notifications.owner') as $key => $info) { + if (array_key_exists($key, $all)) { + app('fireflyconfig')->set(sprintf('notification_%s', $key), $all[$key]); + } + } + if ('' === $all['slack_url']) { + app('fireflyconfig')->delete('slack_webhook_url'); + } + if ('' === $all['discord_url']) { + app('fireflyconfig')->delete('discord_webhook_url'); + } + if ('' !== $all['slack_url']) { + app('fireflyconfig')->set('slack_webhook_url', $all['slack_url']); + } + if ('' !== $all['discord_url']) { + app('fireflyconfig')->set('discord_webhook_url', $all['discord_url']); + } - session()->flash('success', (string)trans('firefly.notification_settings_saved')); + session()->flash('success', (string) trans('firefly.notification_settings_saved')); - return redirect(route('admin.index')); + 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 'discord': + case 'slack': + /** @var User $user */ + $user = auth()->user(); + app('log')->debug(sprintf('Now in testNotification("%s") controller.', $channel)); + event(new TestNotificationChannel($channel, $user)); + session()->flash('success', (string) trans('firefly.notification_test_executed', ['channel' => $channel])); + } + + return redirect(route('admin.notification.index')); } } diff --git a/app/Http/Requests/NotificationRequest.php b/app/Http/Requests/NotificationRequest.php index 2f94e047bf..e0a97ac5ce 100644 --- a/app/Http/Requests/NotificationRequest.php +++ b/app/Http/Requests/NotificationRequest.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; +use FireflyIII\Rules\Admin\IsValidDiscordUrl; +use FireflyIII\Rules\Admin\IsValidSlackUrl; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; @@ -42,18 +44,9 @@ class NotificationRequest extends FormRequest } $return[$key] = $value; } - $return['discord_url'] = $this->convertString('discordUrl'); - $return['slack_url'] = $this->convertString('slackUrl'); + $return['discord_url'] = $this->convertString('discord_url'); + $return['slack_url'] = $this->convertString('slack_url'); return $return; -// if (UrlValidator::isValidWebhookURL($url)) { -// app('fireflyconfig')->set('slack_webhook_url', $url); -// } -// } -// -// -// var_dump($this->all()); -// exit; -// return []; } /** @@ -61,10 +54,14 @@ class NotificationRequest extends FormRequest */ public function rules(): array { - // fixed - return [ - //'password' => 'required', + $rules = [ + 'discord_url' => ['nullable', 'url', 'min:1', new IsValidDiscordUrl()], + 'slack_url' => ['nullable', 'url', 'min:1', new IsValidSlackUrl()], ]; + foreach (config('notifications.notifications.owner') as $key => $info) { + $rules[sprintf('notification_%s', $key)] = 'in:0,1'; + } + return $rules; } } diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 87779dd2b4..6cdca7d42e 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -170,15 +170,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/Notifications/Test/TestNotificationDiscord.php b/app/Notifications/Test/TestNotificationDiscord.php new file mode 100644 index 0000000000..603f1f2561 --- /dev/null +++ b/app/Notifications/Test/TestNotificationDiscord.php @@ -0,0 +1,108 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Messages\SlackMessage; +use Illuminate\Notifications\Notification; + +/** + * Class TestNotification + */ +class TestNotificationDiscord 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) + { + } + + /** + * Get the Slack representation of the notification. + * + * @param mixed $notifiable + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + */ + public function toSlack($notifiable) { + + // since it's an admin notificaiton, grab the URL from fireflyconfig + $url = app('fireflyconfig')->get('discord_webhook_url', '')->data; + + return (new SlackMessage()) + ->content((string)trans('email.admin_test_subject')) + ->to($url); + } + + /** + * Get the notification's delivery channels. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param mixed $notifiable + * + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } +} diff --git a/app/Notifications/Test/TestNotificationEmail.php b/app/Notifications/Test/TestNotificationEmail.php new file mode 100644 index 0000000000..d29be7c357 --- /dev/null +++ b/app/Notifications/Test/TestNotificationEmail.php @@ -0,0 +1,102 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Notification; + +/** + * Class TestNotification + */ +class TestNotificationEmail 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) + * + */ + public function toSlack($notifiable) {} + + /** + * Get the notification's delivery channels. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param mixed $notifiable + * + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } +} diff --git a/app/Notifications/Test/TestNotificationSlack.php b/app/Notifications/Test/TestNotificationSlack.php new file mode 100644 index 0000000000..05f035848d --- /dev/null +++ b/app/Notifications/Test/TestNotificationSlack.php @@ -0,0 +1,113 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Notifications\Test; + +use Illuminate\Bus\Queueable; +use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Notifications\Messages\SlackMessage; +use Illuminate\Notifications\Notification; +//use Illuminate\Notifications\Slack\SlackMessage; + +/** + * Class TestNotification + */ +class TestNotificationSlack 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) + { + } + + /** + * Get the Slack representation of the notification. + * + * @param mixed $notifiable + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + */ + public function toSlack($notifiable) { + + // since it's an admin notification, grab the URL from fireflyconfig + $url = app('fireflyconfig')->get('slack_webhook_url', '')->data; + +// return (new SlackMessage) +// ->text((string)trans('email.admin_test_subject')) +// ->to($url); + return (new SlackMessage()) + ->content((string)trans('email.admin_test_subject')) + ->to($url); + + } + + /** + * Get the notification's delivery channels. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param mixed $notifiable + * + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index b89317fede..bc09ed6fef 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -49,6 +49,7 @@ use FireflyIII\Events\Security\MFANewBackupCodes; use FireflyIII\Events\Security\MFAUsedBackupCode; use FireflyIII\Events\StoredAccount; use FireflyIII\Events\StoredTransactionGroup; +use FireflyIII\Events\Test\TestNotificationChannel; use FireflyIII\Events\TriggeredAuditLog; use FireflyIII\Events\UpdatedAccount; use FireflyIII\Events\UpdatedTransactionGroup; @@ -135,8 +136,8 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail', ], // admin related - AdminRequestedTestMessage::class => [ - 'FireflyIII\Handlers\Events\AdminEventHandler@sendTestMessage', + TestNotificationChannel::class => [ + 'FireflyIII\Handlers\Events\AdminEventHandler@sendTestNotification', ], NewVersionAvailable::class => [ 'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion', diff --git a/app/Rules/Admin/IsValidDiscordUrl.php b/app/Rules/Admin/IsValidDiscordUrl.php new file mode 100644 index 0000000000..f0e03c6447 --- /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/IsValidSlackUrl.php b/app/Rules/Admin/IsValidSlackUrl.php new file mode 100644 index 0000000000..57cd1675a1 --- /dev/null +++ b/app/Rules/Admin/IsValidSlackUrl.php @@ -0,0 +1,32 @@ +translate(); + $message = sprintf('IsValidSlackUrl: "%s" is not a discord URL.', substr($value, 0, 255)); + Log::debug($message); + Log::channel('audit')->info($message); + } + } +} diff --git a/app/User.php b/app/User.php index 3e44cb1e43..fa79d3a12e 100644 --- a/app/User.php +++ b/app/User.php @@ -54,6 +54,8 @@ use FireflyIII\Notifications\Admin\TestNotification; use FireflyIII\Notifications\Admin\UserInvitation; use FireflyIII\Notifications\Admin\UserRegistration; use FireflyIII\Notifications\Admin\VersionCheckResult; +use FireflyIII\Notifications\Test\TestNotificationDiscord; +use FireflyIII\Notifications\Test\TestNotificationSlack; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -399,7 +401,7 @@ class User extends Authenticatable /** * 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; @@ -407,9 +409,19 @@ class User extends Authenticatable $res = ''; } $res = (string)$res; - if ($notification instanceof TestNotification) { + + // not the best way to do this, but alas. + + if ($notification instanceof TestNotificationSlack) { return $res; } + if ($notification instanceof TestNotificationDiscord) { + $res = app('fireflyconfig')->get('discord_webhook_url', '')->data; + if (is_array($res)) { + $res = ''; + } + return (string)$res; + } if ($notification instanceof UserInvitation) { return $res; } diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 5ce9ee368b..a427410ada 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -2483,6 +2483,8 @@ return [ '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', diff --git a/resources/views/admin/notifications/index.twig b/resources/views/admin/notifications/index.twig index 52e5085eb1..2b753f9330 100644 --- a/resources/views/admin/notifications/index.twig +++ b/resources/views/admin/notifications/index.twig @@ -24,8 +24,8 @@
    {% endfor %}

    {{ 'channel_settings'|_ }}

    - {{ ExpandedForm.text('slackUrl', slackUrl, {'label' : 'slack_url_label'|_}) }} - {{ ExpandedForm.text('discordUrl', discordUrl, {'label' : 'discord_url_label'|_}) }} + {{ ExpandedForm.text('slack_url', slackUrl, {'label' : 'slack_url_label'|_}) }} + {{ ExpandedForm.text('discord_url', discordUrl, {'label' : 'discord_url_label'|_}) }}
    {% endfor %}

    {{ 'channel_settings'|_ }}

    - {{ ExpandedForm.text('slack_url', slackUrl, {'label' : 'slack_url_label'|_}) }} + {{ 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, {}) }}
    @@ -357,6 +388,9 @@ {% endblock %} {% block scripts %} + diff --git a/routes/web.php b/routes/web.php index 04b98d3afe..659726d09d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -806,6 +806,7 @@ Route::group( static function (): void { Route::get('', ['uses' => 'PreferencesController@index', 'as' => 'index']); Route::post('', ['uses' => 'PreferencesController@postIndex', 'as' => 'update']); + Route::post('test-notification', ['uses' => 'PreferencesController@testNotification', 'as' => 'test-notification']); } ); From fd2c1615cf49e31a97e5faeb7c14b32f12a967b6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 06:57:57 +0100 Subject: [PATCH 026/167] Start cleaning up notifications. --- .../ReturnsAvailableChannels.php | 39 ++++++++++++++++++- .../Security/DisabledMFANotification.php | 35 +++++++---------- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/app/Notifications/ReturnsAvailableChannels.php b/app/Notifications/ReturnsAvailableChannels.php index c63678a551..56e6de1b0e 100644 --- a/app/Notifications/ReturnsAvailableChannels.php +++ b/app/Notifications/ReturnsAvailableChannels.php @@ -25,20 +25,23 @@ declare(strict_types=1); namespace FireflyIII\Notifications; use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\User; use Illuminate\Support\Facades\Log; use NotificationChannels\Pushover\PushoverChannel; use Wijourdil\NtfyNotificationChannel\Channels\NtfyChannel; class ReturnsAvailableChannels { - public static function returnChannels(string $type): array + public static function returnChannels(string $type, ?User $user = null): array { $channels = ['mail']; - if ('owner' === $type) { return self::returnOwnerChannels(); } + if('user' === $type && null !== $user) { + return self::returnUserChannels($user); + } return $channels; @@ -75,6 +78,38 @@ class ReturnsAvailableChannels 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/Security/DisabledMFANotification.php b/app/Notifications/Security/DisabledMFANotification.php index 2676f3d5b9..96022b12c8 100644 --- a/app/Notifications/Security/DisabledMFANotification.php +++ b/app/Notifications/Security/DisabledMFANotification.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\Support\Notifications\UrlValidator; use FireflyIII\User; use Illuminate\Bus\Queueable; @@ -35,7 +36,7 @@ class DisabledMFANotification extends Notification { use Queueable; - private User $user; + private User $user; /** * Create a new notification instance. @@ -48,13 +49,13 @@ class DisabledMFANotification extends Notification /** * Get the array representation of the notification. * - * @param mixed $notifiable + * @param User $notifiable * * @return array * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; @@ -63,15 +64,15 @@ class DisabledMFANotification extends Notification /** * Get the mail representation of the notification. * - * @param mixed $notifiable + * @param User $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'); return (new MailMessage())->markdown('emails.security.disabled-mfa', ['user' => $this->user])->subject($subject); } @@ -79,15 +80,15 @@ class DisabledMFANotification extends Notification /** * Get the Slack representation of the notification. * - * @param mixed $notifiable + * @param User $notifiable * * @return SlackMessage * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack($notifiable) + public function toSlack(User $notifiable) { - $message = (string)trans('email.disabled_mfa_slack', ['email' => $this->user->email]); + $message = (string) trans('email.disabled_mfa_slack', ['email' => $this->user->email]); return (new SlackMessage())->content($message); } @@ -95,24 +96,14 @@ class DisabledMFANotification extends Notification /** * Get the notification's delivery channels. * - * @param mixed $notifiable + * @param User $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); } } From 8030167ffc3e037aa29fd387e51d4a085c964b8f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 07:05:08 +0100 Subject: [PATCH 027/167] Clean up notifications, fix first user notification. --- .../Admin/UnknownUserLoginAttempt.php | 12 ---------- app/Notifications/Admin/UserInvitation.php | 12 ---------- app/Notifications/Admin/UserRegistration.php | 12 ---------- .../Admin/VersionCheckResult.php | 12 ---------- app/Notifications/ReturnsSettings.php | 11 ++++++++++ .../Security/DisabledMFANotification.php | 22 +++++++++++++++++++ .../Test/OwnerTestNotificationNtfy.php | 12 ---------- .../Test/UserTestNotificationNtfy.php | 12 ---------- 8 files changed, 33 insertions(+), 72 deletions(-) diff --git a/app/Notifications/Admin/UnknownUserLoginAttempt.php b/app/Notifications/Admin/UnknownUserLoginAttempt.php index d402830e2b..4632a06e7e 100644 --- a/app/Notifications/Admin/UnknownUserLoginAttempt.php +++ b/app/Notifications/Admin/UnknownUserLoginAttempt.php @@ -97,18 +97,6 @@ class UnknownUserLoginAttempt extends Notification public function toNtfy(OwnerNotifiable $notifiable): Message { $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); - - // 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']]); - } - $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title((string) trans('email.unknown_user_subject')); diff --git a/app/Notifications/Admin/UserInvitation.php b/app/Notifications/Admin/UserInvitation.php index 5696f41247..761d410f4c 100644 --- a/app/Notifications/Admin/UserInvitation.php +++ b/app/Notifications/Admin/UserInvitation.php @@ -116,18 +116,6 @@ class UserInvitation extends Notification { Log::debug('Now in toNtfy() for UserInvitation'); $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); - - // 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']]); - } - $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title((string) trans('email.invitation_created_subject')); diff --git a/app/Notifications/Admin/UserRegistration.php b/app/Notifications/Admin/UserRegistration.php index 71606cb5ba..5745ca9e6d 100644 --- a/app/Notifications/Admin/UserRegistration.php +++ b/app/Notifications/Admin/UserRegistration.php @@ -110,18 +110,6 @@ class UserRegistration extends Notification { Log::debug('Now in toNtfy() for (Admin) UserRegistration'); $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); - - // 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']]); - } - $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title((string) trans('email.registered_subject_admin')); diff --git a/app/Notifications/Admin/VersionCheckResult.php b/app/Notifications/Admin/VersionCheckResult.php index a9a347e794..001c9c838f 100644 --- a/app/Notifications/Admin/VersionCheckResult.php +++ b/app/Notifications/Admin/VersionCheckResult.php @@ -115,18 +115,6 @@ class VersionCheckResult extends Notification { Log::debug('Now in toNtfy() for VersionCheckResult'); $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); - - // 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']]); - } - $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title((string) trans('email.new_version_email_subject')); diff --git a/app/Notifications/ReturnsSettings.php b/app/Notifications/ReturnsSettings.php index 59f8d8b3fb..6dd2a365c6 100644 --- a/app/Notifications/ReturnsSettings.php +++ b/app/Notifications/ReturnsSettings.php @@ -65,6 +65,17 @@ class ReturnsSettings $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 96022b12c8..f0efecf0cb 100644 --- a/app/Notifications/Security/DisabledMFANotification.php +++ b/app/Notifications/Security/DisabledMFANotification.php @@ -25,12 +25,16 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\Notifications\ReturnsSettings; use FireflyIII\Support\Notifications\UrlValidator; 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 NotificationChannels\Pushover\PushoverMessage; +use Ntfy\Message; class DisabledMFANotification extends Notification { @@ -93,6 +97,24 @@ class DisabledMFANotification extends Notification return (new SlackMessage())->content($message); } + public function toPushover(User $notifiable): PushoverMessage + { + Log::debug('Now in (user) toPushover()'); + + return PushoverMessage::create((string) trans('email.disabled_mfa_slack', ['email' => $this->user->email])) + ->title((string)trans('email.disabled_mfa_subject')); + } + 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.disabled_mfa_subject')); + $message->body((string) trans('email.disabled_mfa_slack', ['email' => $this->user->email])); + + return $message; + } + /** * Get the notification's delivery channels. * diff --git a/app/Notifications/Test/OwnerTestNotificationNtfy.php b/app/Notifications/Test/OwnerTestNotificationNtfy.php index 4f57590dc2..d06594d542 100644 --- a/app/Notifications/Test/OwnerTestNotificationNtfy.php +++ b/app/Notifications/Test/OwnerTestNotificationNtfy.php @@ -68,18 +68,6 @@ class OwnerTestNotificationNtfy extends Notification public function toNtfy(OwnerNotifiable $notifiable): Message { $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); - - // 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']]); - } - $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title((string) trans('email.admin_test_subject')); diff --git a/app/Notifications/Test/UserTestNotificationNtfy.php b/app/Notifications/Test/UserTestNotificationNtfy.php index 15e31fdafd..73877acd4a 100644 --- a/app/Notifications/Test/UserTestNotificationNtfy.php +++ b/app/Notifications/Test/UserTestNotificationNtfy.php @@ -68,18 +68,6 @@ class UserTestNotificationNtfy extends Notification public function toNtfy(User $user): Message { $settings = ReturnsSettings::getSettings('ntfy', 'user', $user); - - // 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']]); - } - $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title((string) trans('email.admin_test_subject')); From b3560ff52541caec65ac919baa0d612af263e74e Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 07:13:01 +0100 Subject: [PATCH 028/167] Clean up notifications --- app/Events/Security/UserAttemptedLogin.php | 42 ++++++++++ app/Handlers/Events/UserEventHandler.php | 21 +++++ app/Http/Controllers/Auth/LoginController.php | 6 +- .../Admin/UnknownUserLoginAttempt.php | 21 ++--- app/Notifications/Admin/UserInvitation.php | 36 ++------ app/Notifications/Admin/UserRegistration.php | 32 ++------ .../Admin/VersionCheckResult.php | 36 ++------ .../Security/DisabledMFANotification.php | 36 ++------ .../Security/EnabledMFANotification.php | 44 ++-------- .../Security/MFABackupFewLeftNotification.php | 44 ++-------- .../Security/MFABackupNoLeftNotification.php | 44 ++-------- .../MFAManyFailedAttemptsNotification.php | 44 ++-------- .../MFAUsedBackupCodeNotification.php | 44 ++-------- .../Security/NewBackupCodesNotification.php | 44 ++-------- .../Security/UserFailedLoginAttempt.php | 82 +++++++++++++++++++ .../Test/OwnerTestNotificationEmail.php | 4 +- .../Test/OwnerTestNotificationNtfy.php | 4 +- .../Test/OwnerTestNotificationPushover.php | 4 +- .../Test/OwnerTestNotificationSlack.php | 4 +- .../Test/UserTestNotificationEmail.php | 4 +- .../Test/UserTestNotificationNtfy.php | 4 +- .../Test/UserTestNotificationPushover.php | 4 +- .../Test/UserTestNotificationSlack.php | 4 +- app/Notifications/User/BillReminder.php | 44 ++-------- app/Notifications/User/NewAccessToken.php | 44 ++-------- app/Notifications/User/RuleActionFailed.php | 34 +------- .../User/TransactionCreation.php | 34 +------- app/Notifications/User/UserLogin.php | 44 ++-------- app/Notifications/User/UserNewPassword.php | 34 +------- app/Notifications/User/UserRegistration.php | 34 +------- app/Providers/EventServiceProvider.php | 4 + 31 files changed, 256 insertions(+), 624 deletions(-) create mode 100644 app/Events/Security/UserAttemptedLogin.php create mode 100644 app/Notifications/Security/UserFailedLoginAttempt.php diff --git a/app/Events/Security/UserAttemptedLogin.php b/app/Events/Security/UserAttemptedLogin.php new file mode 100644 index 0000000000..8457a5ecc3 --- /dev/null +++ b/app/Events/Security/UserAttemptedLogin.php @@ -0,0 +1,42 @@ +user = $user; + } + } +} diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index aaac2ca8d5..91b63b4e18 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -31,6 +31,7 @@ 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\OwnerTestNotificationChannel; use FireflyIII\Events\Test\UserTestNotificationChannel; use FireflyIII\Events\UserChangedEmail; @@ -435,6 +436,26 @@ 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()); + } + } + /** * Sends a test message to an administrator. */ diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 9ec43cb780..6558e626de 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -127,10 +127,14 @@ class LoginController extends Controller } app('log')->warning('Login attempt failed.'); $username = (string) $request->get($this->username()); - if (null === $this->repository->findByEmail($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 diff --git a/app/Notifications/Admin/UnknownUserLoginAttempt.php b/app/Notifications/Admin/UnknownUserLoginAttempt.php index 4632a06e7e..6ee20a56e9 100644 --- a/app/Notifications/Admin/UnknownUserLoginAttempt.php +++ b/app/Notifications/Admin/UnknownUserLoginAttempt.php @@ -39,21 +39,12 @@ class UnknownUserLoginAttempt 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 - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toArray(OwnerNotifiable $notifiable) @@ -63,8 +54,6 @@ class UnknownUserLoginAttempt extends Notification } /** - * Get the mail representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toMail(OwnerNotifiable $notifiable): MailMessage @@ -76,8 +65,6 @@ class UnknownUserLoginAttempt extends Notification } /** - * Get the Slack representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toSlack(OwnerNotifiable $notifiable): SlackMessage @@ -87,6 +74,9 @@ class UnknownUserLoginAttempt extends Notification ); } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toPushover(OwnerNotifiable $notifiable): PushoverMessage { return PushoverMessage::create((string) trans('email.unknown_user_message', ['address' => $this->address])) @@ -94,6 +84,9 @@ class UnknownUserLoginAttempt extends Notification ; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toNtfy(OwnerNotifiable $notifiable): Message { $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); @@ -106,8 +99,6 @@ class UnknownUserLoginAttempt extends Notification } /** - * Get the notification's delivery channels. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function via(OwnerNotifiable $notifiable) diff --git a/app/Notifications/Admin/UserInvitation.php b/app/Notifications/Admin/UserInvitation.php index 761d410f4c..a9d40ab9b0 100644 --- a/app/Notifications/Admin/UserInvitation.php +++ b/app/Notifications/Admin/UserInvitation.php @@ -46,9 +46,7 @@ class UserInvitation extends Notification private InvitedUser $invitee; private OwnerNotifiable $owner; - /** - * Create a new notification instance. - */ + public function __construct(OwnerNotifiable $owner, InvitedUser $invitee) { $this->invitee = $invitee; @@ -56,12 +54,6 @@ class UserInvitation extends Notification } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toArray(OwnerNotifiable $notifiable) @@ -71,12 +63,6 @@ class UserInvitation extends Notification } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toMail(OwnerNotifiable $notifiable) @@ -88,12 +74,6 @@ class UserInvitation extends Notification } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toSlack(OwnerNotifiable $notifiable) @@ -102,7 +82,9 @@ class UserInvitation extends Notification (string) trans('email.invitation_created_body', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email]) ); } - + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toPushover(OwnerNotifiable $notifiable): PushoverMessage { Log::debug('Now in toPushover() for UserInvitation'); @@ -111,7 +93,9 @@ class UserInvitation extends Notification ->title((string) trans('email.invitation_created_subject')) ; } - + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toNtfy(OwnerNotifiable $notifiable): Message { Log::debug('Now in toNtfy() for UserInvitation'); @@ -125,12 +109,6 @@ class UserInvitation extends Notification } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function via(OwnerNotifiable $notifiable) diff --git a/app/Notifications/Admin/UserRegistration.php b/app/Notifications/Admin/UserRegistration.php index 5745ca9e6d..e28e1ca239 100644 --- a/app/Notifications/Admin/UserRegistration.php +++ b/app/Notifications/Admin/UserRegistration.php @@ -46,9 +46,7 @@ class UserRegistration extends Notification private OwnerNotifiable $owner; private User $user; - /** - * Create a new notification instance. - */ + public function __construct(OwnerNotifiable $owner, User $user) { $this->user = $user; @@ -56,8 +54,6 @@ class UserRegistration extends Notification } /** - * Get the array representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toArray(OwnerNotifiable $notifiable) @@ -67,12 +63,6 @@ class UserRegistration extends Notification } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toMail(OwnerNotifiable $notifiable) @@ -84,19 +74,15 @@ class UserRegistration extends Notification } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @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 toPushover(OwnerNotifiable $notifiable): PushoverMessage { Log::debug('Now in toPushover() for UserRegistration'); @@ -105,7 +91,9 @@ class UserRegistration extends Notification ->title((string) trans('email.registered_subject_admin')) ; } - + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toNtfy(OwnerNotifiable $notifiable): Message { Log::debug('Now in toNtfy() for (Admin) UserRegistration'); @@ -119,12 +107,6 @@ class UserRegistration extends Notification } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function via(OwnerNotifiable $notifiable) diff --git a/app/Notifications/Admin/VersionCheckResult.php b/app/Notifications/Admin/VersionCheckResult.php index 001c9c838f..68e28475d9 100644 --- a/app/Notifications/Admin/VersionCheckResult.php +++ b/app/Notifications/Admin/VersionCheckResult.php @@ -44,21 +44,13 @@ 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(OwnerNotifiable $notifiable) @@ -68,12 +60,6 @@ class VersionCheckResult extends Notification } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toMail(OwnerNotifiable $notifiable) @@ -85,12 +71,6 @@ class VersionCheckResult extends Notification } /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toSlack(OwnerNotifiable $notifiable) @@ -101,7 +81,9 @@ class VersionCheckResult extends Notification }) ; } - + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toPushover(OwnerNotifiable $notifiable): PushoverMessage { Log::debug('Now in toPushover() for VersionCheckResult'); @@ -110,7 +92,9 @@ class VersionCheckResult extends Notification ->title((string) trans('email.new_version_email_subject')) ; } - + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toNtfy(OwnerNotifiable $notifiable): Message { Log::debug('Now in toNtfy() for VersionCheckResult'); @@ -124,12 +108,6 @@ class VersionCheckResult extends Notification } /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function via(OwnerNotifiable $notifiable) diff --git a/app/Notifications/Security/DisabledMFANotification.php b/app/Notifications/Security/DisabledMFANotification.php index f0efecf0cb..48dc9462a8 100644 --- a/app/Notifications/Security/DisabledMFANotification.php +++ b/app/Notifications/Security/DisabledMFANotification.php @@ -42,21 +42,11 @@ class DisabledMFANotification extends Notification private User $user; - /** - * Create a new notification instance. - */ public function __construct(User $user) { $this->user = $user; } - /** - * Get the array representation of the notification. - * - * @param User $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toArray(User $notifiable) @@ -66,12 +56,6 @@ class DisabledMFANotification extends Notification } /** - * Get the mail representation of the notification. - * - * @param User $notifiable - * - * @return MailMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toMail(User $notifiable) @@ -80,14 +64,7 @@ class DisabledMFANotification extends Notification return (new MailMessage())->markdown('emails.security.disabled-mfa', ['user' => $this->user])->subject($subject); } - /** - * Get the Slack representation of the notification. - * - * @param User $notifiable - * - * @return SlackMessage - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toSlack(User $notifiable) @@ -96,7 +73,9 @@ class DisabledMFANotification extends Notification return (new SlackMessage())->content($message); } - + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toPushover(User $notifiable): PushoverMessage { Log::debug('Now in (user) toPushover()'); @@ -104,6 +83,9 @@ class DisabledMFANotification extends Notification return PushoverMessage::create((string) trans('email.disabled_mfa_slack', ['email' => $this->user->email])) ->title((string)trans('email.disabled_mfa_subject')); } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toNtfy(User $user): Message { $settings = ReturnsSettings::getSettings('ntfy', 'user', $user); @@ -116,12 +98,6 @@ class DisabledMFANotification extends Notification } /** - * Get the notification's delivery channels. - * - * @param User $notifiable - * - * @return array - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function via(User $notifiable) diff --git a/app/Notifications/Security/EnabledMFANotification.php b/app/Notifications/Security/EnabledMFANotification.php index cfe5cf0e6d..fc927be354 100644 --- a/app/Notifications/Security/EnabledMFANotification.php +++ b/app/Notifications/Security/EnabledMFANotification.php @@ -37,38 +37,20 @@ class EnabledMFANotification extends Notification 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $subject = (string)trans('email.enabled_mfa_subject'); @@ -76,15 +58,7 @@ class EnabledMFANotification extends Notification return (new MailMessage())->markdown('emails.security.enabled-mfa', ['user' => $this->user])->subject($subject); } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $message = (string)trans('email.enabled_mfa_slack', ['email' => $this->user->email]); @@ -92,15 +66,7 @@ class EnabledMFANotification extends Notification return (new SlackMessage())->content($message); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/Security/MFABackupFewLeftNotification.php b/app/Notifications/Security/MFABackupFewLeftNotification.php index d01925686f..c4e7209bb6 100644 --- a/app/Notifications/Security/MFABackupFewLeftNotification.php +++ b/app/Notifications/Security/MFABackupFewLeftNotification.php @@ -38,39 +38,21 @@ class MFABackupFewLeftNotification extends Notification private User $user; private int $count; - /** - * 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $subject = (string)trans('email.mfa_few_backups_left_subject', ['count' => $this->count]); @@ -78,15 +60,7 @@ class MFABackupFewLeftNotification extends Notification return (new MailMessage())->markdown('emails.security.few-backup-codes', ['user' => $this->user, 'count' => $this->count])->subject($subject); } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $message = (string)trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count]); @@ -94,15 +68,7 @@ class MFABackupFewLeftNotification extends Notification return (new SlackMessage())->content($message); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/Security/MFABackupNoLeftNotification.php b/app/Notifications/Security/MFABackupNoLeftNotification.php index 91a98c1101..5e0199c079 100644 --- a/app/Notifications/Security/MFABackupNoLeftNotification.php +++ b/app/Notifications/Security/MFABackupNoLeftNotification.php @@ -37,38 +37,20 @@ class MFABackupNoLeftNotification extends Notification 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $subject = (string)trans('email.mfa_no_backups_left_subject'); @@ -76,15 +58,7 @@ class MFABackupNoLeftNotification extends Notification return (new MailMessage())->markdown('emails.security.no-backup-codes', ['user' => $this->user])->subject($subject); } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $message = (string)trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email]); @@ -92,15 +66,7 @@ class MFABackupNoLeftNotification extends Notification return (new SlackMessage())->content($message); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/Security/MFAManyFailedAttemptsNotification.php b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php index b8926e58e8..d7d78863fe 100644 --- a/app/Notifications/Security/MFAManyFailedAttemptsNotification.php +++ b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php @@ -38,39 +38,21 @@ class MFAManyFailedAttemptsNotification extends Notification private User $user; private int $count; - /** - * 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $subject = (string)trans('email.mfa_many_failed_subject', ['count' => $this->count]); @@ -78,15 +60,7 @@ class MFAManyFailedAttemptsNotification extends Notification return (new MailMessage())->markdown('emails.security.many-failed-attempts', ['user' => $this->user, 'count' => $this->count])->subject($subject); } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $message = (string)trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]); @@ -94,15 +68,7 @@ class MFAManyFailedAttemptsNotification extends Notification return (new SlackMessage())->content($message); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/Security/MFAUsedBackupCodeNotification.php b/app/Notifications/Security/MFAUsedBackupCodeNotification.php index 2179dd2357..5aac05099d 100644 --- a/app/Notifications/Security/MFAUsedBackupCodeNotification.php +++ b/app/Notifications/Security/MFAUsedBackupCodeNotification.php @@ -37,38 +37,20 @@ class MFAUsedBackupCodeNotification extends Notification 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $subject = (string)trans('email.used_backup_code_subject'); @@ -76,15 +58,7 @@ class MFAUsedBackupCodeNotification extends Notification return (new MailMessage())->markdown('emails.security.used-backup-code', ['user' => $this->user])->subject($subject); } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $message = (string)trans('email.used_backup_code_slack', ['email' => $this->user->email]); @@ -92,15 +66,7 @@ class MFAUsedBackupCodeNotification extends Notification return (new SlackMessage())->content($message); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/Security/NewBackupCodesNotification.php b/app/Notifications/Security/NewBackupCodesNotification.php index eeaac3b0ac..8973242e02 100644 --- a/app/Notifications/Security/NewBackupCodesNotification.php +++ b/app/Notifications/Security/NewBackupCodesNotification.php @@ -37,38 +37,20 @@ class NewBackupCodesNotification extends Notification 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $subject = (string)trans('email.new_backup_codes_subject'); @@ -76,15 +58,7 @@ class NewBackupCodesNotification extends Notification return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user])->subject($subject); } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $message = (string)trans('email.new_backup_codes_slack', ['email' => $this->user->email]); @@ -92,15 +66,7 @@ class NewBackupCodesNotification extends Notification return (new SlackMessage())->content($message); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/Security/UserFailedLoginAttempt.php b/app/Notifications/Security/UserFailedLoginAttempt.php new file mode 100644 index 0000000000..51a4462dc7 --- /dev/null +++ b/app/Notifications/Security/UserFailedLoginAttempt.php @@ -0,0 +1,82 @@ +user = $user; + } + + + public function toArray($notifiable) + { + return [ + ]; + } + + + public function toMail($notifiable) + { + $subject = (string)trans('email.new_backup_codes_subject'); + + return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user])->subject($subject); + } + + + public function toSlack($notifiable) + { + $message = (string)trans('email.new_backup_codes_slack', ['email' => $this->user->email]); + + return (new SlackMessage())->content($message); + } + + + public function via($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']; + } +} diff --git a/app/Notifications/Test/OwnerTestNotificationEmail.php b/app/Notifications/Test/OwnerTestNotificationEmail.php index 9ac18ed14b..ed74b12537 100644 --- a/app/Notifications/Test/OwnerTestNotificationEmail.php +++ b/app/Notifications/Test/OwnerTestNotificationEmail.php @@ -38,9 +38,7 @@ class OwnerTestNotificationEmail extends Notification private OwnerNotifiable $owner; - /** - * Create a new notification instance. - */ + public function __construct(OwnerNotifiable $owner) { $this->owner = $owner; diff --git a/app/Notifications/Test/OwnerTestNotificationNtfy.php b/app/Notifications/Test/OwnerTestNotificationNtfy.php index d06594d542..d6ec326625 100644 --- a/app/Notifications/Test/OwnerTestNotificationNtfy.php +++ b/app/Notifications/Test/OwnerTestNotificationNtfy.php @@ -42,9 +42,7 @@ class OwnerTestNotificationNtfy extends Notification public OwnerNotifiable $owner; - /** - * Create a new notification instance. - */ + public function __construct(OwnerNotifiable $owner) { $this->owner = $owner; diff --git a/app/Notifications/Test/OwnerTestNotificationPushover.php b/app/Notifications/Test/OwnerTestNotificationPushover.php index 4d71f095ab..f5fdf1a032 100644 --- a/app/Notifications/Test/OwnerTestNotificationPushover.php +++ b/app/Notifications/Test/OwnerTestNotificationPushover.php @@ -42,9 +42,7 @@ class OwnerTestNotificationPushover extends Notification private OwnerNotifiable $owner; - /** - * Create a new notification instance. - */ + public function __construct(OwnerNotifiable $owner) { $this->owner = $owner; diff --git a/app/Notifications/Test/OwnerTestNotificationSlack.php b/app/Notifications/Test/OwnerTestNotificationSlack.php index 55dce8f62a..ea3dbab6d7 100644 --- a/app/Notifications/Test/OwnerTestNotificationSlack.php +++ b/app/Notifications/Test/OwnerTestNotificationSlack.php @@ -40,9 +40,7 @@ class OwnerTestNotificationSlack extends Notification private OwnerNotifiable $owner; - /** - * Create a new notification instance. - */ + public function __construct(OwnerNotifiable $owner) { $this->owner = $owner; diff --git a/app/Notifications/Test/UserTestNotificationEmail.php b/app/Notifications/Test/UserTestNotificationEmail.php index d617756849..25463f8789 100644 --- a/app/Notifications/Test/UserTestNotificationEmail.php +++ b/app/Notifications/Test/UserTestNotificationEmail.php @@ -39,9 +39,7 @@ class UserTestNotificationEmail extends Notification private User $user; - /** - * Create a new notification instance. - */ + public function __construct(User $user) { $this->user = $user; diff --git a/app/Notifications/Test/UserTestNotificationNtfy.php b/app/Notifications/Test/UserTestNotificationNtfy.php index 73877acd4a..5891701ed2 100644 --- a/app/Notifications/Test/UserTestNotificationNtfy.php +++ b/app/Notifications/Test/UserTestNotificationNtfy.php @@ -42,9 +42,7 @@ class UserTestNotificationNtfy extends Notification public User $user; - /** - * Create a new notification instance. - */ + public function __construct(User $user) { $this->user = $user; diff --git a/app/Notifications/Test/UserTestNotificationPushover.php b/app/Notifications/Test/UserTestNotificationPushover.php index 3acb2cf8aa..d694ae00ed 100644 --- a/app/Notifications/Test/UserTestNotificationPushover.php +++ b/app/Notifications/Test/UserTestNotificationPushover.php @@ -43,9 +43,7 @@ class UserTestNotificationPushover extends Notification private User $user; - /** - * Create a new notification instance. - */ + public function __construct(User $user) { $this->user = $user; diff --git a/app/Notifications/Test/UserTestNotificationSlack.php b/app/Notifications/Test/UserTestNotificationSlack.php index 759c06a318..0d935bccce 100644 --- a/app/Notifications/Test/UserTestNotificationSlack.php +++ b/app/Notifications/Test/UserTestNotificationSlack.php @@ -40,9 +40,7 @@ class UserTestNotificationSlack extends Notification private OwnerNotifiable $owner; - /** - * Create a new notification instance. - */ + public function __construct(OwnerNotifiable $owner) { $this->owner = $owner; diff --git a/app/Notifications/User/BillReminder.php b/app/Notifications/User/BillReminder.php index 36d93b84e6..5b2cb849b9 100644 --- a/app/Notifications/User/BillReminder.php +++ b/app/Notifications/User/BillReminder.php @@ -43,9 +43,7 @@ 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; @@ -53,30 +51,14 @@ class BillReminder extends Notification $this->diff = $diff; } - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toArray($notifiable) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $subject = (string)trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); @@ -90,15 +72,7 @@ class BillReminder extends Notification ; } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $message = (string)trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); @@ -117,15 +91,7 @@ class BillReminder extends Notification ; } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/User/NewAccessToken.php b/app/Notifications/User/NewAccessToken.php index b32192d089..126bc2f8c7 100644 --- a/app/Notifications/User/NewAccessToken.php +++ b/app/Notifications/User/NewAccessToken.php @@ -38,35 +38,17 @@ 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { return (new MailMessage()) @@ -75,29 +57,13 @@ class NewAccessToken extends Notification ; } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { 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) { /** @var null|User $user */ diff --git a/app/Notifications/User/RuleActionFailed.php b/app/Notifications/User/RuleActionFailed.php index 669d8b8e7b..35ea148b1e 100644 --- a/app/Notifications/User/RuleActionFailed.php +++ b/app/Notifications/User/RuleActionFailed.php @@ -43,9 +43,7 @@ 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; @@ -56,30 +54,14 @@ class RuleActionFailed extends Notification $this->ruleLink = $ruleLink; } - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toArray($notifiable) { return [ ]; } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $groupTitle = $this->groupTitle; @@ -94,15 +76,7 @@ class RuleActionFailed extends Notification }); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/User/TransactionCreation.php b/app/Notifications/User/TransactionCreation.php index fcf9f95fc0..61e2626d8a 100644 --- a/app/Notifications/User/TransactionCreation.php +++ b/app/Notifications/User/TransactionCreation.php @@ -37,38 +37,20 @@ 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { return (new MailMessage()) @@ -77,15 +59,7 @@ class TransactionCreation extends Notification ; } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { return ['mail']; diff --git a/app/Notifications/User/UserLogin.php b/app/Notifications/User/UserLogin.php index e7fc4e8a53..3107748de0 100644 --- a/app/Notifications/User/UserLogin.php +++ b/app/Notifications/User/UserLogin.php @@ -41,38 +41,20 @@ class UserLogin extends Notification 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { $time = now(config('app.timezone'))->isoFormat((string)trans('config.date_time_js')); @@ -94,15 +76,7 @@ class UserLogin extends Notification ; } - /** - * Get the Slack representation of the notification. - * - * @param mixed $notifiable - * - * @return SlackMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toSlack($notifiable) { $host = ''; @@ -120,15 +94,7 @@ class UserLogin extends Notification return (new SlackMessage())->content((string)trans('email.slack_login_from_new_ip', ['host' => $host, 'ip' => $this->ip])); } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { /** @var null|User $user */ diff --git a/app/Notifications/User/UserNewPassword.php b/app/Notifications/User/UserNewPassword.php index e4090984d5..e75861add3 100644 --- a/app/Notifications/User/UserNewPassword.php +++ b/app/Notifications/User/UserNewPassword.php @@ -37,38 +37,20 @@ 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { return (new MailMessage()) @@ -77,15 +59,7 @@ class UserNewPassword extends Notification ; } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { return ['mail']; diff --git a/app/Notifications/User/UserRegistration.php b/app/Notifications/User/UserRegistration.php index 50aae568a3..bde33969f6 100644 --- a/app/Notifications/User/UserRegistration.php +++ b/app/Notifications/User/UserRegistration.php @@ -35,35 +35,17 @@ 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) { return [ ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @return MailMessage - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function toMail($notifiable) { return (new MailMessage()) @@ -72,15 +54,7 @@ class UserRegistration extends Notification ; } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * - * @return array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ + public function via($notifiable) { return ['mail']; diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index d6931d1da6..9ce9cef6b0 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -47,6 +47,7 @@ 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; @@ -109,6 +110,9 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership', 'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates', ], + UserAttemptedLogin::class => [ + 'FireflyIII\Handlers\Events\UserEventHandler@sendLoginAttemptNotification', + ], // is a User related event. Login::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin', From 5f1502eea78ce8a64008fe2e2b035472c825e87a Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 07:22:46 +0100 Subject: [PATCH 029/167] Clean up code. --- .../Admin/UnknownUserLoginAttempt.php | 43 +++++++------- app/Notifications/Admin/UserInvitation.php | 44 +++++++------- app/Notifications/Admin/UserRegistration.php | 40 ++++++------- .../Admin/VersionCheckResult.php | 47 ++++++++------- .../Notifiables/OwnerNotifiable.php | 43 +++++++------- .../ReturnsAvailableChannels.php | 17 +++--- app/Notifications/ReturnsSettings.php | 2 +- .../Security/DisabledMFANotification.php | 27 +++++---- .../Security/EnabledMFANotification.php | 43 +++++++------- .../Security/MFABackupFewLeftNotification.php | 46 +++++++-------- .../Security/MFABackupNoLeftNotification.php | 43 +++++++------- .../MFAManyFailedAttemptsNotification.php | 42 +++++++------- .../MFAUsedBackupCodeNotification.php | 43 +++++++------- .../Security/NewBackupCodesNotification.php | 44 +++++++------- .../Security/UserFailedLoginAttempt.php | 41 +++++++------ .../Test/OwnerTestNotificationEmail.php | 19 +------ .../Test/OwnerTestNotificationNtfy.php | 9 +-- .../Test/OwnerTestNotificationPushover.php | 12 ++-- .../Test/OwnerTestNotificationSlack.php | 10 ---- .../Test/UserTestNotificationEmail.php | 24 +------- .../Test/UserTestNotificationNtfy.php | 11 ++-- .../Test/UserTestNotificationPushover.php | 13 ++--- .../Test/UserTestNotificationSlack.php | 14 +---- app/Notifications/User/BillReminder.php | 57 +++++++++---------- app/Notifications/User/NewAccessToken.php | 39 ++++++------- app/Notifications/User/RuleActionFailed.php | 49 +++++++--------- .../User/TransactionCreation.php | 24 +++++--- app/Notifications/User/UserLogin.php | 41 ++++++------- app/Notifications/User/UserNewPassword.php | 25 +++++--- app/Notifications/User/UserRegistration.php | 25 +++++--- 30 files changed, 432 insertions(+), 505 deletions(-) diff --git a/app/Notifications/Admin/UnknownUserLoginAttempt.php b/app/Notifications/Admin/UnknownUserLoginAttempt.php index 6ee20a56e9..e4eb4a678b 100644 --- a/app/Notifications/Admin/UnknownUserLoginAttempt.php +++ b/app/Notifications/Admin/UnknownUserLoginAttempt.php @@ -37,6 +37,7 @@ use Ntfy\Message; class UnknownUserLoginAttempt extends Notification { use Queueable; + private string $address; public function __construct(string $address) @@ -60,28 +61,7 @@ class UnknownUserLoginAttempt extends Notification { return new MailMessage() ->markdown('emails.owner.unknown-user', ['address' => $this->address]) - ->subject((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 toPushover(OwnerNotifiable $notifiable): PushoverMessage - { - return PushoverMessage::create((string) trans('email.unknown_user_message', ['address' => $this->address])) - ->title((string) trans('email.unknown_user_subject')) - ; + ->subject((string) trans('email.unknown_user_subject')); } /** @@ -98,6 +78,25 @@ class UnknownUserLoginAttempt extends Notification 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) */ diff --git a/app/Notifications/Admin/UserInvitation.php b/app/Notifications/Admin/UserInvitation.php index a9d40ab9b0..8ac48334aa 100644 --- a/app/Notifications/Admin/UserInvitation.php +++ b/app/Notifications/Admin/UserInvitation.php @@ -69,30 +69,9 @@ class UserInvitation extends Notification { return (new MailMessage()) ->markdown('emails.invitation-created', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email]) - ->subject((string) trans('email.invitation_created_subject')) - ; + ->subject((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]) - ); - } - /** - * @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) */ @@ -108,6 +87,27 @@ class UserInvitation extends Notification 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]) + ); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/app/Notifications/Admin/UserRegistration.php b/app/Notifications/Admin/UserRegistration.php index e28e1ca239..90af37ff63 100644 --- a/app/Notifications/Admin/UserRegistration.php +++ b/app/Notifications/Admin/UserRegistration.php @@ -69,28 +69,9 @@ class UserRegistration extends Notification { return (new MailMessage()) ->markdown('emails.registered-admin', ['email' => $this->user->email, 'id' => $this->user->id]) - ->subject((string) trans('email.registered_subject_admin')) - ; + ->subject((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 toPushover(OwnerNotifiable $notifiable): PushoverMessage - { - Log::debug('Now in toPushover() for UserRegistration'); - - 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) */ @@ -106,6 +87,25 @@ class UserRegistration extends Notification return $message; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(OwnerNotifiable $notifiable): PushoverMessage + { + Log::debug('Now in toPushover() for UserRegistration'); + + 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) */ diff --git a/app/Notifications/Admin/VersionCheckResult.php b/app/Notifications/Admin/VersionCheckResult.php index 68e28475d9..d809913e74 100644 --- a/app/Notifications/Admin/VersionCheckResult.php +++ b/app/Notifications/Admin/VersionCheckResult.php @@ -66,32 +66,9 @@ class VersionCheckResult extends Notification { 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')); } - /** - * @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'); - }) - ; - } - /** - * @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) */ @@ -107,6 +84,28 @@ class VersionCheckResult extends Notification 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'); + }); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/app/Notifications/Notifiables/OwnerNotifiable.php b/app/Notifications/Notifiables/OwnerNotifiable.php index 4fc165487d..47abd5e0fd 100644 --- a/app/Notifications/Notifiables/OwnerNotifiable.php +++ b/app/Notifications/Notifiables/OwnerNotifiable.php @@ -31,27 +31,6 @@ use NotificationChannels\Pushover\PushoverReceiver; class OwnerNotifiable { - public function routeNotificationForSlack(): string - { - $res = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; - if (is_array($res)) { - $res = ''; - } - - return (string) $res; - } - - 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) - ; - } - /** * Get the notification routing information for the given driver. * @@ -62,7 +41,7 @@ class OwnerNotifiable */ public function routeNotificationFor($driver, $notification = null) { - $method = 'routeNotificationFor'.Str::studly($driver); + $method = 'routeNotificationFor' . Str::studly($driver); if (method_exists($this, $method)) { Log::debug(sprintf('Redirect for settings to "%s".', $method)); @@ -75,4 +54,24 @@ class OwnerNotifiable 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 index 56e6de1b0e..0e35e1bd64 100644 --- a/app/Notifications/ReturnsAvailableChannels.php +++ b/app/Notifications/ReturnsAvailableChannels.php @@ -39,7 +39,7 @@ class ReturnsAvailableChannels if ('owner' === $type) { return self::returnOwnerChannels(); } - if('user' === $type && null !== $user) { + if ('user' === $type && null !== $user) { return self::returnUserChannels($user); } @@ -50,8 +50,8 @@ class ReturnsAvailableChannels private static function returnOwnerChannels(): array { - $channels = ['mail']; - $slackUrl = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; + $channels = ['mail']; + $slackUrl = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data; if (UrlValidator::isValidWebhookURL($slackUrl)) { $channels[] = 'slack'; } @@ -81,19 +81,20 @@ class ReturnsAvailableChannels return $channels; } - private static function returnUserChannels(User $user): array { - $channels = ['mail']; - $slackUrl = app('preferences')->getEncryptedForUser($user, 'slack_webhook_url', '')->data; + 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) { + 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) { + if ('' === (string) app('preferences')->getEncryptedForUser($user, 'ntfy_topic', '')->data) { Log::warning('No topic name for Ntfy, channel is disabled.'); } diff --git a/app/Notifications/ReturnsSettings.php b/app/Notifications/ReturnsSettings.php index 6dd2a365c6..f0582ec751 100644 --- a/app/Notifications/ReturnsSettings.php +++ b/app/Notifications/ReturnsSettings.php @@ -50,7 +50,7 @@ class ReturnsSettings 'ntfy_pass' => '', ]; - if('user' === $type && null !== $user) { + 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; diff --git a/app/Notifications/Security/DisabledMFANotification.php b/app/Notifications/Security/DisabledMFANotification.php index 48dc9462a8..de2917e711 100644 --- a/app/Notifications/Security/DisabledMFANotification.php +++ b/app/Notifications/Security/DisabledMFANotification.php @@ -26,7 +26,6 @@ namespace FireflyIII\Notifications\Security; use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\Notifications\ReturnsSettings; -use FireflyIII\Support\Notifications\UrlValidator; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -46,6 +45,7 @@ class DisabledMFANotification extends Notification { $this->user = $user; } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -64,15 +64,21 @@ class DisabledMFANotification extends Notification return (new MailMessage())->markdown('emails.security.disabled-mfa', ['user' => $this->user])->subject($subject); } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toSlack(User $notifiable) + public function toNtfy(User $user): Message { - $message = (string) trans('email.disabled_mfa_slack', ['email' => $this->user->email]); + $settings = ReturnsSettings::getSettings('ntfy', 'user', $user); + $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 (new SlackMessage())->content($message); + return $message; } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -81,20 +87,17 @@ class DisabledMFANotification extends Notification Log::debug('Now in (user) toPushover()'); return PushoverMessage::create((string) trans('email.disabled_mfa_slack', ['email' => $this->user->email])) - ->title((string)trans('email.disabled_mfa_subject')); + ->title((string) trans('email.disabled_mfa_subject')); } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function toNtfy(User $user): Message + public function toSlack(User $notifiable) { - $settings = ReturnsSettings::getSettings('ntfy', 'user', $user); - $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])); + $message = (string) trans('email.disabled_mfa_slack', ['email' => $this->user->email]); - return $message; + return (new SlackMessage())->content($message); } /** diff --git a/app/Notifications/Security/EnabledMFANotification.php b/app/Notifications/Security/EnabledMFANotification.php index fc927be354..db6ba42652 100644 --- a/app/Notifications/Security/EnabledMFANotification.php +++ b/app/Notifications/Security/EnabledMFANotification.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -35,7 +35,7 @@ class EnabledMFANotification extends Notification { use Queueable; - private User $user; + private User $user; public function __construct(User $user) @@ -43,42 +43,41 @@ class EnabledMFANotification extends Notification $this->user = $user; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { - $subject = (string)trans('email.enabled_mfa_subject'); + $subject = (string) trans('email.enabled_mfa_subject'); return (new MailMessage())->markdown('emails.security.enabled-mfa', ['user' => $this->user])->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans('email.enabled_mfa_slack', ['email' => $this->user->email]); + $message = (string) trans('email.enabled_mfa_slack', ['email' => $this->user->email]); return (new SlackMessage())->content($message); } - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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/Security/MFABackupFewLeftNotification.php b/app/Notifications/Security/MFABackupFewLeftNotification.php index c4e7209bb6..f9b1c65e8e 100644 --- a/app/Notifications/Security/MFABackupFewLeftNotification.php +++ b/app/Notifications/Security/MFABackupFewLeftNotification.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -35,9 +35,8 @@ class MFABackupFewLeftNotification extends Notification { use Queueable; - private User $user; - private int $count; - + private int $count; + private User $user; public function __construct(User $user, int $count) { @@ -45,42 +44,41 @@ class MFABackupFewLeftNotification extends Notification $this->count = $count; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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]); return (new MailMessage())->markdown('emails.security.few-backup-codes', ['user' => $this->user, 'count' => $this->count])->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count]); + $message = (string) trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count]); return (new SlackMessage())->content($message); } - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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/Security/MFABackupNoLeftNotification.php b/app/Notifications/Security/MFABackupNoLeftNotification.php index 5e0199c079..ee53d2ecb7 100644 --- a/app/Notifications/Security/MFABackupNoLeftNotification.php +++ b/app/Notifications/Security/MFABackupNoLeftNotification.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -35,7 +35,7 @@ class MFABackupNoLeftNotification extends Notification { use Queueable; - private User $user; + private User $user; public function __construct(User $user) @@ -43,42 +43,41 @@ class MFABackupNoLeftNotification extends Notification $this->user = $user; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { - $subject = (string)trans('email.mfa_no_backups_left_subject'); + $subject = (string) trans('email.mfa_no_backups_left_subject'); return (new MailMessage())->markdown('emails.security.no-backup-codes', ['user' => $this->user])->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email]); + $message = (string) trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email]); return (new SlackMessage())->content($message); } - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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/Security/MFAManyFailedAttemptsNotification.php b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php index d7d78863fe..f21bc3d337 100644 --- a/app/Notifications/Security/MFAManyFailedAttemptsNotification.php +++ b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -35,9 +35,8 @@ class MFAManyFailedAttemptsNotification extends Notification { use Queueable; - private User $user; - private int $count; - + private int $count; + private User $user; public function __construct(User $user, int $count) { @@ -46,41 +45,38 @@ class MFAManyFailedAttemptsNotification extends Notification } - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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]); return (new MailMessage())->markdown('emails.security.many-failed-attempts', ['user' => $this->user, 'count' => $this->count])->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]); + $message = (string) trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]); return (new SlackMessage())->content($message); } - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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/Security/MFAUsedBackupCodeNotification.php b/app/Notifications/Security/MFAUsedBackupCodeNotification.php index 5aac05099d..30915d8fcd 100644 --- a/app/Notifications/Security/MFAUsedBackupCodeNotification.php +++ b/app/Notifications/Security/MFAUsedBackupCodeNotification.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -35,7 +35,7 @@ class MFAUsedBackupCodeNotification extends Notification { use Queueable; - private User $user; + private User $user; public function __construct(User $user) @@ -43,42 +43,41 @@ class MFAUsedBackupCodeNotification extends Notification $this->user = $user; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { - $subject = (string)trans('email.used_backup_code_subject'); + $subject = (string) trans('email.used_backup_code_subject'); return (new MailMessage())->markdown('emails.security.used-backup-code', ['user' => $this->user])->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans('email.used_backup_code_slack', ['email' => $this->user->email]); + $message = (string) trans('email.used_backup_code_slack', ['email' => $this->user->email]); return (new SlackMessage())->content($message); } - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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/Security/NewBackupCodesNotification.php b/app/Notifications/Security/NewBackupCodesNotification.php index 8973242e02..6fbe432cc2 100644 --- a/app/Notifications/Security/NewBackupCodesNotification.php +++ b/app/Notifications/Security/NewBackupCodesNotification.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -35,7 +35,7 @@ class NewBackupCodesNotification extends Notification { use Queueable; - private User $user; + private User $user; public function __construct(User $user) @@ -43,42 +43,40 @@ class NewBackupCodesNotification extends Notification $this->user = $user; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { - $subject = (string)trans('email.new_backup_codes_subject'); + $subject = (string) trans('email.new_backup_codes_subject'); return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user])->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans('email.new_backup_codes_slack', ['email' => $this->user->email]); + $message = (string) trans('email.new_backup_codes_slack', ['email' => $this->user->email]); return (new SlackMessage())->content($message); } - - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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/Security/UserFailedLoginAttempt.php b/app/Notifications/Security/UserFailedLoginAttempt.php index 51a4462dc7..90308f1aac 100644 --- a/app/Notifications/Security/UserFailedLoginAttempt.php +++ b/app/Notifications/Security/UserFailedLoginAttempt.php @@ -23,7 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\SlackMessage; @@ -33,7 +34,7 @@ class UserFailedLoginAttempt extends Notification { use Queueable; - private User $user; + private User $user; public function __construct(User $user) @@ -42,41 +43,37 @@ class UserFailedLoginAttempt extends Notification } - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { - $subject = (string)trans('email.new_backup_codes_subject'); + $subject = (string) trans('email.new_backup_codes_subject'); return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user])->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans('email.new_backup_codes_slack', ['email' => $this->user->email]); + $message = (string) trans('email.new_backup_codes_slack', ['email' => $this->user->email]); return (new SlackMessage())->content($message); } - - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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/Test/OwnerTestNotificationEmail.php b/app/Notifications/Test/OwnerTestNotificationEmail.php index ed74b12537..60d6911a83 100644 --- a/app/Notifications/Test/OwnerTestNotificationEmail.php +++ b/app/Notifications/Test/OwnerTestNotificationEmail.php @@ -45,11 +45,7 @@ class OwnerTestNotificationEmail extends Notification } /** - * Get the array representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray(OwnerNotifiable $notifiable) { @@ -58,13 +54,7 @@ class OwnerTestNotificationEmail extends Notification } /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return MailMessage */ public function toMail(OwnerNotifiable $notifiable) { @@ -72,18 +62,11 @@ class OwnerTestNotificationEmail extends Notification return (new MailMessage()) ->markdown('emails.admin-test', ['email' => $address]) - ->subject((string) trans('email.admin_test_subject')) - ; + ->subject((string) trans('email.admin_test_subject')); } /** - * Get the notification's delivery channels. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @param mixed $notifiable - * - * @return array */ public function via(OwnerNotifiable $notifiable) { diff --git a/app/Notifications/Test/OwnerTestNotificationNtfy.php b/app/Notifications/Test/OwnerTestNotificationNtfy.php index d6ec326625..a343a49c5a 100644 --- a/app/Notifications/Test/OwnerTestNotificationNtfy.php +++ b/app/Notifications/Test/OwnerTestNotificationNtfy.php @@ -49,13 +49,7 @@ class OwnerTestNotificationNtfy extends Notification } /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray($notifiable) { @@ -63,6 +57,9 @@ class OwnerTestNotificationNtfy extends Notification ]; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toNtfy(OwnerNotifiable $notifiable): Message { $settings = ReturnsSettings::getSettings('ntfy', 'owner', null); diff --git a/app/Notifications/Test/OwnerTestNotificationPushover.php b/app/Notifications/Test/OwnerTestNotificationPushover.php index f5fdf1a032..bbb25a50b2 100644 --- a/app/Notifications/Test/OwnerTestNotificationPushover.php +++ b/app/Notifications/Test/OwnerTestNotificationPushover.php @@ -49,11 +49,7 @@ class OwnerTestNotificationPushover extends Notification } /** - * Get the array representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray(OwnerNotifiable $notifiable) { @@ -61,13 +57,15 @@ class OwnerTestNotificationPushover extends Notification ]; } + /** + * @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')) - ; + return PushoverMessage::create((string) trans('email.admin_test_message', ['channel' => 'Pushover'])) + ->title((string) trans('email.admin_test_subject')); } /** diff --git a/app/Notifications/Test/OwnerTestNotificationSlack.php b/app/Notifications/Test/OwnerTestNotificationSlack.php index ea3dbab6d7..ee32e870f1 100644 --- a/app/Notifications/Test/OwnerTestNotificationSlack.php +++ b/app/Notifications/Test/OwnerTestNotificationSlack.php @@ -47,11 +47,7 @@ class OwnerTestNotificationSlack extends Notification } /** - * Get the array representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray(OwnerNotifiable $notifiable) { @@ -60,8 +56,6 @@ class OwnerTestNotificationSlack extends Notification } /** - * Get the Slack representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toSlack(OwnerNotifiable $notifiable) @@ -71,11 +65,7 @@ class OwnerTestNotificationSlack extends Notification } /** - * Get the notification's delivery channels. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function via(OwnerNotifiable $notifiable) { diff --git a/app/Notifications/Test/UserTestNotificationEmail.php b/app/Notifications/Test/UserTestNotificationEmail.php index 25463f8789..f3ab78154a 100644 --- a/app/Notifications/Test/UserTestNotificationEmail.php +++ b/app/Notifications/Test/UserTestNotificationEmail.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Test; -use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -46,11 +45,7 @@ class UserTestNotificationEmail extends Notification } /** - * Get the array representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray(User $notifiable) { @@ -58,33 +53,18 @@ class UserTestNotificationEmail extends Notification ]; } - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return MailMessage - */ + 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')) - ; + ->subject((string) trans('email.admin_test_subject')); } /** - * Get the notification's delivery channels. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @param User $notifiable - * - * @return array */ public function via(User $notifiable) { diff --git a/app/Notifications/Test/UserTestNotificationNtfy.php b/app/Notifications/Test/UserTestNotificationNtfy.php index 5891701ed2..cbacae16f6 100644 --- a/app/Notifications/Test/UserTestNotificationNtfy.php +++ b/app/Notifications/Test/UserTestNotificationNtfy.php @@ -49,13 +49,7 @@ class UserTestNotificationNtfy extends Notification } /** - * Get the array representation of the notification. - * - * @param User $notifiable - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray(User $notifiable) { @@ -63,10 +57,13 @@ class UserTestNotificationNtfy extends Notification ]; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function toNtfy(User $user): Message { $settings = ReturnsSettings::getSettings('ntfy', 'user', $user); - $message = new Message(); + $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'])); diff --git a/app/Notifications/Test/UserTestNotificationPushover.php b/app/Notifications/Test/UserTestNotificationPushover.php index d694ae00ed..e2828ed7f8 100644 --- a/app/Notifications/Test/UserTestNotificationPushover.php +++ b/app/Notifications/Test/UserTestNotificationPushover.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Test; -use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; @@ -50,11 +49,7 @@ class UserTestNotificationPushover extends Notification } /** - * Get the array representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray(User $notifiable) { @@ -62,13 +57,15 @@ class UserTestNotificationPushover extends Notification ]; } + /** + * @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')) - ; + return PushoverMessage::create((string) trans('email.admin_test_message', ['channel' => 'Pushover'])) + ->title((string) trans('email.admin_test_subject')); } /** diff --git a/app/Notifications/Test/UserTestNotificationSlack.php b/app/Notifications/Test/UserTestNotificationSlack.php index 0d935bccce..5f6a02a836 100644 --- a/app/Notifications/Test/UserTestNotificationSlack.php +++ b/app/Notifications/Test/UserTestNotificationSlack.php @@ -40,18 +40,16 @@ class UserTestNotificationSlack extends Notification private OwnerNotifiable $owner; - + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ public function __construct(OwnerNotifiable $owner) { $this->owner = $owner; } /** - * Get the array representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function toArray(OwnerNotifiable $notifiable) { @@ -60,8 +58,6 @@ class UserTestNotificationSlack extends Notification } /** - * Get the Slack representation of the notification. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toSlack(OwnerNotifiable $notifiable) @@ -71,11 +67,7 @@ class UserTestNotificationSlack extends Notification } /** - * Get the notification's delivery channels. - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @return array */ public function via(OwnerNotifiable $notifiable) { diff --git a/app/Notifications/User/BillReminder.php b/app/Notifications/User/BillReminder.php index 5b2cb849b9..0fa3dedd5a 100644 --- a/app/Notifications/User/BillReminder.php +++ b/app/Notifications/User/BillReminder.php @@ -25,7 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; use FireflyIII\Models\Bill; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -52,58 +52,55 @@ class BillReminder extends Notification } - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { - $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 (new MailMessage()) ->markdown('emails.bill-warning', ['field' => $this->field, 'diff' => $this->diff, 'bill' => $this->bill]) - ->subject($subject) - ; + ->subject($subject); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { - $message = (string)trans(sprintf('email.bill_warning_subject_%s', $this->field), ['diff' => $this->diff, 'name' => $this->bill->name]); + $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]); + $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()) ->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($message); } - - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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 126bc2f8c7..4144b19bf6 100644 --- a/app/Notifications/User/NewAccessToken.php +++ b/app/Notifications/User/NewAccessToken.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -42,40 +42,35 @@ class NewAccessToken extends Notification public function __construct() {} - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { return (new MailMessage()) ->markdown('emails.token-created') - ->subject((string)trans('email.access_token_created_subject')) - ; + ->subject((string) trans('email.access_token_created_subject')); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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')); } - - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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 35ea148b1e..32c3a199a5 100644 --- a/app/Notifications/User/RuleActionFailed.php +++ b/app/Notifications/User/RuleActionFailed.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\SlackMessage; @@ -47,22 +47,26 @@ class RuleActionFailed extends Notification public function __construct(array $params) { [$mainMessage, $groupTitle, $groupLink, $ruleTitle, $ruleLink] = $params; - $this->message = $mainMessage; - $this->groupTitle = $groupTitle; - $this->groupLink = $groupLink; - $this->ruleTitle = $ruleTitle; - $this->ruleLink = $ruleLink; + $this->message = $mainMessage; + $this->groupTitle = $groupTitle; + $this->groupLink = $groupLink; + $this->ruleTitle = $ruleTitle; + $this->ruleLink = $ruleLink; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { $groupTitle = $this->groupTitle; $groupLink = $this->groupLink; @@ -70,28 +74,19 @@ class RuleActionFailed extends Notification $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); + $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); }); } - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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)) { - app('log')->debug('Will send ruleActionFailed through Slack or Discord!'); - - return ['slack']; - } - app('log')->debug('Will NOT send ruleActionFailed through Slack or Discord'); - - return []; + // todo disable mail channel + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/User/TransactionCreation.php b/app/Notifications/User/TransactionCreation.php index 61e2626d8a..4262543c7e 100644 --- a/app/Notifications/User/TransactionCreation.php +++ b/app/Notifications/User/TransactionCreation.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -43,25 +45,33 @@ class TransactionCreation extends Notification $this->collection = $collection; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ - public function toMail($notifiable) + public function toMail(User $notifiable) { return (new MailMessage()) ->markdown('emails.report-new-journals', ['transformed' => $this->collection]) - ->subject(trans_choice('email.new_journals_subject', count($this->collection))) - ; + ->subject(trans_choice('email.new_journals_subject', count($this->collection))); } - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) { - return ['mail']; + // todo only over email? + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/User/UserLogin.php b/app/Notifications/User/UserLogin.php index 3107748de0..82682e7c59 100644 --- a/app/Notifications/User/UserLogin.php +++ b/app/Notifications/User/UserLogin.php @@ -25,7 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Support\Notifications\UrlValidator; +use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -48,16 +48,18 @@ class UserLogin extends Notification } - public function toArray($notifiable) + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { - $time = now(config('app.timezone'))->isoFormat((string)trans('config.date_time_js')); + $time = now(config('app.timezone'))->isoFormat((string) trans('config.date_time_js')); $host = ''; try { @@ -72,12 +74,13 @@ class UserLogin extends Notification return (new MailMessage()) ->markdown('emails.new-ip', ['time' => $time, 'ipAddress' => $this->ip, 'host' => $host]) - ->subject((string)trans('email.login_from_new_ip')) - ; + ->subject((string) trans('email.login_from_new_ip')); } - - public function toSlack($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toSlack(User $notifiable) { $host = ''; @@ -91,22 +94,14 @@ class UserLogin extends Notification $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', ['host' => $host, 'ip' => $this->ip])); } - - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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 e75861add3..e7988763b7 100644 --- a/app/Notifications/User/UserNewPassword.php +++ b/app/Notifications/User/UserNewPassword.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -43,25 +45,30 @@ class UserNewPassword extends Notification $this->url = $url; } - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toMail(User $notifiable) { return (new MailMessage()) ->markdown('emails.password', ['url' => $this->url]) - ->subject((string)trans('email.reset_pw_subject')) - ; + ->subject((string) trans('email.reset_pw_subject')); } - - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) { - return ['mail']; + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } diff --git a/app/Notifications/User/UserRegistration.php b/app/Notifications/User/UserRegistration.php index bde33969f6..a973b4a7fc 100644 --- a/app/Notifications/User/UserRegistration.php +++ b/app/Notifications/User/UserRegistration.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; +use FireflyIII\Notifications\ReturnsAvailableChannels; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -38,25 +40,30 @@ class UserRegistration extends Notification public function __construct() {} - - public function toArray($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toArray(User $notifiable) { return [ ]; } - - public function toMail($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + 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')); } - - public function via($notifiable) + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function via(User $notifiable) { - return ['mail']; + return ReturnsAvailableChannels::returnChannels('user', $notifiable); } } From 55209928617ff4f7b5e013e1c926774dec4a56fb Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 07:52:02 +0100 Subject: [PATCH 030/167] Clean up and fix notifications. --- app/Events/Security/UserAttemptedLogin.php | 1 + app/Handlers/Events/UserEventHandler.php | 1 + app/Http/Controllers/Auth/LoginController.php | 1 + app/Notifications/Admin/UserInvitation.php | 2 +- app/Notifications/Admin/UserRegistration.php | 2 +- .../Admin/VersionCheckResult.php | 2 +- .../Security/DisabledMFANotification.php | 11 ++----- .../Security/EnabledMFANotification.php | 25 ++++++++++++++- .../Security/MFABackupFewLeftNotification.php | 25 ++++++++++++++- .../Security/MFABackupNoLeftNotification.php | 25 ++++++++++++++- .../MFAManyFailedAttemptsNotification.php | 25 ++++++++++++++- .../MFAUsedBackupCodeNotification.php | 24 +++++++++++++- .../Security/NewBackupCodesNotification.php | 25 ++++++++++++++- .../Security/UserFailedLoginAttempt.php | 31 ++++++++++++++++--- app/Notifications/User/BillReminder.php | 2 +- app/Notifications/User/NewAccessToken.php | 2 +- app/Notifications/User/RuleActionFailed.php | 2 +- app/Notifications/User/UserLogin.php | 2 +- resources/lang/en_US/email.php | 6 ++++ .../emails/security/failed-login.blade.php | 6 ++++ 20 files changed, 195 insertions(+), 25 deletions(-) create mode 100644 resources/views/emails/security/failed-login.blade.php diff --git a/app/Events/Security/UserAttemptedLogin.php b/app/Events/Security/UserAttemptedLogin.php index 8457a5ecc3..55dc3af6a9 100644 --- a/app/Events/Security/UserAttemptedLogin.php +++ b/app/Events/Security/UserAttemptedLogin.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Events\Security; use FireflyIII\Events\Event; +use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Queue\SerializesModels; diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 91b63b4e18..3eaa2dd6ea 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -43,6 +43,7 @@ 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\OwnerTestNotificationEmail; use FireflyIII\Notifications\Test\OwnerTestNotificationNtfy; use FireflyIII\Notifications\Test\OwnerTestNotificationPushover; diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 6558e626de..8ef53caf16 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -26,6 +26,7 @@ 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; diff --git a/app/Notifications/Admin/UserInvitation.php b/app/Notifications/Admin/UserInvitation.php index 8ac48334aa..7f9b90a0e0 100644 --- a/app/Notifications/Admin/UserInvitation.php +++ b/app/Notifications/Admin/UserInvitation.php @@ -103,7 +103,7 @@ class UserInvitation extends Notification */ public function toSlack(OwnerNotifiable $notifiable) { - return (new SlackMessage())->content( + return new SlackMessage()->content( (string) trans('email.invitation_created_body', ['email' => $this->invitee->user->email, 'invitee' => $this->invitee->email]) ); } diff --git a/app/Notifications/Admin/UserRegistration.php b/app/Notifications/Admin/UserRegistration.php index 90af37ff63..5a6e505da0 100644 --- a/app/Notifications/Admin/UserRegistration.php +++ b/app/Notifications/Admin/UserRegistration.php @@ -103,7 +103,7 @@ class UserRegistration extends Notification */ public function toSlack(OwnerNotifiable $notifiable) { - return (new SlackMessage())->content((string) trans('email.admin_new_user_registered', ['email' => $this->user->email, 'id' => $this->user->id])); + return new SlackMessage()->content((string) trans('email.admin_new_user_registered', ['email' => $this->user->email, 'id' => $this->user->id])); } /** diff --git a/app/Notifications/Admin/VersionCheckResult.php b/app/Notifications/Admin/VersionCheckResult.php index d809913e74..809504a546 100644 --- a/app/Notifications/Admin/VersionCheckResult.php +++ b/app/Notifications/Admin/VersionCheckResult.php @@ -100,7 +100,7 @@ class VersionCheckResult extends Notification */ public function toSlack(OwnerNotifiable $notifiable) { - return (new SlackMessage())->content($this->message) + return new SlackMessage()->content($this->message) ->attachment(static function ($attachment): void { $attachment->title('Firefly III @ GitHub', 'https://github.com/firefly-iii/firefly-iii/releases'); }); diff --git a/app/Notifications/Security/DisabledMFANotification.php b/app/Notifications/Security/DisabledMFANotification.php index de2917e711..f870510ce5 100644 --- a/app/Notifications/Security/DisabledMFANotification.php +++ b/app/Notifications/Security/DisabledMFANotification.php @@ -65,12 +65,9 @@ class DisabledMFANotification extends Notification return (new MailMessage())->markdown('emails.security.disabled-mfa', ['user' => $this->user])->subject($subject); } - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function toNtfy(User $user): Message + public function toNtfy(User $notifiable): Message { - $settings = ReturnsSettings::getSettings('ntfy', 'user', $user); + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title((string) trans('email.disabled_mfa_subject')); @@ -84,8 +81,6 @@ class DisabledMFANotification extends Notification */ public function toPushover(User $notifiable): PushoverMessage { - Log::debug('Now in (user) toPushover()'); - return PushoverMessage::create((string) trans('email.disabled_mfa_slack', ['email' => $this->user->email])) ->title((string) trans('email.disabled_mfa_subject')); } @@ -97,7 +92,7 @@ class DisabledMFANotification extends Notification { $message = (string) trans('email.disabled_mfa_slack', ['email' => $this->user->email]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); } /** diff --git a/app/Notifications/Security/EnabledMFANotification.php b/app/Notifications/Security/EnabledMFANotification.php index db6ba42652..2bc1c9858d 100644 --- a/app/Notifications/Security/EnabledMFANotification.php +++ b/app/Notifications/Security/EnabledMFANotification.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; 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 EnabledMFANotification extends Notification { @@ -62,6 +65,26 @@ class EnabledMFANotification extends Notification return (new MailMessage())->markdown('emails.security.enabled-mfa', ['user' => $this->user])->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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.enabled_mfa_slack', ['email' => $this->user->email])) + ->title((string) trans('email.enabled_mfa_subject')); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -69,7 +92,7 @@ class EnabledMFANotification extends Notification { $message = (string) trans('email.enabled_mfa_slack', ['email' => $this->user->email]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); } diff --git a/app/Notifications/Security/MFABackupFewLeftNotification.php b/app/Notifications/Security/MFABackupFewLeftNotification.php index f9b1c65e8e..cad845ed69 100644 --- a/app/Notifications/Security/MFABackupFewLeftNotification.php +++ b/app/Notifications/Security/MFABackupFewLeftNotification.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; 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 MFABackupFewLeftNotification extends Notification { @@ -70,7 +73,27 @@ class MFABackupFewLeftNotification extends Notification { $message = (string) trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); + } + + 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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + 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')); } diff --git a/app/Notifications/Security/MFABackupNoLeftNotification.php b/app/Notifications/Security/MFABackupNoLeftNotification.php index ee53d2ecb7..a9389977bc 100644 --- a/app/Notifications/Security/MFABackupNoLeftNotification.php +++ b/app/Notifications/Security/MFABackupNoLeftNotification.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; 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 MFABackupNoLeftNotification extends Notification { @@ -69,7 +72,27 @@ class MFABackupNoLeftNotification extends Notification { $message = (string) trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); + } + + 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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email])) + ->title((string) trans('email.mfa_no_backups_left_slack')); } diff --git a/app/Notifications/Security/MFAManyFailedAttemptsNotification.php b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php index f21bc3d337..54f3e5a9ad 100644 --- a/app/Notifications/Security/MFAManyFailedAttemptsNotification.php +++ b/app/Notifications/Security/MFAManyFailedAttemptsNotification.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; 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 MFAManyFailedAttemptsNotification extends Notification { @@ -68,7 +71,27 @@ class MFAManyFailedAttemptsNotification extends Notification { $message = (string) trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); + } + + 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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + 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')); } diff --git a/app/Notifications/Security/MFAUsedBackupCodeNotification.php b/app/Notifications/Security/MFAUsedBackupCodeNotification.php index 30915d8fcd..f0680e72d5 100644 --- a/app/Notifications/Security/MFAUsedBackupCodeNotification.php +++ b/app/Notifications/Security/MFAUsedBackupCodeNotification.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; 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 MFAUsedBackupCodeNotification extends Notification { @@ -69,9 +72,28 @@ class MFAUsedBackupCodeNotification extends Notification { $message = (string) trans('email.used_backup_code_slack', ['email' => $this->user->email]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); } + 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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.used_backup_code_slack', ['email' => $this->user->email])) + ->title((string) trans('email.used_backup_code_subject')); + } /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/Notifications/Security/NewBackupCodesNotification.php b/app/Notifications/Security/NewBackupCodesNotification.php index 6fbe432cc2..196c577b74 100644 --- a/app/Notifications/Security/NewBackupCodesNotification.php +++ b/app/Notifications/Security/NewBackupCodesNotification.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; 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 NewBackupCodesNotification extends Notification { @@ -69,7 +72,27 @@ class NewBackupCodesNotification extends Notification { $message = (string) trans('email.new_backup_codes_slack', ['email' => $this->user->email]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); + } + + 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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.new_backup_codes_slack', ['email' => $this->user->email])) + ->title((string) trans('email.new_backup_codes_subject')); } /** diff --git a/app/Notifications/Security/UserFailedLoginAttempt.php b/app/Notifications/Security/UserFailedLoginAttempt.php index 90308f1aac..c3827e45fd 100644 --- a/app/Notifications/Security/UserFailedLoginAttempt.php +++ b/app/Notifications/Security/UserFailedLoginAttempt.php @@ -24,11 +24,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\Security; 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 UserFailedLoginAttempt extends Notification { @@ -54,9 +57,9 @@ class UserFailedLoginAttempt extends Notification */ public function toMail(User $notifiable) { - $subject = (string) trans('email.new_backup_codes_subject'); + $subject = (string) trans('email.failed_login_subject'); - return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user])->subject($subject); + return (new MailMessage())->markdown('emails.security.failed-login', ['user' => $this->user])->subject($subject); } /** @@ -64,9 +67,29 @@ class UserFailedLoginAttempt extends Notification */ public function toSlack(User $notifiable) { - $message = (string) trans('email.new_backup_codes_slack', ['email' => $this->user->email]); + $message = (string) trans('email.failed_login_message', ['email' => $this->user->email]); - return (new SlackMessage())->content($message); + return new SlackMessage()->content($message); + } + + 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')); } /** diff --git a/app/Notifications/User/BillReminder.php b/app/Notifications/User/BillReminder.php index 0fa3dedd5a..35b3f53047 100644 --- a/app/Notifications/User/BillReminder.php +++ b/app/Notifications/User/BillReminder.php @@ -88,7 +88,7 @@ class BillReminder extends Notification $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); diff --git a/app/Notifications/User/NewAccessToken.php b/app/Notifications/User/NewAccessToken.php index 4144b19bf6..16175a22ae 100644 --- a/app/Notifications/User/NewAccessToken.php +++ b/app/Notifications/User/NewAccessToken.php @@ -63,7 +63,7 @@ class NewAccessToken extends Notification */ 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')); } /** diff --git a/app/Notifications/User/RuleActionFailed.php b/app/Notifications/User/RuleActionFailed.php index 32c3a199a5..ea36540bb4 100644 --- a/app/Notifications/User/RuleActionFailed.php +++ b/app/Notifications/User/RuleActionFailed.php @@ -73,7 +73,7 @@ class RuleActionFailed extends Notification $ruleTitle = $this->ruleTitle; $ruleLink = $this->ruleLink; - return (new SlackMessage())->content($this->message)->attachment(static function ($attachment) use ($groupTitle, $groupLink): void { + 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); diff --git a/app/Notifications/User/UserLogin.php b/app/Notifications/User/UserLogin.php index 82682e7c59..c0a06bbad2 100644 --- a/app/Notifications/User/UserLogin.php +++ b/app/Notifications/User/UserLogin.php @@ -94,7 +94,7 @@ class UserLogin extends Notification $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', ['host' => $host, 'ip' => $this->ip])); } /** diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index 087b24c9a5..408e75132a 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -66,6 +66,12 @@ return [ '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', 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..afe4ab48d2 --- /dev/null +++ b/resources/views/emails/security/failed-login.blade.php @@ -0,0 +1,6 @@ +@component('mail::message') +{{ trans('email.failed_login_body', ['email' => $user->email]) }} + +{{ trans('email.failed_login_warning') }} + +@endcomponent From 03e9e3dbdbb51e930b15b9af27425ff5fe6ae868 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 08:03:18 +0100 Subject: [PATCH 031/167] Expand all notifications. --- app/Notifications/User/BillReminder.php | 45 +++++++++---- app/Notifications/User/NewAccessToken.php | 23 +++++++ app/Notifications/User/RuleActionFailed.php | 28 +++++++- .../User/TransactionCreation.php | 3 +- app/Notifications/User/UserLogin.php | 65 ++++++++++++------- app/Notifications/User/UserNewPassword.php | 27 ++++++++ app/Notifications/User/UserRegistration.php | 4 +- resources/lang/en_US/email.php | 1 + 8 files changed, 154 insertions(+), 42 deletions(-) diff --git a/app/Notifications/User/BillReminder.php b/app/Notifications/User/BillReminder.php index 35b3f53047..8c41751137 100644 --- a/app/Notifications/User/BillReminder.php +++ b/app/Notifications/User/BillReminder.php @@ -26,11 +26,14 @@ namespace FireflyIII\Notifications\User; use FireflyIII\Models\Bill; 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 @@ -66,14 +69,9 @@ class BillReminder extends Notification */ 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()); } /** @@ -81,10 +79,6 @@ class BillReminder extends Notification */ 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]); @@ -93,7 +87,36 @@ class BillReminder extends Notification ->attachment(static function ($attachment) use ($bill, $url): void { $attachment->title((string) trans('firefly.visit_bill', ['name' => $bill->name]), $url); }) - ->content($message); + ->content($this->getSubject()); + } + + 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()); + } + + 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; } /** diff --git a/app/Notifications/User/NewAccessToken.php b/app/Notifications/User/NewAccessToken.php index 16175a22ae..65269d02a5 100644 --- a/app/Notifications/User/NewAccessToken.php +++ b/app/Notifications/User/NewAccessToken.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; 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 NewAccessToken @@ -66,6 +69,26 @@ class NewAccessToken extends Notification return new SlackMessage()->content((string) trans('email.access_token_created_body')); } + 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')); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/app/Notifications/User/RuleActionFailed.php b/app/Notifications/User/RuleActionFailed.php index ea36540bb4..60f3016e90 100644 --- a/app/Notifications/User/RuleActionFailed.php +++ b/app/Notifications/User/RuleActionFailed.php @@ -25,10 +25,13 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; 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 @@ -81,12 +84,33 @@ class RuleActionFailed extends Notification } + 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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create($this->message); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function via(User $notifiable) { - // todo disable mail channel - return ReturnsAvailableChannels::returnChannels('user', $notifiable); + $channels = ReturnsAvailableChannels::returnChannels('user', $notifiable); + if (($key = array_search('mail', $channels)) !== false) { + unset($channels[$key]); + } + return $channels; } } diff --git a/app/Notifications/User/TransactionCreation.php b/app/Notifications/User/TransactionCreation.php index 4262543c7e..9d1ac6e198 100644 --- a/app/Notifications/User/TransactionCreation.php +++ b/app/Notifications/User/TransactionCreation.php @@ -71,7 +71,6 @@ class TransactionCreation extends Notification */ public function via(User $notifiable) { - // todo only over email? - return ReturnsAvailableChannels::returnChannels('user', $notifiable); + return ['mail']; } } diff --git a/app/Notifications/User/UserLogin.php b/app/Notifications/User/UserLogin.php index c0a06bbad2..bfa803fe30 100644 --- a/app/Notifications/User/UserLogin.php +++ b/app/Notifications/User/UserLogin.php @@ -26,11 +26,14 @@ namespace FireflyIII\Notifications\User; use FireflyIII\Exceptions\FireflyException; 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 UserLogin @@ -60,41 +63,38 @@ class UserLogin extends Notification 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; - } return (new MailMessage()) - ->markdown('emails.new-ip', ['time' => $time, 'ipAddress' => $this->ip, 'host' => $host]) + ->markdown('emails.new-ip', ['time' => $time, 'ipAddress' => $this->ip, 'host' => $this->getHost()]) ->subject((string) trans('email.login_from_new_ip')); } + 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.login_from_new_ip')); + $message->body((string) trans('email.slack_login_from_new_ip', ['host' => $this->getHost(), 'ip' => $this->ip])); + + return $message; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.slack_login_from_new_ip', ['host' => $this->getHost(), 'ip' => $this->ip])) + ->title((string) trans('email.login_from_new_ip')); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function toSlack(User $notifiable) { - $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; - } - - 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', ['host' => $this->getHost(), 'ip' => $this->ip])); } /** @@ -104,4 +104,19 @@ class UserLogin extends Notification { return ReturnsAvailableChannels::returnChannels('user', $notifiable); } + + private function getHost(): string { + $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; + } + return $host; + } } diff --git a/app/Notifications/User/UserNewPassword.php b/app/Notifications/User/UserNewPassword.php index e7988763b7..3b2cc285d0 100644 --- a/app/Notifications/User/UserNewPassword.php +++ b/app/Notifications/User/UserNewPassword.php @@ -25,10 +25,14 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; 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 UserNewPassword @@ -67,6 +71,29 @@ class UserNewPassword extends Notification /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ + public function toSlack(User $notifiable) + { + return new SlackMessage()->content((string) trans('email.reset_pw_message')); + } + + 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; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((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 a973b4a7fc..d839797b13 100644 --- a/app/Notifications/User/UserRegistration.php +++ b/app/Notifications/User/UserRegistration.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; -use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; @@ -64,6 +63,7 @@ class UserRegistration extends Notification */ public function via(User $notifiable) { - return ReturnsAvailableChannels::returnChannels('user', $notifiable); + // other settings will not be available at this point anyway. + return ['mail']; } } diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index 408e75132a..9f07ac0a38 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -109,6 +109,7 @@ return [ // reset password '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!', From fb6c67fa046452ee0e4cb80e0bbbe6f10f6c5d0e Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 08:10:58 +0100 Subject: [PATCH 032/167] Fix https://github.com/firefly-iii/firefly-iii/issues/9532 --- app/Console/Commands/Integrity/ReportSum.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/Console/Commands/Integrity/ReportSum.php b/app/Console/Commands/Integrity/ReportSum.php index 62ab39afbb..d348468aba 100644 --- a/app/Console/Commands/Integrity/ReportSum.php +++ b/app/Console/Commands/Integrity/ReportSum.php @@ -59,18 +59,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)); } } From 6a62f781e9e8e0a92f17466576ac666e50347ae4 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Dec 2024 17:32:03 +0100 Subject: [PATCH 033/167] Multi account piggy banks. --- .../Commands/Upgrade/UpgradeDatabase.php | 1 + .../Upgrade/UpgradeMultiPiggyBanks.php | 2 +- app/Factory/PiggyBankFactory.php | 30 +- .../PiggyBank/AmountController.php | 165 +++++--- .../Controllers/PiggyBank/EditController.php | 17 +- .../Controllers/PiggyBank/ShowController.php | 1 + app/Http/Requests/PiggyBankUpdateRequest.php | 78 +++- app/Models/Account.php | 5 +- .../PiggyBank/ModifiesPiggyBanks.php | 113 +++--- .../PiggyBank/PiggyBankRepository.php | 84 ++-- .../PiggyBankRepositoryInterface.php | 19 +- app/Support/Steam.php | 368 +++++++++--------- .../Actions/UpdatePiggybank.php | 5 + app/Transformers/PiggyBankTransformer.php | 2 +- app/User.php | 4 + config/notifications.php | 4 +- resources/views/list/piggy-bank-events.twig | 4 +- resources/views/piggy-banks/add-mobile.twig | 18 +- resources/views/piggy-banks/add.twig | 14 +- resources/views/piggy-banks/create.twig | 2 +- resources/views/piggy-banks/edit.twig | 5 +- .../views/piggy-banks/remove-mobile.twig | 18 +- resources/views/piggy-banks/remove.twig | 9 +- resources/views/piggy-banks/show.twig | 8 +- 24 files changed, 572 insertions(+), 404 deletions(-) diff --git a/app/Console/Commands/Upgrade/UpgradeDatabase.php b/app/Console/Commands/Upgrade/UpgradeDatabase.php index 5aea98ad90..8090c16c90 100644 --- a/app/Console/Commands/Upgrade/UpgradeDatabase.php +++ b/app/Console/Commands/Upgrade/UpgradeDatabase.php @@ -72,6 +72,7 @@ class UpgradeDatabase extends Command 'firefly-iii:create-group-memberships', 'firefly-iii:upgrade-group-information', 'firefly-iii:upgrade-currency-preferences', + 'firefly-iii:upgrade-multi-piggies', 'firefly-iii:correct-database', ]; $args = []; diff --git a/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php b/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php index e425277e1b..af1d492a82 100644 --- a/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php +++ b/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php @@ -93,7 +93,7 @@ class UpgradeMultiPiggyBanks extends Command { $this->repository->setUser($piggyBank->account->user); $this->accountRepository->setUser($piggyBank->account->user); - $repetition = $this->repository->getRepetition($piggyBank); + $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. diff --git a/app/Factory/PiggyBankFactory.php b/app/Factory/PiggyBankFactory.php index 84dbab3853..4f58734d05 100644 --- a/app/Factory/PiggyBankFactory.php +++ b/app/Factory/PiggyBankFactory.php @@ -42,12 +42,22 @@ class PiggyBankFactory public User $user { set(User $value) { $this->user = $value; + $this->currencyRepository->setUser($value); + $this->accountRepository->setUser($value); + $this->piggyBankRepository->setUser($value); } } private CurrencyRepositoryInterface $currencyRepository; private AccountRepositoryInterface $accountRepository; 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. * @@ -56,12 +66,7 @@ class PiggyBankFactory * @return PiggyBank */ public function store(array $data): PiggyBank { - $this->currencyRepository = app(CurrencyRepositoryInterface::class); - $this->accountRepository = app(AccountRepositoryInterface::class); - $this->piggyBankRepository = app(PiggyBankRepositoryInterface::class); - $this->currencyRepository->setUser($this->user); - $this->accountRepository->setUser($this->user); - $this->piggyBankRepository->setUser($this->user); + $piggyBankData =$data; // unset some fields @@ -202,14 +207,23 @@ class PiggyBankFactory } - private function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void { + public function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void { + $toBeLinked = []; /** @var array $info */ foreach($accounts as $info) { $account = $this->accountRepository->find((int)($info['account_id'] ?? 0)); if(null === $account) { continue; } - $piggyBank->accounts()->syncWithoutDetaching([$account->id => ['current_amount' => $info['current_amount'] ?? '0']]); + if(array_key_exists('current_amount',$info)) { + $toBeLinked[$account->id] = ['current_amount' => $info['current_amount']]; + //$piggyBank->accounts()->syncWithoutDetaching([$account->id => ['current_amount' => $info['current_amount'] ?? '0']]); + } + if(!array_key_exists('current_amount', $info)) { + $toBeLinked[$account->id] = []; + //$piggyBank->accounts()->syncWithoutDetaching([$account->id]); + } } + $piggyBank->accounts()->sync($toBeLinked); } } diff --git a/app/Http/Controllers/PiggyBank/AmountController.php b/app/Http/Controllers/PiggyBank/AmountController.php index d14446d291..857d377c26 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->target_amount, '0')) { - $leftToSave = bcsub($piggyBank->target_amount, $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->target_amount, '0')) { - $leftToSave = bcsub($piggyBank->target_amount, $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,47 @@ 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 +178,43 @@ 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 +228,14 @@ class AmountController extends Controller */ public function remove(PiggyBank $piggyBank) { - $repetition = $this->piggyRepos->getRepetition($piggyBank); - $currency = $this->accountRepos->getAccountCurrency($piggyBank->account) ?? app('amount')->getDefaultCurrency(); - - return view('piggy-banks.remove', compact('piggyBank', 'repetition', 'currency')); + $accounts = []; + foreach ($piggyBank->accounts as $account) { + $accounts[] = [ + 'account' => $account, + 'saved_so_far' => $this->piggyRepos->getCurrentAmount($piggyBank, $account), + ]; + } + return view('piggy-banks.remove', compact('piggyBank', 'accounts')); } /** @@ -197,9 +245,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/EditController.php b/app/Http/Controllers/PiggyBank/EditController.php index 6df3ec3101..0a3d8a377b 100644 --- a/app/Http/Controllers/PiggyBank/EditController.php +++ b/app/Http/Controllers/PiggyBank/EditController.php @@ -79,22 +79,21 @@ class EditController extends Controller // Flash some data to fill the form. $targetDate = $piggyBank->target_date?->format('Y-m-d'); $startDate = $piggyBank->start_date?->format('Y-m-d'); - $currency = $this->accountRepository->getAccountCurrency($piggyBank->account); - if (null === $currency) { - $currency = app('amount')->getDefaultCurrency(); - } $preFilled = [ 'name' => $piggyBank->name, - 'account_id' => $piggyBank->account_id, - 'targetamount' => app('steam')->bcround($piggyBank->target_amount, $currency->decimal_places), - 'targetdate' => $targetDate, - 'startdate' => $startDate, + '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, ]; + foreach($piggyBank->accounts as $account) { + $preFilled['accounts'][] = $account->id; + } if (0 === bccomp($piggyBank->target_amount, '0')) { - $preFilled['targetamount'] = ''; + $preFilled['target_amount'] = ''; } session()->flash('preFilled', $preFilled); diff --git a/app/Http/Controllers/PiggyBank/ShowController.php b/app/Http/Controllers/PiggyBank/ShowController.php index 2fd69286ae..b3bc52bc1c 100644 --- a/app/Http/Controllers/PiggyBank/ShowController.php +++ b/app/Http/Controllers/PiggyBank/ShowController.php @@ -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/Requests/PiggyBankUpdateRequest.php b/app/Http/Requests/PiggyBankUpdateRequest.php index 7f10011678..5c2192ffdd 100644 --- a/app/Http/Requests/PiggyBankUpdateRequest.php +++ b/app/Http/Requests/PiggyBankUpdateRequest.php @@ -24,6 +24,8 @@ 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\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; @@ -44,15 +46,22 @@ 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 +73,62 @@ 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 app('amount')->getDefaultCurrency(); + } + + return $currency; + } } diff --git a/app/Models/Account.php b/app/Models/Account.php index fe523e2fe4..f77143d846 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -32,6 +32,7 @@ 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; @@ -159,9 +160,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/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index 61f21c39d0..b7c53c9ea2 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -27,12 +27,14 @@ 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\TransactionJournal; use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; use FireflyIII\Support\Facades\Amount; +use Illuminate\Support\Facades\Log; /** * Trait ModifiesPiggyBanks @@ -55,30 +57,42 @@ trait ModifiesPiggyBanks } } - 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->current_amount = bcsub($repetition->current_amount, $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->save(); - app('log')->debug('addAmount [a]: Trigger change for negative amount.'); + app('log')->debug('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 removeAmountFromAll(PiggyBank $piggyBank, string $amount): void { - $repetition = $this->getRepetition($piggyBank); - if (null === $repetition) { - return false; + 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); + } } - $currentAmount = $repetition->current_amount ?? '0'; - $repetition->current_amount = bcadd($currentAmount, $amount); - $repetition->save(); + } + + public function addAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool + { + $currentAmount = $this->getCurrentAmount($piggyBank, $account); + $pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot; + $pivot->current_amount = bcadd($currentAmount, $amount); + $pivot->save(); app('log')->debug('addAmount [b]: Trigger change for positive amount.'); event(new ChangedAmount($piggyBank, $amount, $journal, null)); @@ -86,37 +100,36 @@ trait ModifiesPiggyBanks 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)->current_amount; + 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; + + app('log')->debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d H:i:s'))); + app('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; + app('log')->debug(sprintf('Left to save: %s', $leftToSave)); + app('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))); + app('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->current_amount; + $savedSoFar = $this->getCurrentAmount($piggyBank, $account); return bccomp($amount, $savedSoFar) <= 0; } @@ -244,17 +257,24 @@ trait ModifiesPiggyBanks $this->setOrder($piggyBank, $newOrder); } + // update the accounts + $factory = new PiggyBankFactory(); + $factory->user = $this->user; + $factory->linkToAccountIds($piggyBank, $data['accounts']); + + // 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->current_amount > $piggyBank->target_amount && 0 !== bccomp($piggyBank->target_amount, '0')) { - $difference = bcsub($piggyBank->target_amount, $repetition->current_amount); + $currentAmount = $this->getCurrentAmount($piggyBank); + if (1 === bccomp($currentAmount, '100') && 0 !== bccomp($piggyBank->target_amount, '0')) { + $difference = bcsub($piggyBank->target_amount, $currentAmount); // an amount will be removed, create "negative" event: event(new ChangedAmount($piggyBank, $difference, null, null)); - $repetition->current_amount = $piggyBank->target_amount; - $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: @@ -295,22 +315,19 @@ 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->target_amount = $data['targetamount']; - } - if (array_key_exists('targetamount', $data) && '' === $data['targetamount']) { + if (array_key_exists('target_amount', $data) && '' === $data['target_amount']) { $piggyBank->target_amount = '0'; } - if (array_key_exists('targetdate', $data) && '' !== $data['targetdate']) { - $piggyBank->target_date = $data['targetdate']; - $piggyBank->target_date_tz = $data['targetdate']?->format('e'); + 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('startdate', $data)) { - $piggyBank->start_date = $data['startdate']; - $piggyBank->start_date_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(); diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 0d9943b5c3..7d7c8c3e2b 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -26,6 +26,7 @@ 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; @@ -95,7 +96,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function getAttachments(PiggyBank $piggyBank): Collection { - $set = $piggyBank->attachments()->get(); + $set = $piggyBank->attachments()->get(); /** @var \Storage $disk */ $disk = \Storage::disk('upload'); @@ -114,22 +115,28 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface /** * Get current amount saved in piggy bank. */ - public function getCurrentAmount(PiggyBank $piggyBank): string + public function getCurrentAmount(PiggyBank $piggyBank, ?Account $account = null): string { $sum = '0'; - foreach ($piggyBank->accounts as $account) { - $amount = (string) $account->pivot->current_amount; + 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): ?PiggyBankRepetition + public function getRepetition(PiggyBank $piggyBank, bool $overrule = false): ?PiggyBankRepetition { - throw new FireflyException('[b] Piggy bank repetitions are EOL.'); - + if (false === $overrule) { + throw new FireflyException('[b] Piggy bank repetitions are EOL.'); + } + Log::warning('Piggy bank repetitions are EOL.'); return $piggyBank->piggyBankRepetitions()->first(); } @@ -148,15 +155,15 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface throw new FireflyException('[c] Piggy bank repetitions are EOL.'); app('log')->debug(sprintf('Now in getExactAmount(%d, %d, %d)', $piggyBank->id, $repetition->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); @@ -165,10 +172,10 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBankCurrency->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) { @@ -190,7 +197,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } // currency of the account + the piggy bank currency are almost the same. // which amount from the transaction matches? - $amount = null; + $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 @@ -206,8 +213,8 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } app('log')->debug(sprintf('The currency is %s and the amount is %s', $currency->code, $amount)); - $room = bcsub($piggyBank->target_amount, $repetition->current_amount); - $compare = bcmul($repetition->current_amount, '-1'); + $room = bcsub($piggyBank->target_amount, $repetition->current_amount); + $compare = bcmul($repetition->current_amount, '-1'); if (0 === bccomp($piggyBank->target_amount, '0')) { // amount is zero? then the "room" is positive amount of we wish to add or remove. @@ -239,7 +246,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return (string) $amount; } - public function setUser(null|Authenticatable|User $user): void + public function setUser(null | Authenticatable | User $user): void { if ($user instanceof User) { $this->user = $user; @@ -264,12 +271,12 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface { $currency = app('amount')->getDefaultCurrency(); - $set = $this->getPiggyBanks(); + $set = $this->getPiggyBanks(); /** @var PiggyBank $piggy */ foreach ($set as $piggy) { $currentAmount = $this->getRepetition($piggy)->current_amount ?? '0'; - $piggy->name = $piggy->name.' ('.app('amount')->formatAnything($currency, $currentAmount, false).')'; + $piggy->name = $piggy->name . ' (' . app('amount')->formatAnything($currency, $currentAmount, false) . ')'; } return $set; @@ -278,16 +285,15 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function getPiggyBanks(): Collection { 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', auth()->user()->id) - ->with( - [ - 'account', - 'objectGroups', - ] - ) - ->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']) - ; + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', auth()->user()->id) + ->with( + [ + 'account', + 'objectGroups', + ] + ) + ->orderBy('piggy_banks.order', 'ASC')->distinct()->get(['piggy_banks.*']); } /** @@ -320,21 +326,22 @@ 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 = app('steam')->balanceConvertedIgnoreVirtual($account, $date, $piggyBank->transactionCurrency); + 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->current_amount); - } + $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; } @@ -345,8 +352,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface $search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query)); } $search->orderBy('piggy_banks.order', 'ASC') - ->orderBy('piggy_banks.name', 'ASC') - ; + ->orderBy('piggy_banks.name', 'ASC'); return $search->take($limit)->get(); } diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index b1168bdaa0..29338df6ee 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 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. @@ -68,7 +69,10 @@ 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 current amount saved in piggy bank. + */ /** * Get all events. @@ -97,7 +101,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 "". @@ -107,9 +111,10 @@ 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 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; diff --git a/app/Support/Steam.php b/app/Support/Steam.php index a08d243652..fba143a4fb 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -44,34 +44,52 @@ class Steam */ public function balanceIgnoreVirtual(Account $account, Carbon $date): string { - // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); + throw new FireflyException('Deprecated method balanceIgnoreVirtual.'); /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); + $repository = app(AccountRepositoryInterface::class); $repository->setUser($account->user); - $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'); + $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'); // 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() - ; + $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); } + + public function balanceConvertedIgnoreVirtual(Account $account, Carbon $date, TransactionCurrency $currency): string + { + $balance = $this->balanceConverted($account, $date, $currency); + $virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance; + + // currency of account + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($account->user); + $accountCurrency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); + if ($accountCurrency->id !== $currency->id && 0 !== bccomp($virtual, '0')) { + // convert amount to given currency. + Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); + $converter = new ExchangeRateConverter(); + $virtual = $converter->convert($accountCurrency, $currency, $date, $virtual); + } + + return bcsub($balance, $virtual); + } + public function sumTransactions(array $transactions, string $key): string { $sum = '0'; @@ -96,7 +114,7 @@ class Steam public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-in-range'); $cache->addProperty(null !== $currency ? $currency->id : 0); @@ -108,42 +126,41 @@ class Steam $start->subDay(); $end->addDay(); - $balances = []; - $formatted = $start->format('Y-m-d'); - $startBalance = $this->balance($account, $start, $currency); + $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); + $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); } - $currencyId = $currency->id; + $currencyId = $currency->id; $start->addDay(); // query! - $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')) - ->groupBy('transaction_journals.date') - ->groupBy('transactions.transaction_currency_id') - ->groupBy('transactions.foreign_currency_id') - ->orderBy('transaction_journals.date', 'ASC') - ->whereNull('transaction_journals.deleted_at') - ->get( - [ // @phpstan-ignore-line - 'transaction_journals.date', - 'transactions.transaction_currency_id', - \DB::raw('SUM(transactions.amount) AS modified'), - 'transactions.foreign_currency_id', - \DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), - ] - ) - ; + $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')) + ->groupBy('transaction_journals.date') + ->groupBy('transactions.transaction_currency_id') + ->groupBy('transactions.foreign_currency_id') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') + ->get( + [ // @phpstan-ignore-line + 'transaction_journals.date', + 'transactions.transaction_currency_id', + \DB::raw('SUM(transactions.amount) AS modified'), + 'transactions.foreign_currency_id', + \DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), + ] + ); - $currentBalance = $startBalance; + $currentBalance = $startBalance; /** @var Transaction $entry */ foreach ($set as $entry) { @@ -173,7 +190,7 @@ class Steam public function balanceByTransactions(Account $account, Carbon $date, ?TransactionCurrency $currency): array { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-by-transactions'); $cache->addProperty($date); @@ -182,13 +199,12 @@ class Steam return $cache->get(); } - $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') - ; + $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); @@ -203,7 +219,7 @@ class Steam $return = []; $result = $query->get(['transactions.transaction_currency_id', 'transactions.balance_after']); foreach ($result as $entry) { - $key = (int) $entry->transaction_currency_id; + $key = (int) $entry->transaction_currency_id; if (array_key_exists($key, $return)) { continue; } @@ -222,7 +238,7 @@ class Steam { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // abuse chart properties: - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance'); $cache->addProperty($date); @@ -232,26 +248,24 @@ class Steam } /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); + $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'); + $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() - ; + ->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; @@ -270,7 +284,7 @@ class Steam 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 = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-in-range-converted'); $cache->addProperty($native->id); @@ -290,35 +304,34 @@ class Steam Log::debug(sprintf('Start balance on %s is %s', $formatted, $startBalance)); Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); - $converter = new ExchangeRateConverter(); + $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() - ; + $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; + $currentBalance = $startBalance; /** @var Transaction $transaction */ foreach ($set as $transaction) { - $day = false; + $day = false; try { $day = Carbon::parse($transaction['date'], config('app.timezone')); @@ -328,7 +341,7 @@ class Steam if (false === $day) { $day = today(config('app.timezone')); } - $format = $day->format('Y-m-d'); + $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. @@ -351,21 +364,21 @@ class Steam $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; + $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 - )); + '%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); @@ -397,7 +410,7 @@ class Steam { // 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 = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance'); $cache->addProperty($date); @@ -418,72 +431,66 @@ class Steam 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() - ; + $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() - ; + ->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() - ; + $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() - ; + ->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() - ; + $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() - ; + $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')); + $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(); + $start = clone $date; + $end = clone $date; + $converter = new ExchangeRateConverter(); foreach ($new as $set) { foreach ($set as $transaction) { $currentDate = false; @@ -506,7 +513,7 @@ class Steam foreach ($new as $set) { foreach ($set as $transaction) { - $currentDate = false; + $currentDate = false; try { $currentDate = Carbon::parse($transaction['date'], config('app.timezone')); @@ -523,9 +530,9 @@ class Steam } // 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); + $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); @@ -542,9 +549,9 @@ class Steam public function balancesByAccounts(Collection $accounts, Carbon $date): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $ids = $accounts->pluck('id')->toArray(); + $ids = $accounts->pluck('id')->toArray(); // cache this property. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($ids); $cache->addProperty('balances'); $cache->addProperty($date); @@ -573,9 +580,9 @@ class Steam public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $ids = $accounts->pluck('id')->toArray(); + $ids = $accounts->pluck('id')->toArray(); // cache this property. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($ids); $cache->addProperty('balances-converted'); $cache->addProperty($date); @@ -591,9 +598,9 @@ class Steam $default = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); $result[$account->id] = [ - 'balance' => $this->balance($account, $date), - 'native_balance' => $this->balanceConverted($account, $date, $default), - ]; + 'balance' => $this->balance($account, $date), + 'native_balance' => $this->balanceConverted($account, $date, $default), + ]; } $cache->store($result); @@ -607,9 +614,9 @@ class Steam public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); - $ids = $accounts->pluck('id')->toArray(); + $ids = $accounts->pluck('id')->toArray(); // cache this property. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($ids); $cache->addProperty('balances-per-currency'); $cache->addProperty($date); @@ -634,7 +641,7 @@ class Steam { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // abuse chart properties: - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-per-currency'); $cache->addProperty($date); @@ -642,10 +649,9 @@ class Steam 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') - ; + ->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 = []; @@ -677,10 +683,10 @@ class Steam // Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision)); if (str_contains($number, '.')) { if ('-' !== $number[0]) { - return bcadd($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcadd($number, '0.' . str_repeat('0', $precision) . '5', $precision); } - return bcsub($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcsub($number, '0.' . str_repeat('0', $precision) . '5', $precision); } return $number; @@ -763,15 +769,15 @@ class Steam { $list = []; - $set = auth()->user()->transactions() - ->whereIn('transactions.account_id', $accounts) - ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) - ->get(['transactions.account_id', \DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line + $set = auth()->user()->transactions() + ->whereIn('transactions.account_id', $accounts) + ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) + ->get(['transactions.account_id', \DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line ; /** @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; } @@ -846,9 +852,9 @@ class Steam public function getSafeUrl(string $unknownUrl, string $safeUrl): string { // Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl)); - $returnUrl = $safeUrl; - $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); - $safeHost = parse_url($safeUrl, PHP_URL_HOST); + $returnUrl = $safeUrl; + $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); + $safeHost = parse_url($safeUrl, PHP_URL_HOST); if (null !== $unknownHost && $unknownHost === $safeHost) { $returnUrl = $unknownUrl; @@ -885,7 +891,7 @@ class Steam */ public function floatalize(string $value): string { - $value = strtoupper($value); + $value = strtoupper($value); if (!str_contains($value, 'E')) { return $value; } diff --git a/app/TransactionRules/Actions/UpdatePiggybank.php b/app/TransactionRules/Actions/UpdatePiggybank.php index 1e748c1926..c437318018 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,7 @@ 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( @@ -161,6 +163,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 +172,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); } @@ -199,6 +203,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/Transformers/PiggyBankTransformer.php b/app/Transformers/PiggyBankTransformer.php index 98a0fe4a4f..4a094dbfcc 100644 --- a/app/Transformers/PiggyBankTransformer.php +++ b/app/Transformers/PiggyBankTransformer.php @@ -78,7 +78,7 @@ class PiggyBankTransformer extends AbstractTransformer // get currently saved amount: $currency = $piggyBank->transactionCurrency; - $currentAmount = app('steam')->bcround($this->piggyRepos->getCurrentAmount($piggyBank), $currency->decimal_places); + $currentAmount = $this->piggyRepos->getCurrentAmount($piggyBank); // Amounts, depending on 0.0 state of target amount $percentage = null; diff --git a/app/User.php b/app/User.php index 00ca3d2e74..c32c6d2ad6 100644 --- a/app/User.php +++ b/app/User.php @@ -106,6 +106,10 @@ class User extends Authenticatable return $this->hasMany(Account::class); } + public function piggyBanks() { + throw new FireflyException('Method no longer supported.'); + } + /** * Link to attachments */ diff --git a/config/notifications.php b/config/notifications.php index cc137cc174..7e33c2b223 100644 --- a/config/notifications.php +++ b/config/notifications.php @@ -28,8 +28,8 @@ return [ '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], +// 'gotify' => ['enabled' => false, 'ui_configurable' => 0], +// 'pushbullet' => ['enabled' => false, 'ui_configurable' => 0], ], 'notifications' => [ 'user' => [ 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/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 %}
    diff --git a/resources/views/piggy-banks/remove-mobile.twig b/resources/views/piggy-banks/remove-mobile.twig index b0046f55a4..192f5e7d53 100644 --- a/resources/views/piggy-banks/remove-mobile.twig +++ b/resources/views/piggy-banks/remove-mobile.twig @@ -14,15 +14,17 @@
    -

    - {{ '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 %}