mirror of
				https://github.com/firefly-iii/firefly-iii.git
				synced 2025-10-31 18:54:58 +00:00 
			
		
		
		
	Compare commits
	
		
			452 Commits
		
	
	
		
			develop-20
			...
			develop-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e7526ac5e3 | ||
|  | 441ada70b8 | ||
|  | dedc06a46b | ||
|  | b0adf1b277 | ||
|  | 28f65e9f44 | ||
|  | a013af5f0d | ||
|  | 9552701662 | ||
|  | ef52f0aad1 | ||
|  | 0b6f04905a | ||
|  | cdb36357d4 | ||
|  | 8938622bd9 | ||
|  | b210294aa9 | ||
|  | 5b02f20775 | ||
|  | fac382a5df | ||
|  | 88d88bebc9 | ||
|  | 755fb9c29b | ||
|  | 51a835ab51 | ||
|  | c9895ab182 | ||
|  | e71d46a4e5 | ||
|  | 8d1d5f37c1 | ||
|  | 525a68682d | ||
|  | 715648d0d8 | ||
|  | 9452e93f22 | ||
|  | a6aa145471 | ||
|  | 25aa6dcb59 | ||
|  | bb2270b274 | ||
|  | d7f6b4143e | ||
|  | 0cf0e26fa8 | ||
|  | cc23197d60 | ||
|  | bc1721d95e | ||
|  | 0d19173da6 | ||
|  | 1983f07d3c | ||
|  | aad1b91cc2 | ||
|  | 8cb1057a33 | ||
|  | b178032985 | ||
|  | 561213e95d | ||
|  | 44fa7c4306 | ||
|  | e2169563e2 | ||
|  | 845344e003 | ||
|  | cdb48453e8 | ||
|  | 9669cef518 | ||
|  | f962f71ed7 | ||
|  | 94ed4021fb | ||
|  | 1765855c57 | ||
|  | 55cf3e7d44 | ||
|  | 9f1840dc05 | ||
|  | 78dab2e5f9 | ||
|  | 103b9d5005 | ||
|  | 1b75b778d8 | ||
|  | 7e665dbdfc | ||
|  | b6897ec3a9 | ||
|  | 660260174a | ||
|  | 78d32865b5 | ||
|  | edfa92c1aa | ||
|  | 63012f269c | ||
|  | 7d0e7f779f | ||
|  | b8e18f80f4 | ||
|  | 481b01e4f7 | ||
|  | edf2030251 | ||
|  | bd1cfffb61 | ||
|  | 629f70d27d | ||
|  | 57f5ebc0f9 | ||
|  | b4f51e7b47 | ||
|  | d78d254e86 | ||
|  | eb0c113699 | ||
|  | 23045ebd59 | ||
|  | 2e5931f304 | ||
|  | a620b07c00 | ||
|  | cb724145f2 | ||
|  | 8ef17f6686 | ||
|  | debfd9160c | ||
|  | f2482e4ace | ||
|  | d98d757f8b | ||
|  | 0c9a41a929 | ||
|  | e26f78bf50 | ||
|  | ed265f68ba | ||
|  | d2e9b64bf5 | ||
|  | 3811aff206 | ||
|  | 762d898fee | ||
|  | 5e6034fc86 | ||
|  | 9da10459d6 | ||
|  | ff80cedd6b | ||
|  | b213148ae8 | ||
|  | c8646e20cb | ||
|  | 76a41fec50 | ||
|  | 0e705bd038 | ||
|  | f33ffb98ff | ||
|  | faa0d59340 | ||
|  | 5af0219884 | ||
|  | dafd99f155 | ||
|  | 3560f0388c | ||
|  | b2954658d8 | ||
|  | 44581d9983 | ||
|  | 02dcfeb227 | ||
|  | d8bafb349d | ||
|  | 889598a4c8 | ||
|  | 7e37d10016 | ||
|  | ebaebb09d1 | ||
|  | 531a3a4b6c | ||
|  | b3e313821b | ||
|  | 51958af422 | ||
|  | e3b21ccdba | ||
|  | 31bb208835 | ||
|  | 8c97e805a2 | ||
|  | ac8a43bb37 | ||
|  | 2df4b40a28 | ||
|  | e06736c254 | ||
|  | ec367e94ce | ||
|  | 1515dea9fa | ||
|  | adedf9c17d | ||
|  | 0b52fb84f1 | ||
|  | 16e742ae73 | ||
|  | 1b4471dfae | ||
|  | ae152ce0a4 | ||
|  | 2aa023f140 | ||
|  | 6e2e4c6f08 | ||
|  | 1c8c038735 | ||
|  | 4d339a6da8 | ||
|  | b7edd4407a | ||
|  | a679a1e94a | ||
|  | 180451d32f | ||
|  | 7396f22bca | ||
|  | 058019aa84 | ||
|  | 695f83d1d8 | ||
|  | ac4dfb3baf | ||
|  | 427001b223 | ||
|  | 3117d8b30d | ||
|  | d19dd2a8b2 | ||
|  | de3dcc3fc2 | ||
|  | 077f3e095b | ||
|  | ad3b0bb320 | ||
|  | 8538741341 | ||
|  | a0aef5d579 | ||
|  | fdd93427aa | ||
|  | ac3f6557de | ||
|  | b0a909150c | ||
|  | 913f163fe4 | ||
|  | 3126b07b33 | ||
|  | 08ca90cf75 | ||
|  | 540ac2a277 | ||
|  | ed80bed066 | ||
|  | 41d2541c6a | ||
|  | 5dedf63498 | ||
|  | 09bc4f41d2 | ||
|  | cebf0b5c57 | ||
|  | 1632a57e3e | ||
|  | 744c4be7d1 | ||
|  | bd99ef3eff | ||
|  | 8a86f13a5d | ||
|  | 7418b2f0ee | ||
|  | a0e9de9312 | ||
|  | 7e23a6f5e8 | ||
|  | 44589f8744 | ||
|  | d24531030f | ||
|  | 25bdab1346 | ||
|  | 41af1c863a | ||
|  | 76b3b18cfb | ||
|  | e6fb2958a9 | ||
|  | 15b75b322f | ||
|  | 86149d1032 | ||
|  | ded142cd9e | ||
|  | 7923eb9ec9 | ||
|  | 132553c108 | ||
|  | c2269fc9a4 | ||
|  | aed30d1499 | ||
|  | 84a1a876e1 | ||
|  | dc675707f9 | ||
|  | d5667c7ef6 | ||
|  | cba1213dd1 | ||
|  | 7219c90957 | ||
|  | af13bd991e | ||
|  | 48e548eb52 | ||
|  | 1a19e27f0e | ||
|  | 0cbd22426d | ||
|  | d5e52e99e0 | ||
|  | f52978e71f | ||
|  | 3a3358124d | ||
|  | 929808c633 | ||
|  | a78df574f3 | ||
|  | 875cad16b6 | ||
|  | 7bc30192ca | ||
|  | a1a8968e98 | ||
|  | 6abb74a038 | ||
|  | 2d7d05e985 | ||
|  | d426e09474 | ||
|  | 72d55cb953 | ||
|  | 73ad865581 | ||
|  | fefb52beb7 | ||
|  | abd503543b | ||
|  | e3eb550581 | ||
|  | 46b780758e | ||
|  | b2c3ee9779 | ||
|  | dca899bcee | ||
|  | 9667b8a948 | ||
|  | 661f225fe7 | ||
|  | 4c6fe0c8de | ||
|  | 78f457950e | ||
|  | d831cc8df2 | ||
|  | 7056406afc | ||
|  | c85cfcf3e6 | ||
|  | db06d06789 | ||
|  | a28b990cd1 | ||
|  | dab4bfa7a6 | ||
|  | 6575236f2b | ||
|  | ad582c8806 | ||
|  | 452e9cb953 | ||
|  | a64f137b39 | ||
|  | c067d6aab0 | ||
|  | 119b9920a6 | ||
|  | 99ed54fce8 | ||
|  | 2ea57cdd38 | ||
|  | bb94bdfdaf | ||
|  | 4de8398cc2 | ||
|  | e6e8cd5d8a | ||
|  | 0b200309ba | ||
|  | a184548912 | ||
|  | c987191212 | ||
|  | 7009b444d9 | ||
|  | 06551d5367 | ||
|  | a20622ac0c | ||
|  | ca38117fca | ||
|  | 9478f78d4f | ||
|  | 5c2397bbae | ||
|  | 92fefef816 | ||
|  | d3ced65524 | ||
|  | 29eb748831 | ||
|  | 76df3d5f33 | ||
|  | 252076ec1f | ||
|  | bbec28591f | ||
|  | 075a360ba6 | ||
|  | 477524a8ae | ||
|  | dfe055732d | ||
|  | 78b611a18d | ||
|  | 367bdf65e6 | ||
|  | 3fc9caa31a | ||
|  | 95a41fcab7 | ||
|  | 58b409fc00 | ||
|  | 3eaaac09ad | ||
|  | bcb672920c | ||
|  | 79b91e25c2 | ||
|  | 7170931464 | ||
|  | c1b5a1a13e | ||
|  | a6265ce8ab | ||
|  | 90109917df | ||
|  | 0acd54c2b7 | ||
|  | c96226b9b4 | ||
|  | 6d143f1624 | ||
|  | 93324d1154 | ||
|  | a39f0e1891 | ||
|  | 822f609a22 | ||
|  | cd7ddd1c61 | ||
|  | 0b63ba26bb | ||
|  | 94d70cdb62 | ||
|  | acb3831c8b | ||
|  | c9d9ecede4 | ||
|  | 4eb5873353 | ||
|  | 7ca39fdb21 | ||
|  | b8d1d7a8c0 | ||
|  | 1af79eab30 | ||
|  | 03be2704ce | ||
|  | 34baea66a7 | ||
|  | 0638d109d0 | ||
|  | 561e228a2d | ||
|  | cb5d856769 | ||
|  | 04fe5d1fc4 | ||
|  | 45e9d4f8de | ||
|  | 73fdbb6202 | ||
|  | e49dbefddd | ||
|  | fc5143337a | ||
|  | 4b3eb6dace | ||
|  | c741b2a819 | ||
|  | cebfaa32bf | ||
|  | d356d39d43 | ||
|  | 7d9f22d3f4 | ||
|  | c6c8f282e2 | ||
|  | 6a64420721 | ||
|  | fcde4e2488 | ||
|  | aa5c4c20e9 | ||
|  | 794e31e487 | ||
|  | 16bf186312 | ||
|  | 45c722e786 | ||
|  | 36d9e5c3fe | ||
|  | 8d614de67f | ||
|  | d17da670ab | ||
|  | 5bf4df9ad8 | ||
|  | 07db6b59ce | ||
|  | e16f1cf4ee | ||
|  | 4c80d929ca | ||
|  | 16364d9859 | ||
|  | c24f6acb2c | ||
|  | 4bd19e0627 | ||
|  | 69d839997a | ||
|  | c02c027f4f | ||
|  | b14606625e | ||
|  | 222d7b56c7 | ||
|  | b7f7bf42b2 | ||
|  | 0cbd64d31a | ||
|  | 60bdae47c4 | ||
|  | ca4b38d905 | ||
|  | 3674465f53 | ||
|  | b951d4130c | ||
|  | 7992b810fd | ||
|  | c1c0afa40b | ||
|  | 56c9026299 | ||
|  | 021ddfc36b | ||
|  | 5bd72f6428 | ||
|  | feabfe54f0 | ||
|  | 565409b486 | ||
|  | f57366da5f | ||
|  | 064217ccb0 | ||
|  | fa3ccbda33 | ||
|  | f43aadf02d | ||
|  | 3b8a4d3e9b | ||
|  | 3d410556ef | ||
|  | f15ca1d0a1 | ||
|  | 7002463c54 | ||
|  | 649f876437 | ||
|  | 3cfd178cbd | ||
|  | cefbaafa19 | ||
|  | a8c88800c4 | ||
|  | 9d1a127200 | ||
|  | 3fdde2d1c8 | ||
|  | cdc0b8dd2c | ||
|  | 1a1e06e6e8 | ||
|  | 6d39b8468c | ||
|  | bdee3947b2 | ||
|  | 2317037655 | ||
|  | dcea6b757b | ||
|  | bd7fe92818 | ||
|  | 850e47d8db | ||
|  | 96fe62400f | ||
|  | 5d07fcdcb6 | ||
|  | fd5d2d57a8 | ||
|  | 8e7d42201f | ||
|  | f26bd3cb31 | ||
|  | 36915cdace | ||
|  | 8a5cecd2a0 | ||
|  | 78da1b22bb | ||
|  | 6d970a9794 | ||
|  | 8bb7739f05 | ||
|  | 7788bb4b33 | ||
|  | 2ecb4bb3b7 | ||
|  | 4a783d3c3c | ||
|  | e16645ae87 | ||
|  | 9d3189be7e | ||
|  | 07fca78293 | ||
|  | 82080501c7 | ||
|  | d93d6bfc66 | ||
|  | a41326ef94 | ||
|  | 90b77845c3 | ||
|  | 57af80d820 | ||
|  | fc4d5a1dfd | ||
|  | 8ab9ab8d21 | ||
|  | 75674b5793 | ||
|  | a7d6f26051 | ||
|  | eb540ce148 | ||
|  | a3077fe43b | ||
|  | 63bb84d375 | ||
|  | e5f5aa628e | ||
|  | c54f84dc8e | ||
|  | c2e562623c | ||
|  | c8d5e8a9dc | ||
|  | 963f017be3 | ||
|  | 0e0eeb736f | ||
|  | e8d9b8fa49 | ||
|  | c166b9242e | ||
|  | 8ff8efced2 | ||
|  | 0b4fb9a806 | ||
|  | ba9fef9410 | ||
|  | f7d94d17cd | ||
|  | 1fea9c6817 | ||
|  | a88c8bedbe | ||
|  | fbf3468053 | ||
|  | 2a3ba9799e | ||
|  | d121aad28f | ||
|  | dc808fa807 | ||
|  | a1be6ff62b | ||
|  | 20dc5b0256 | ||
|  | edd54e23c5 | ||
|  | 1238df8784 | ||
|  | b8c62652b0 | ||
|  | 54b2d02f63 | ||
|  | 47faf89a5c | ||
|  | b7fb5a3854 | ||
|  | a384b4202a | ||
|  | 99ecac0ce4 | ||
|  | 6102982456 | ||
|  | b88e981b4b | ||
|  | 827263b03e | ||
|  | d44e74d334 | ||
|  | 911f46c590 | ||
|  | 7d42c4ee5d | ||
|  | ea89f6177f | ||
|  | 74291b3870 | ||
|  | 2c4f2082fe | ||
|  | d8d58cc29b | ||
|  | 85b17e4035 | ||
|  | 83de5667b3 | ||
|  | 5fffe873c6 | ||
|  | 78c09c82d6 | ||
|  | 704abc315d | ||
|  | 86b4965458 | ||
|  | d2dc0c2bf0 | ||
|  | 9f4894bbb5 | ||
|  | 76a8675a34 | ||
|  | 6988301da1 | ||
|  | 109cd37211 | ||
|  | 284ff4d1b0 | ||
|  | bc0ab7af99 | ||
|  | a17bc7258f | ||
|  | 87911c2438 | ||
|  | 746f1fd300 | ||
|  | 9e5faf919f | ||
|  | cc6cbe6605 | ||
|  | dc6d708897 | ||
|  | 6189d24b98 | ||
|  | f6e28dc88f | ||
|  | 75ea035630 | ||
|  | 4cdb14301d | ||
|  | 9f95221ba3 | ||
|  | e3a67be412 | ||
|  | 5749b642ce | ||
|  | baff7c67f9 | ||
|  | ccc005942f | ||
|  | 5b83c33039 | ||
|  | cc32578c5f | ||
|  | 80f410835b | ||
|  | b537a3145d | ||
|  | bfa1fcbaf8 | ||
|  | 56243907c4 | ||
|  | 5928dd72e6 | ||
|  | c6bf0ff1cd | ||
|  | 19d1cf192b | ||
|  | 37d7dc7e3e | ||
|  | 95a3a194b8 | ||
|  | 3542387188 | ||
|  | da1b002a64 | ||
|  | 46daee28e7 | ||
|  | 9ade5635d4 | ||
|  | e14e80f33c | ||
|  | 0c824e21c8 | ||
|  | fab1c68569 | ||
|  | c1534657f2 | ||
|  | 39841de680 | ||
|  | 43a720b62b | ||
|  | 5ec54de29e | ||
|  | 397e37f344 | ||
|  | b6f84c2b99 | ||
|  | 843f86fc66 | ||
|  | 0e8e364074 | ||
|  | bbccbef578 | ||
|  | ee11a8e3a0 | 
| @@ -19,6 +19,8 @@ | ||||
|  * along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| use PhpCsFixer\Runner\Parallel\ParallelConfigFactory; | ||||
| 
 | ||||
| $current = __DIR__; | ||||
| 
 | ||||
| $paths = [ | ||||
| @@ -35,6 +37,7 @@ $finder = PhpCsFixer\Finder::create() | ||||
| 
 | ||||
| 
 | ||||
| $config = new PhpCsFixer\Config(); | ||||
| $config->setParallelConfig(ParallelConfigFactory::detect()); | ||||
| return $config->setRules( | ||||
|     [ | ||||
|         // rule sets
 | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| { | ||||
|   "require": { | ||||
|     "friendsofphp/php-cs-fixer": "^3.12" | ||||
|   } | ||||
|     "require": { | ||||
|         "friendsofphp/php-cs-fixer": "^3.12" | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1052
									
								
								.ci/php-cs-fixer/composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1052
									
								
								.ci/php-cs-fixer/composer.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										19
									
								
								.ci/phpcs.sh
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								.ci/phpcs.sh
									
									
									
									
									
								
							| @@ -20,23 +20,8 @@ | ||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| # | ||||
|  | ||||
| # Install composer packages | ||||
| #composer install --no-scripts --no-ansi | ||||
|  | ||||
| SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | ||||
|  | ||||
| # enable test .env file. | ||||
| # cp .ci/.env.ci .env | ||||
|  | ||||
| OUTPUT_FORMAT=txt | ||||
| EXTRA_PARAMS="" | ||||
|  | ||||
| if [[ $GITHUB_ACTIONS = "true" ]] | ||||
| then | ||||
|     OUTPUT_FORMAT=txt | ||||
|     EXTRA_PARAMS="" | ||||
| fi | ||||
|  | ||||
| # clean up php code | ||||
| cd $SCRIPT_DIR/php-cs-fixer | ||||
| composer update --quiet | ||||
| @@ -44,8 +29,8 @@ rm -f .php-cs-fixer.cache | ||||
| PHP_CS_FIXER_IGNORE_ENV=true | ||||
| ./vendor/bin/php-cs-fixer fix \ | ||||
|     --config $SCRIPT_DIR/php-cs-fixer/.php-cs-fixer.php \ | ||||
|     --format=$OUTPUT_FORMAT \ | ||||
|     --allow-risky=yes $EXTRA_PARAMS | ||||
|     --format=txt \ | ||||
|     --allow-risky=yes | ||||
|  | ||||
| EXIT_CODE=$? | ||||
|  | ||||
|   | ||||
							
								
								
									
										28
									
								
								.ci/phpmd/composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								.ci/phpmd/composer.lock
									
									
									
										generated
									
									
									
								
							| @@ -9,16 +9,16 @@ | ||||
|     "packages-dev": [ | ||||
|         { | ||||
|             "name": "composer/pcre", | ||||
|             "version": "3.1.2", | ||||
|             "version": "3.1.3", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/composer/pcre.git", | ||||
|                 "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace" | ||||
|                 "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace", | ||||
|                 "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace", | ||||
|                 "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", | ||||
|                 "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -60,7 +60,7 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/composer/pcre/issues", | ||||
|                 "source": "https://github.com/composer/pcre/tree/3.1.2" | ||||
|                 "source": "https://github.com/composer/pcre/tree/3.1.3" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -76,20 +76,20 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-03-07T15:38:35+00:00" | ||||
|             "time": "2024-03-19T10:26:25+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "composer/xdebug-handler", | ||||
|             "version": "3.0.3", | ||||
|             "version": "3.0.4", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/composer/xdebug-handler.git", | ||||
|                 "reference": "ced299686f41dce890debac69273b47ffe98a40c" | ||||
|                 "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", | ||||
|                 "reference": "ced299686f41dce890debac69273b47ffe98a40c", | ||||
|                 "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", | ||||
|                 "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -100,7 +100,7 @@ | ||||
|             "require-dev": { | ||||
|                 "phpstan/phpstan": "^1.0", | ||||
|                 "phpstan/phpstan-strict-rules": "^1.1", | ||||
|                 "symfony/phpunit-bridge": "^6.0" | ||||
|                 "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "autoload": { | ||||
| @@ -124,9 +124,9 @@ | ||||
|                 "performance" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "irc": "irc://irc.freenode.org/composer", | ||||
|                 "irc": "ircs://irc.libera.chat:6697/composer", | ||||
|                 "issues": "https://github.com/composer/xdebug-handler/issues", | ||||
|                 "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" | ||||
|                 "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -142,7 +142,7 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2022-02-25T21:32:43+00:00" | ||||
|             "time": "2024-03-26T18:29:49+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "pdepend/pdepend", | ||||
|   | ||||
| @@ -19,9 +19,9 @@ | ||||
|   ~ along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|   --> | ||||
|  | ||||
| <ruleset name="pcsg-generated-ruleset" | ||||
| <ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|          name="pcsg-generated-ruleset" | ||||
|          xmlns="http://pmd.sf.net/ruleset/1.0.0" | ||||
|          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|          xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd" | ||||
|          xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"> | ||||
|     <description>Firefly III ruleset.</description> | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| parameters: | ||||
|   scanFiles: | ||||
|     - ../_ide_helper_models.php | ||||
|   universalObjectCratesClasses: | ||||
|     - Illuminate\Database\Eloquent\Model | ||||
|   # TODO: slowly remove these parameters and fix the issues found. | ||||
|   reportUnmatchedIgnoredErrors: false | ||||
|   checkGenericClassInNonGenericObjectType: false  # remove this rule when all other issues are solved. | ||||
|   ignoreErrors: | ||||
|   # TODO: slowly remove these exceptions and fix the issues found. | ||||
|     - '#Dynamic call to static method#' # all the Laravel ORM things depend on this. | ||||
| @@ -11,6 +12,7 @@ parameters: | ||||
|     - '#with no value type specified in iterable type array#' # remove this rule when all other issues are solved. | ||||
|     - '#has no value type specified in iterable type array#' # remove this rule when all other issues are solved. | ||||
|     - '#is not allowed to extend#' | ||||
|     - '#does not specify its types#' | ||||
|     - '#switch is forbidden to use#' | ||||
|     - '#is neither abstract nor final#' | ||||
|     - '#on left side of \?\?\= always exists and is not nullable#' | ||||
|   | ||||
							
								
								
									
										21
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								.env.example
									
									
									
									
									
								
							| @@ -299,27 +299,6 @@ DKR_BUILD_LOCALE=false | ||||
| # Won't significantly speed up things. | ||||
| DKR_CHECK_SQLITE=true | ||||
|  | ||||
| # Run database creation and migration commands. Disable this only if you're 100% sure the DB exists | ||||
| # and is up to date. | ||||
| DKR_RUN_MIGRATION=true | ||||
|  | ||||
| # Run database upgrade commands. Disable this only when you're 100% sure your DB is up-to-date | ||||
| # with the latest fixes (outside of migrations!) | ||||
| DKR_RUN_UPGRADE=true | ||||
|  | ||||
| # Verify database integrity. Includes all data checks and verifications. | ||||
| # Disabling this makes Firefly III assume your DB is intact. | ||||
| DKR_RUN_VERIFY=true | ||||
|  | ||||
| # Run database reporting commands. When disabled, Firefly III won't go over your data to report current state. | ||||
| # Disabling this should have no impact on data integrity or safety but it won't warn you of possible issues. | ||||
| DKR_RUN_REPORT=true | ||||
|  | ||||
| # Generate OAuth2 keys. | ||||
| # When disabled, Firefly III won't attempt to generate OAuth2 Passport keys. This won't be an issue, IFF (if and only if) | ||||
| # you had previously generated keys already and they're stored in your database for restoration. | ||||
| DKR_RUN_PASSPORT_INSTALL=true | ||||
|  | ||||
| # Leave the following configuration vars as is. | ||||
| # Unless you like to tinker and know what you're doing. | ||||
| APP_NAME=FireflyIII | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ updates: | ||||
|   # Check for updates to GitHub Actions every week | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     labels: [] | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
|  | ||||
| @@ -11,6 +12,7 @@ updates: | ||||
|   - package-ecosystem: "composer" | ||||
|     directory: "/" # Location of package manifests | ||||
|     target-branch: develop | ||||
|     labels: [] | ||||
|     versioning-strategy: increase | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
| @@ -18,6 +20,7 @@ updates: | ||||
|   # yarn / JS updates | ||||
|   - package-ecosystem: "npm" | ||||
|     directory: "/" | ||||
|     labels: [] | ||||
|     target-branch: develop | ||||
|     versioning-strategy: increase | ||||
|     schedule: | ||||
|   | ||||
							
								
								
									
										19
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,20 @@ | ||||
| <!-- | ||||
| Before you create a new PR, please consider: | ||||
| Thank you for submitting new code to Firefly III, or any of the related projects. Please read the following rules carefully. | ||||
| 
 | ||||
| 1) Pull requests for the MAIN branch will be closed. | ||||
| 2) DO NOT include translations in your PR. Only English US sentences. | ||||
| - Please do not submit solutions for problems that are not already reported in an issue. | ||||
| - Unfortunately, Firefly III can't be your learning experience. If you're new to all of this, please open an issue first. | ||||
| - Please do not open PRs to "discuss" possible solutions or to "get feedback" on your code. I simply don't have time for that. | ||||
| - Pull requests for the MAIN branch will be closed. | ||||
| - DO NOT include translated strings in your PR. | ||||
| - PRs (or parts thereof) that only fix issues inside code comments will not be accepted. | ||||
| 
 | ||||
| If it feels necessary to open an issue first, please do so, before you open a PR. | ||||
| 
 | ||||
| See also: https://docs.firefly-iii.org/explanation/support/#contributing-code | ||||
| 
 | ||||
| Thanks. | ||||
| --> | ||||
| 
 | ||||
| Fixes issue # (if relevant) | ||||
|      | ||||
| This PR fixes issue # (if relevant). | ||||
| 
 | ||||
| Changes in this pull request: | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										58
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,58 +0,0 @@ | ||||
| # Configuration for probot-stale - https://github.com/probot/stale | ||||
|  | ||||
| # Number of days of inactivity before an Issue or Pull Request becomes stale | ||||
| daysUntilStale: 14 | ||||
|  | ||||
| # Number of days of inactivity before a stale Issue or Pull Request is closed. | ||||
| # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. | ||||
| daysUntilClose: 14 | ||||
|  | ||||
| # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable | ||||
| # - "[Status] Maybe Later" | ||||
| exemptLabels: | ||||
|   - enhancement | ||||
|   - feature | ||||
|   - bug | ||||
|   - announcement | ||||
|   - "layout-v3" | ||||
|  | ||||
| # Set to true to ignore issues in a project (defaults to false) | ||||
| exemptProjects: false | ||||
|  | ||||
| # Set to true to ignore issues in a milestone (defaults to false) | ||||
| exemptMilestones: false | ||||
|  | ||||
| # Label to use when marking as stale | ||||
| staleLabel: stale | ||||
|  | ||||
| # Comment to post when marking as stale. Set to `false` to disable | ||||
| markComment: > | ||||
|   This issue has been automatically marked as stale because it has not had | ||||
|   recent activity. It will be closed if no further activity occurs. Thank you | ||||
|   for your contributions. | ||||
|  | ||||
| # Comment to post when removing the stale label. | ||||
| # unmarkComment: > | ||||
| #   Your comment here. | ||||
|  | ||||
| # Comment to post when closing a stale Issue or Pull Request. | ||||
| # closeComment: > | ||||
| #   Your comment here. | ||||
|  | ||||
| # Limit the number of actions per hour, from 1-30. Default is 30 | ||||
| limitPerRun: 30 | ||||
|  | ||||
| # Limit to only `issues` or `pulls` | ||||
| # only: issues | ||||
|  | ||||
| # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': | ||||
| # pulls: | ||||
| #   daysUntilStale: 30 | ||||
| #   markComment: > | ||||
| #     This pull request has been automatically marked as stale because it has not had | ||||
| #     recent activity. It will be closed if no further activity occurs. Thank you | ||||
| #     for your contributions. | ||||
|  | ||||
| # issues: | ||||
| #   exemptLabels: | ||||
| #     - confirmed | ||||
							
								
								
									
										4
									
								
								.github/workflows/close-duplicates.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/close-duplicates.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,7 @@ name: "Issues - Command to close duplicate issues" | ||||
| # the workflow to execute on is comments that are newly created | ||||
| on: | ||||
|   issue_comment: | ||||
|     types: [created] | ||||
|     types: [ created ] | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
| @@ -13,7 +13,7 @@ jobs: | ||||
|   close_duplicates: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: github/command@v1.1.0 | ||||
|       - uses: github/command@v1.2.1 | ||||
|         id: command | ||||
|         with: | ||||
|           allowed_contexts: "issue" | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/debug-info-actions.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/debug-info-actions.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,9 +3,9 @@ name: 'Issues - Respond to hidden commands' | ||||
| # the workflow to execute on is comments that are newly created | ||||
| on: | ||||
|   issues: | ||||
|     types: [opened, edited] | ||||
|     types: [ opened, edited ] | ||||
|   issue_comment: | ||||
|     types: [created] | ||||
|     types: [ created ] | ||||
|  | ||||
| # permissions needed for reacting to IssueOps commands on issues and PRs | ||||
| permissions: | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/label-actions.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/label-actions.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,11 +2,11 @@ name: 'Issues - Reply to specific labels' | ||||
|  | ||||
| on: | ||||
|   issues: | ||||
|     types: [labeled, unlabeled] | ||||
|     types: [ labeled, unlabeled ] | ||||
|   pull_request_target: | ||||
|     types: [labeled, unlabeled] | ||||
|     types: [ labeled, unlabeled ] | ||||
|   discussion: | ||||
|     types: [labeled, unlabeled] | ||||
|     types: [ labeled, unlabeled ] | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|   | ||||
							
								
								
									
										133
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										133
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,11 +4,11 @@ on: | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|       version: | ||||
|         description: 'Version to release' | ||||
|         description: 'Release "v1.2.3" or "develop"' | ||||
|         required: true | ||||
|         default: 'develop' | ||||
|   schedule: | ||||
|     - cron:  '0 3 * * MON,THU' | ||||
|     - cron: '0 3 * * MON' | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
| @@ -39,7 +39,7 @@ jobs: | ||||
|           php-version: '8.3' | ||||
|           extensions: mbstring, intl, zip, bcmath | ||||
|       - name: crowdin action | ||||
|         uses: crowdin/github-action@v1 | ||||
|         uses: crowdin/github-action@v2 | ||||
|         with: | ||||
|           upload_sources: true | ||||
|           download_translations: true | ||||
| @@ -51,7 +51,7 @@ jobs: | ||||
|           CROWDIN_TOKEN: ${{ secrets.CROWDIN_TOKEN }} | ||||
|       - name: Cleanup translations | ||||
|         id: cleanup-transactions | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:crowdin-warning' | ||||
|           output: '' | ||||
| @@ -60,16 +60,25 @@ jobs: | ||||
|           GH_TOKEN: '' | ||||
|       - name: Cleanup changelog | ||||
|         id: cleanup-changelog | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:changelog' | ||||
|           output: '' | ||||
|         env: | ||||
|           FIREFLY_III_ROOT: /github/workspace | ||||
|           GH_TOKEN: ${{ secrets.CHANGELOG_TOKEN }} | ||||
|       - name: "Create THANKS.md" | ||||
|         id: thank-you | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:thank-you' | ||||
|           output: '' | ||||
|         env: | ||||
|           FIREFLY_III_ROOT: /github/workspace | ||||
|           GH_TOKEN: '' | ||||
|       - name: Extract changelog | ||||
|         id: extract-changelog | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:extract-changelog' | ||||
|           output: 'output' | ||||
| @@ -78,7 +87,7 @@ jobs: | ||||
|           GH_TOKEN: "" | ||||
|       - name: Replace version | ||||
|         id: replace-version | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:version' | ||||
|           output: '' | ||||
| @@ -88,7 +97,7 @@ jobs: | ||||
|           FF_III_VERSION: ${{ github.event_name == 'schedule' && 'develop' || github.event.inputs.version }} | ||||
|       - name: Generate JSON v1 | ||||
|         id: json-v1 | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:json-translations v1' | ||||
|           output: '' | ||||
| @@ -97,7 +106,7 @@ jobs: | ||||
|           GH_TOKEN: '' | ||||
|       - name: Generate JSON v2 | ||||
|         id: json-v2 | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:json-translations v2' | ||||
|           output: '' | ||||
| @@ -106,57 +115,76 @@ jobs: | ||||
|           GH_TOKEN: '' | ||||
|       - name: Code cleanup | ||||
|         id: code-cleanup | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         uses: JC5/firefly-iii-dev@main | ||||
|         with: | ||||
|           action: 'ff3:code' | ||||
|           output: '' | ||||
|         env: | ||||
|           FIREFLY_III_ROOT: /github/workspace | ||||
|           GH_TOKEN: '' | ||||
|       - name: Build new JS | ||||
|       - name: Build JS | ||||
|         run: | | ||||
|           npm install | ||||
|           npm run prod  --workspace=v1 | ||||
|           npm run build --workspace=v2 | ||||
|           npm update | ||||
|           npm run build | ||||
|       - name: Build old JS | ||||
|         id: old-js | ||||
|         uses: JC5/firefly-iii-dev@v36 | ||||
|         with: | ||||
|           action: 'ff3:old-js' | ||||
|           output: '' | ||||
|         env: | ||||
|           FIREFLY_III_ROOT: /github/workspace | ||||
|           GH_TOKEN: '' | ||||
|       - name: Run CI | ||||
|         run: | | ||||
|           rm -rf vendor composer.lock | ||||
|           composer validate --strict | ||||
|           composer update --no-dev --no-scripts --no-plugins -q | ||||
|           sudo chown -R runner:docker resources/lang | ||||
|           .ci/phpcs.sh | ||||
|       - name: Import GPG key | ||||
|         uses: crazy-max/ghaction-import-gpg@v6 | ||||
|         with: | ||||
|           gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} | ||||
|           passphrase: ${{ secrets.PASSPHRASE }} | ||||
|       - name: Release | ||||
|         run: | | ||||
|           # do some configuration | ||||
|           sudo timedatectl set-timezone Europe/Amsterdam | ||||
|           git config user.name github-actions | ||||
|           git config user.email 41898282+github-actions[bot]@users.noreply.github.com | ||||
|           git config advice.addIgnoredFile false | ||||
|  | ||||
|           # update composer (again) | ||||
|           composer validate --strict | ||||
|           composer update --no-dev --no-scripts --no-plugins | ||||
|           composer dump-autoload | ||||
|  | ||||
|           # set some variables | ||||
|           releaseName=$version | ||||
|           originalName=$version | ||||
|           zipName=FireflyIII-$version.zip | ||||
|           tarName=FireflyIII-$version.tar.gz | ||||
|  | ||||
|           # update composer (again) | ||||
|           composer update --no-dev --no-scripts --no-plugins | ||||
|           composer dump-autoload | ||||
|  | ||||
|           # if this is a develop build, slightly different variable names. | ||||
|           if [[ "develop" == "$version" ]]; then | ||||
|             [[ -z $(git status --untracked-files=normal --porcelain) ]] && echo "this branch is clean, no need to push..." && exit 0; | ||||
|             releaseName=$version-$(date +'%Y%m%d') | ||||
|             originalName=$releaseName | ||||
|             zipName=FireflyIII-develop.zip | ||||
|             tarName=FireflyIII-develop.tar.gz | ||||
|           fi | ||||
|  | ||||
|           # in both cases, if the release or tag already exists, add ".1" until it no longer exists. | ||||
|           tagFound=true | ||||
|           tagCount=1 | ||||
|           while [ "$tagFound" = true ] | ||||
|           do | ||||
|             if [ $(git tag -l "$releaseName") ]; then | ||||
|               echo "Tag $releaseName exists already." | ||||
|               releaseName="$originalName"."$tagCount" | ||||
|               echo "Tag for release is now $releaseName" | ||||
|               tagCount=$((tagCount+1)) | ||||
|             else | ||||
|              echo "Tag $releaseName does not exist, can continue" | ||||
|              tagFound=false | ||||
|             fi | ||||
|           done | ||||
|           echo "Will use tag and release name $releaseName." | ||||
|  | ||||
|           # add all content, except output.txt (this contains the changelog and/or the download instructions) | ||||
|           echo 'Add all and reset output.txt' | ||||
|           git add -A | ||||
|           if test -f "output.txt"; then | ||||
|             git reset output.txt | ||||
| @@ -165,25 +193,33 @@ jobs: | ||||
|           git push | ||||
|  | ||||
|           # zip and tar everything | ||||
|           echo 'Zip and tar...' | ||||
|           zip -rq $zipName . -x "*.git*" "*.ci*" "*.github*" "*node_modules*" "*output.txt*" | ||||
|           touch $tarName | ||||
|           tar --exclude=$tarName --exclude='./.git' --exclude='./.ci' --exclude='./.github' --exclude='./node_modules' --exclude='./output.txt' -czf $tarName . | ||||
|           tar --exclude=$tarName --exclude=$zipName --exclude='./.git' --exclude='./.ci' --exclude='./.github' --exclude='./node_modules' --exclude='./output.txt' -czf $tarName . | ||||
|  | ||||
|           # add sha256 sum | ||||
|           echo 'Sha sum ...' | ||||
|           sha256sum -b $zipName > $zipName.sha256 | ||||
|           sha256sum -b $tarName > $tarName.sha256 | ||||
|  | ||||
|           # add signatures: | ||||
|           gpg --armor --detach-sign $zipName | ||||
|           gpg --armor --detach-sign $tarName | ||||
|  | ||||
|           # create a development (nightly) release: | ||||
|           if [[ "develop" == "$version" ]]; then | ||||
|             echo 'Develop release.' | ||||
|             # add text to output.txt (instructions) | ||||
|             rm output.txt | ||||
|             echo "Bi-weekly development release of Firefly III with the latest fixes, translations and features. Docker users can find this release under the \`develop\` tag." >> output.txt | ||||
|             echo "" >> output.txt | ||||
|             echo "This release was created on **$(date +'%Y-%m-%d')** and may contain unexpected bugs. Data loss is rare but is not impossible." >> output.txt | ||||
|             echo "This release was created on **$(date +'%Y-%m-%d')** and may contain unexpected bugs. Data loss is rare but is not impossible. The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/)." >> output.txt | ||||
|             echo "" >> output.txt | ||||
|             echo "* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt | ||||
|             echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt | ||||
|             echo "" >> output.txt | ||||
|             echo ":warning: Please be careful with this pre-release, as is may not work as expected." >> output.txt | ||||
|             echo ":warning: Please be careful with this pre-release, as it may not work as expected." >> output.txt | ||||
|  | ||||
|             # create the release: | ||||
|             echo "Create nightly release." | ||||
| @@ -201,27 +237,56 @@ jobs: | ||||
|             gh release upload $releaseName $zipName.sha256 | ||||
|             gh release upload $releaseName $tarName.sha256 | ||||
|  | ||||
|             # rm output.txt again | ||||
|             rm output.txt | ||||
|             # add signatures to release | ||||
|             gh release upload $releaseName $zipName.asc | ||||
|             gh release upload $releaseName $tarName.asc | ||||
|  | ||||
|             # get current HEAD and add as file to the release | ||||
|             HEAD=$(git rev-parse HEAD) | ||||
|             echo $HEAD > HEAD.txt | ||||
|             gh release upload $releaseName HEAD.txt | ||||
|           else | ||||
|             echo 'MAIN (real) release' | ||||
|             sudo chown -R runner:docker output.txt | ||||
|             # add text to output.txt (more instructions) | ||||
|             echo '' >> output.txt | ||||
|             echo '### Instructions' >> output.txt | ||||
|             echo '' >> output.txt | ||||
|             echo "* Installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt | ||||
|             echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt | ||||
|             echo "* The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/)." >> output.txt | ||||
|  | ||||
|             echo "Create default release." | ||||
|             git tag -a $releaseName -m "Here be changelog" | ||||
|             git push origin $releaseName | ||||
|             gh release create $releaseName -F output.txt -t "$releaseName" --verify-tag | ||||
|             # add zip file to release. | ||||
|  | ||||
|             # add archive files to release | ||||
|             gh release upload $releaseName $zipName | ||||
|             # add sha256 sum to release | ||||
|             gh release upload $releaseName $tarName | ||||
|  | ||||
|             # add sha256 sums to release | ||||
|             gh release upload $releaseName $zipName.sha256 | ||||
|             gh release upload $releaseName $tarName.sha256 | ||||
|  | ||||
|             # add signatures to release | ||||
|             gh release upload $releaseName $zipName.asc | ||||
|             gh release upload $releaseName $tarName.asc | ||||
|  | ||||
|             # get current HEAD and add as file to the release | ||||
|             HEAD=$(git rev-parse HEAD) | ||||
|             echo $HEAD > HEAD.txt | ||||
|             gh release upload $releaseName HEAD.txt | ||||
|  | ||||
|             # remove all temporary files | ||||
|             rm output.txt | ||||
|             rm HEAD.txt | ||||
|             rm $zipName | ||||
|             rm $zipName.sha256 | ||||
|             rm $tarName | ||||
|             rm $tarName.sha256 | ||||
|  | ||||
|             # merge main back into develop | ||||
|             git checkout develop | ||||
|             git merge main | ||||
|             git push | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|       - uses: actions/stale@v9 | ||||
|         with: | ||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           stale-issue-message: > | ||||
|           stale-issue-message: | | ||||
|             Hi there! | ||||
|  | ||||
|             This is an automatic reply. `Share and enjoy` | ||||
| @@ -25,7 +25,7 @@ jobs: | ||||
|             This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. | ||||
|  | ||||
|             Thank you for your contributions. | ||||
|           stale-pr-message: > | ||||
|           stale-pr-message: | | ||||
|             Hi there! | ||||
|  | ||||
|             This is an automatic reply. `Share and enjoy` | ||||
|   | ||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -10,3 +10,11 @@ coverage.xml | ||||
|  | ||||
| # ignore generated files. | ||||
| public/build | ||||
|  | ||||
| # ignore v1 build files | ||||
| resources/assets/v1/node_modules | ||||
| resources/assets/v1/build | ||||
|  | ||||
| # ignore v2 build files | ||||
| resources/assets/v2/node_modules | ||||
| resources/assets/v2/build | ||||
|   | ||||
							
								
								
									
										198
									
								
								THANKS.md
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										198
									
								
								THANKS.md
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| # Thank you! :tada: :heart: :tada: | ||||
| 
 | ||||
| Over time, many people have contributed to Firefly III. Their efforts are not always visible, but always remembered and appreciated. | ||||
| Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution. | ||||
| 
 | ||||
| ## 2024 | ||||
| - Jhon Pedroza | ||||
| - mzhubail | ||||
| - tasnim | ||||
| - withbest | ||||
| - Steve Wasiura | ||||
| - imlonghao | ||||
| - Rahman Yusuf | ||||
| - Michael Thomas | ||||
| - WardenJakx | ||||
| - kuilin | ||||
| - Stevie Robinson | ||||
| - luzpaz | ||||
| - Lemuel Roberto Bonifácio | ||||
| - maureenferreira | ||||
| 
 | ||||
| ## 2023 | ||||
| - tieu1991 | ||||
| - Maxco10 | ||||
| - zqye | ||||
| - Mateus Pereira | ||||
| - josephbadow | ||||
| - Christian Desktop | ||||
| - Edgars | ||||
| - Hannah K | ||||
| - noxonad | ||||
| - Kaijia Feng | ||||
| - Marc Ordinas i Llopis | ||||
| - Kuba Turek | ||||
| - Julien Stébenne | ||||
| 
 | ||||
| ## 2022 | ||||
| - Johannes Zellner | ||||
| - Janne Heß | ||||
| - charlesteets | ||||
| - Nathan PERIER | ||||
| - Jan Willhaus | ||||
| - canoine | ||||
| - Rick Cuddy | ||||
| - James | ||||
| - Hugo Meyronneinc | ||||
| - naveen | ||||
| - neilnaveen | ||||
| - naveensrinivasan | ||||
| - Federico Micelli | ||||
| - George Hahn | ||||
| 
 | ||||
| ## 2021 | ||||
| - StillLoading | ||||
| - Igor Rzegocki | ||||
| - Lorenzo Breda | ||||
| - Hosh | ||||
| - Flightkick | ||||
| - alex6480 | ||||
| - VREEdom | ||||
| - Hamza FADIL | ||||
| - Kasper Læssø Sørensen | ||||
| - Alex | ||||
| - Jeroen De Meerleer | ||||
| - Ruben van Erk | ||||
| - Fabian Zimmermann | ||||
| - Mirko Berger | ||||
| - KaihatsuOnline | ||||
| - MihataBG | ||||
| 
 | ||||
| ## 2020 | ||||
| - Hannes Körber | ||||
| - Julien Cassagne | ||||
| - bu4ak | ||||
| - Viktor Yakovlev | ||||
| - Oliver Kaufmann | ||||
| - Arvind Chembarpu | ||||
| - GrayStrider | ||||
| - psychowood | ||||
| - Hosh Sadiq | ||||
| - emansih | ||||
| - Aniruddha Maru | ||||
| - johnny | ||||
| - sephrat | ||||
| - bpatath | ||||
| - Florian Dupret | ||||
| - Maxim Kurbatov | ||||
| - Lucas Guima | ||||
| - Sandro | ||||
| - Ruben Verhoef | ||||
| - Daniel Idzerda | ||||
| - Calum Smith | ||||
| - Agraphie | ||||
| - Tomer Shvueli | ||||
| - Tomer S | ||||
| 
 | ||||
| ## 2019 | ||||
| - Pascal Jungblut | ||||
| - Justyn Shull | ||||
| - Timendum | ||||
| - Nicolas Lœuillet | ||||
| - Dominic Guhl | ||||
| - Melroy van den Berg | ||||
| - Henning Stein | ||||
| - Jan Klepek | ||||
| - Jonathan | ||||
| - Geoffrey “Frogeye” Preud'homme | ||||
| - Michael Fix | ||||
| - Juraj Mlich | ||||
| - Eddybrando Vásquez | ||||
| - hulloanson | ||||
| - Will Rouesnel | ||||
| - lastlink | ||||
| - Mr. Funk | ||||
| - Simon Taddiken | ||||
| - Joris | ||||
| - Bastiaan Nijkamp | ||||
| 
 | ||||
| ## 2018 | ||||
| - a1ex4 | ||||
| - Daniel Quah | ||||
| - Marco Lourenço | ||||
| - Dennis Enderink | ||||
| - Luca Bognolo | ||||
| - Mike Conway | ||||
| - Ben | ||||
| - Mathieu Post | ||||
| - George Hertz | ||||
| - HamuZ HamuZ | ||||
| - David Meiseles | ||||
| - Erik Gelderblom | ||||
| - Luca Vallerini | ||||
| - Clemens Wijnekus | ||||
| - Jacob Weisz | ||||
| - Mateusz Gozdek | ||||
| - anmol26s | ||||
| - Kevin Hellemun | ||||
| - Shashank M Chakravarthy | ||||
| - Nico Schreiner | ||||
| - Paul Sohier | ||||
| - Brenden Conte | ||||
| - Ben Yanke | ||||
| - Andrew Prokhorenkov | ||||
| - devlearner | ||||
| - Kelvin | ||||
| - J'informatique | ||||
| 
 | ||||
| ## 2017 | ||||
| - Victor Mosin | ||||
| - Justin | ||||
| - Hugo van Duijn | ||||
| - Lukas Winkler | ||||
| - Marcin Szymanski | ||||
| - Jens Kat | ||||
| - koziolek | ||||
| - jleeong | ||||
| - Simon Hanna | ||||
| - richard & xeli.eu | ||||
| - Sergey Besedin | ||||
| - Welbert Serra | ||||
| - Joris de Vries | ||||
| - Patrick Kostjens | ||||
| - Enrico Lamperti | ||||
| - Christian Musa | ||||
| - Enno Lohmeier | ||||
| 
 | ||||
| ## 2016 | ||||
| - Sander | ||||
| - Toon Schoenmakers | ||||
| - Telyn | ||||
| - Sander Kleykens | ||||
| - Tom van der Werf | ||||
| - Matthew Peck | ||||
| - Sander Mulders | ||||
| - Bonno Nachtegaal-Karels | ||||
| - Niek Haarman | ||||
| - Edwin | ||||
| - Thijs Alkemade | ||||
| - zjean | ||||
| - Graham Miller | ||||
| - Robert Horlings | ||||
| - leander091 | ||||
| 
 | ||||
| ## 2015 | ||||
| - Antonio Spinelli | ||||
| - Colin O'Dell | ||||
| - RonaldvanMeer | ||||
| - Richard Ebbers | ||||
| - Balazs Varkonyi | ||||
| - Niek van der Kooy | ||||
| - Ilya Kil | ||||
| 
 | ||||
| ## 2014 | ||||
| - Stewart Malik | ||||
| - Graham Campbell | ||||
| 
 | ||||
| 
 | ||||
| Thank you for all your support! | ||||
| @@ -83,17 +83,17 @@ class AccountController extends Controller | ||||
|         // user's preferences
 | ||||
|         $defaultSet = $this->repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray(); | ||||
| 
 | ||||
|         /** @var Preference $frontPage */ | ||||
|         $frontPage  = app('preferences')->get('frontPageAccounts', $defaultSet); | ||||
|         /** @var Preference $frontpage */ | ||||
|         $frontpage  = app('preferences')->get('frontpageAccounts', $defaultSet); | ||||
|         $default    = app('amount')->getDefaultCurrency(); | ||||
| 
 | ||||
|         if (!(is_array($frontPage->data) && count($frontPage->data) > 0)) { | ||||
|             $frontPage->data = $defaultSet; | ||||
|             $frontPage->save(); | ||||
|         if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) { | ||||
|             $frontpage->data = $defaultSet; | ||||
|             $frontpage->save(); | ||||
|         } | ||||
| 
 | ||||
|         // get accounts:
 | ||||
|         $accounts   = $this->repository->getAccountsById($frontPage->data); | ||||
|         $accounts   = $this->repository->getAccountsById($frontpage->data); | ||||
|         $chartData  = []; | ||||
| 
 | ||||
|         /** @var Account $account */ | ||||
|   | ||||
| @@ -69,9 +69,15 @@ class UpdateController extends Controller | ||||
|      */ | ||||
|     public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse | ||||
|     { | ||||
|         app('log')->debug('Now in update routine for transaction group!'); | ||||
|         app('log')->debug('Now in update routine for transaction group'); | ||||
|         $data             = $request->getAll(); | ||||
| 
 | ||||
|         // Fixes 8750.
 | ||||
|         $transactions     = $data['transactions'] ?? []; | ||||
|         foreach ($transactions as $index => $info) { | ||||
|             unset($data['transactions'][$index]['type']); | ||||
|         } | ||||
| 
 | ||||
|         $transactionGroup = $this->groupRepository->update($transactionGroup, $data); | ||||
|         $manager          = $this->getManager(); | ||||
| 
 | ||||
|   | ||||
| @@ -34,6 +34,7 @@ use FireflyIII\Support\Http\Api\TransactionFilter; | ||||
| use FireflyIII\Transformers\CurrencyTransformer; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use League\Fractal\Resource\Item; | ||||
| 
 | ||||
| /** | ||||
| @@ -164,6 +165,7 @@ class UpdateController extends Controller | ||||
|     public function update(UpdateRequest $request, TransactionCurrency $currency): JsonResponse | ||||
|     { | ||||
|         $data        = $request->getAll(); | ||||
|         Log::debug(__METHOD__, $data); | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user        = auth()->user(); | ||||
| @@ -173,6 +175,11 @@ class UpdateController extends Controller | ||||
|         if (array_key_exists('enabled', $data) && false === $data['enabled'] && 1 === count($set) && $set->first()->id === $currency->id) { | ||||
|             return response()->json([], 409); | ||||
|         } | ||||
|         // second safety catch on currency disable.
 | ||||
|         if (array_key_exists('enabled', $data) && false === $data['enabled'] && $this->repository->currencyInUse($currency)) { | ||||
|             return response()->json([], 409); | ||||
|         } | ||||
| 
 | ||||
|         $currency    = $this->repository->update($currency, $data); | ||||
| 
 | ||||
|         app('preferences')->mark(); | ||||
|   | ||||
| @@ -32,6 +32,7 @@ use FireflyIII\Models\Preference; | ||||
| use FireflyIII\Transformers\PreferenceTransformer; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Pagination\LengthAwarePaginator; | ||||
| use Illuminate\Support\Collection; | ||||
| use League\Fractal\Pagination\IlluminatePaginatorAdapter; | ||||
| use League\Fractal\Resource\Collection as FractalCollection; | ||||
| use League\Fractal\Resource\Item; | ||||
| @@ -84,6 +85,10 @@ class PreferencesController extends Controller | ||||
|     { | ||||
|         $manager     = $this->getManager(); | ||||
| 
 | ||||
|         if ('currencyPreference' === $preference->name) { | ||||
|             throw new FireflyException('Please use api/v1/currencies/default instead.'); | ||||
|         } | ||||
| 
 | ||||
|         /** @var PreferenceTransformer $transformer */ | ||||
|         $transformer = app(PreferenceTransformer::class); | ||||
|         $transformer->setParameters($this->parameters); | ||||
| @@ -93,6 +98,34 @@ class PreferencesController extends Controller | ||||
|         return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * TODO This endpoint is not documented. | ||||
|      * | ||||
|      * Return a single preference by name. | ||||
|      * | ||||
|      * @param Collection<int, Preference> $collection | ||||
|      */ | ||||
|     public function showList(Collection $collection): JsonResponse | ||||
|     { | ||||
|         $manager     = $this->getManager(); | ||||
|         $count       = $collection->count(); | ||||
|         $pageSize    = $this->parameters->get('limit'); | ||||
|         $preferences = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); | ||||
| 
 | ||||
|         // make paginator:
 | ||||
|         $paginator   = new LengthAwarePaginator($preferences, $count, $pageSize, $this->parameters->get('page')); | ||||
|         $paginator->setPath(route('api.v1.preferences.show-list').$this->buildParams()); | ||||
| 
 | ||||
|         /** @var PreferenceTransformer $transformer */ | ||||
|         $transformer = app(PreferenceTransformer::class); | ||||
|         $transformer->setParameters($this->parameters); | ||||
| 
 | ||||
|         $resource    = new FractalCollection($preferences, $transformer, self::RESOURCE_KEY); | ||||
|         $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); | ||||
| 
 | ||||
|         return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This endpoint is documented at: | ||||
|      * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/preferences/storePreference
 | ||||
| @@ -103,6 +136,11 @@ class PreferencesController extends Controller | ||||
|     { | ||||
|         $manager     = $this->getManager(); | ||||
|         $data        = $request->getAll(); | ||||
| 
 | ||||
|         if ('currencyPreference' === $data['name']) { | ||||
|             throw new FireflyException('Please use api/v1/currencies/default instead.'); | ||||
|         } | ||||
| 
 | ||||
|         $pref        = app('preferences')->set($data['name'], $data['data']); | ||||
| 
 | ||||
|         /** @var PreferenceTransformer $transformer */ | ||||
| @@ -122,6 +160,10 @@ class PreferencesController extends Controller | ||||
|      */ | ||||
|     public function update(PreferenceUpdateRequest $request, Preference $preference): JsonResponse | ||||
|     { | ||||
|         if ('currencyPreference' === $preference->name) { | ||||
|             throw new FireflyException('Please use api/v1/currencies/default instead.'); | ||||
|         } | ||||
| 
 | ||||
|         $manager     = $this->getManager(); | ||||
|         $data        = $request->getAll(); | ||||
|         $pref        = app('preferences')->set($preference->name, $data['data']); | ||||
|   | ||||
| @@ -63,7 +63,7 @@ class StoreRequest extends FormRequest | ||||
|             'order'                   => $this->convertInteger('order'), | ||||
|             'currency_code'           => $this->convertString('currency_code'), | ||||
|             'virtual_balance'         => $this->convertString('virtual_balance'), | ||||
|             'iban'                    => $this->convertString('iban'), | ||||
|             'iban'                    => $this->convertIban('iban'), | ||||
|             'BIC'                     => $this->convertString('bic'), | ||||
|             'account_number'          => $this->convertString('account_number'), | ||||
|             'account_role'            => $this->convertString('account_role'), | ||||
|   | ||||
| @@ -51,7 +51,7 @@ class UpdateRequest extends FormRequest | ||||
|             'include_net_worth'       => ['include_net_worth', 'boolean'], | ||||
|             'account_type_name'       => ['type', 'convertString'], | ||||
|             'virtual_balance'         => ['virtual_balance', 'convertString'], | ||||
|             'iban'                    => ['iban', 'convertString'], | ||||
|             'iban'                    => ['iban', 'convertIban'], | ||||
|             'BIC'                     => ['bic', 'convertString'], | ||||
|             'account_number'          => ['account_number', 'convertString'], | ||||
|             'account_role'            => ['account_role', 'convertString'], | ||||
|   | ||||
| @@ -58,7 +58,7 @@ class StoreRequest extends FormRequest | ||||
|         $models = config('firefly.valid_attachment_models'); | ||||
|         $models = array_map( | ||||
|             static function (string $className) { | ||||
|                 return str_replace('FireflyIII\\Models\\', '', $className); | ||||
|                 return str_replace('FireflyIII\Models\\', '', $className); | ||||
|             }, | ||||
|             $models | ||||
|         ); | ||||
|   | ||||
| @@ -60,7 +60,7 @@ class UpdateRequest extends FormRequest | ||||
|         $models = config('firefly.valid_attachment_models'); | ||||
|         $models = array_map( | ||||
|             static function (string $className) { | ||||
|                 return str_replace('FireflyIII\\Models\\', '', $className); | ||||
|                 return str_replace('FireflyIII\Models\\', '', $className); | ||||
|             }, | ||||
|             $models | ||||
|         ); | ||||
|   | ||||
| @@ -71,8 +71,9 @@ class StoreRequest extends FormRequest | ||||
|         if (is_array($triggers)) { | ||||
|             foreach ($triggers as $trigger) { | ||||
|                 $return[] = [ | ||||
|                     'type'            => $trigger['type'], | ||||
|                     'value'           => $trigger['value'], | ||||
|                     'type'            => $trigger['type'] ?? '', | ||||
|                     'value'           => $trigger['value'] ?? null, | ||||
|                     'prohibited'      => $this->convertBoolean((string)($trigger['prohibited'] ?? 'false')), | ||||
|                     'active'          => $this->convertBoolean((string)($trigger['active'] ?? 'true')), | ||||
|                     'stop_processing' => $this->convertBoolean((string)($trigger['stop_processing'] ?? 'false')), | ||||
|                 ]; | ||||
|   | ||||
| @@ -54,17 +54,13 @@ class TestRequest extends FormRequest | ||||
| 
 | ||||
|     private function getDate(string $field): ?Carbon | ||||
|     { | ||||
|         $value  = $this->query($field); | ||||
|         $value = $this->query($field); | ||||
|         if (is_array($value)) { | ||||
|             return null; | ||||
|         } | ||||
|         $value  = (string)$value; | ||||
|         $result = null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|         if (false === $result) { | ||||
|             return null; | ||||
|         } | ||||
|         $value = (string)$value; | ||||
| 
 | ||||
|         return $result; | ||||
|         return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|     } | ||||
| 
 | ||||
|     private function getAccounts(): array | ||||
|   | ||||
| @@ -48,17 +48,13 @@ class TriggerRequest extends FormRequest | ||||
| 
 | ||||
|     private function getDate(string $field): ?Carbon | ||||
|     { | ||||
|         $value  = $this->query($field); | ||||
|         $value = $this->query($field); | ||||
|         if (is_array($value)) { | ||||
|             return null; | ||||
|         } | ||||
|         $value  = (string)$value; | ||||
|         $result = null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|         if (false === $result) { | ||||
|             return null; | ||||
|         } | ||||
|         $value = (string)$value; | ||||
| 
 | ||||
|         return $result; | ||||
|         return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|     } | ||||
| 
 | ||||
|     private function getAccounts(): array | ||||
|   | ||||
| @@ -81,10 +81,12 @@ class UpdateRequest extends FormRequest | ||||
|         if (is_array($triggers)) { | ||||
|             foreach ($triggers as $trigger) { | ||||
|                 $active         = array_key_exists('active', $trigger) ? $trigger['active'] : true; | ||||
|                 $prohibited     = array_key_exists('prohibited', $trigger) ? $trigger['prohibited'] : false; | ||||
|                 $stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false; | ||||
|                 $return[]       = [ | ||||
|                     'type'            => $trigger['type'], | ||||
|                     'value'           => $trigger['value'], | ||||
|                     'prohibited'      => $prohibited, | ||||
|                     'active'          => $active, | ||||
|                     'stop_processing' => $stopProcessing, | ||||
|                 ]; | ||||
|   | ||||
| @@ -48,17 +48,13 @@ class TestRequest extends FormRequest | ||||
| 
 | ||||
|     private function getDate(string $field): ?Carbon | ||||
|     { | ||||
|         $value  = $this->query($field); | ||||
|         $value = $this->query($field); | ||||
|         if (is_array($value)) { | ||||
|             return null; | ||||
|         } | ||||
|         $value  = (string)$value; | ||||
|         $result = null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|         if (false === $result) { | ||||
|             return null; | ||||
|         } | ||||
|         $value = (string)$value; | ||||
| 
 | ||||
|         return $result; | ||||
|         return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|     } | ||||
| 
 | ||||
|     private function getAccounts(): array | ||||
|   | ||||
| @@ -48,17 +48,13 @@ class TriggerRequest extends FormRequest | ||||
| 
 | ||||
|     private function getDate(string $field): ?Carbon | ||||
|     { | ||||
|         $value  = $this->query($field); | ||||
|         $value = $this->query($field); | ||||
|         if (is_array($value)) { | ||||
|             return null; | ||||
|         } | ||||
|         $value  = (string)$value; | ||||
|         $result = null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|         if (false === $result) { | ||||
|             return null; | ||||
|         } | ||||
|         $value = (string)$value; | ||||
| 
 | ||||
|         return $result; | ||||
|         return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10)); | ||||
|     } | ||||
| 
 | ||||
|     private function getAccounts(): array | ||||
|   | ||||
| @@ -103,14 +103,14 @@ class StoreRequest extends FormRequest | ||||
|                 // source of transaction. If everything is null, assume cash account.
 | ||||
|                 'source_id'             => $this->integerFromValue((string)$object['source_id']), | ||||
|                 'source_name'           => $this->clearString((string)$object['source_name']), | ||||
|                 'source_iban'           => $this->clearString((string)$object['source_iban']), | ||||
|                 'source_iban'           => $this->clearIban((string)$object['source_iban']), | ||||
|                 'source_number'         => $this->clearString((string)$object['source_number']), | ||||
|                 'source_bic'            => $this->clearString((string)$object['source_bic']), | ||||
| 
 | ||||
|                 // destination of transaction. If everything is null, assume cash account.
 | ||||
|                 'destination_id'        => $this->integerFromValue((string)$object['destination_id']), | ||||
|                 'destination_name'      => $this->clearString((string)$object['destination_name']), | ||||
|                 'destination_iban'      => $this->clearString((string)$object['destination_iban']), | ||||
|                 'destination_iban'      => $this->clearIban((string)$object['destination_iban']), | ||||
|                 'destination_number'    => $this->clearString((string)$object['destination_number']), | ||||
|                 'destination_bic'       => $this->clearString((string)$object['destination_bic']), | ||||
| 
 | ||||
|   | ||||
| @@ -28,22 +28,21 @@ use FireflyIII\Api\V2\Controllers\Controller; | ||||
| use FireflyIII\Api\V2\Request\Autocomplete\AutocompleteRequest; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Models\AccountType; | ||||
| use FireflyIII\Repositories\Account\AccountRepositoryInterface; | ||||
| use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface; | ||||
| use FireflyIII\Support\Http\Api\AccountFilter; | ||||
| use FireflyIII\Models\AccountBalance; | ||||
| use FireflyIII\Models\TransactionCurrency; | ||||
| use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; | ||||
| use FireflyIII\Support\Http\Api\ExchangeRateConverter; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class AccountController | ||||
|  */ | ||||
| class AccountController extends Controller | ||||
| { | ||||
|     use AccountFilter; | ||||
| 
 | ||||
|     private AdminAccountRepositoryInterface $adminRepository; | ||||
|     private array                           $balanceTypes; | ||||
|     private AccountRepositoryInterface      $repository; | ||||
|     private AccountRepositoryInterface $repository; | ||||
|     private TransactionCurrency        $default; | ||||
|     private ExchangeRateConverter      $converter; | ||||
| 
 | ||||
|     /** | ||||
|      * AccountController constructor. | ||||
| @@ -53,83 +52,88 @@ class AccountController extends Controller | ||||
|         parent::__construct(); | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository      = app(AccountRepositoryInterface::class); | ||||
|                 $this->adminRepository = app(AdminAccountRepositoryInterface::class); | ||||
| 
 | ||||
|                 $userGroup             = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->adminRepository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 $this->repository = app(AccountRepositoryInterface::class); | ||||
|                 $this->repository->setUserGroup($userGroup); | ||||
|                 $this->default    = app('amount')->getDefaultCurrency(); | ||||
|                 $this->converter  = app(ExchangeRateConverter::class); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|         ); | ||||
|         $this->balanceTypes = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Documentation for this endpoint: | ||||
|      * TODO list of checks | ||||
|      * 1. use dates from ParameterBag | ||||
|      * 2. Request validates dates | ||||
|      * 3. Request includes user_group_id | ||||
|      * 4. Endpoint is documented. | ||||
|      * 5. Collector uses user_group_id | ||||
|      * | ||||
|      * @throws FireflyException | ||||
|      * @throws FireflyException | ||||
|      * Documentation: https://api-docs.firefly-iii.org/?urls.primaryName=2.1.0%20(v2)#/autocomplete/getAccountsAC
 | ||||
|      */ | ||||
|     public function accounts(AutocompleteRequest $request): JsonResponse | ||||
|     { | ||||
|         $data            = $request->getData(); | ||||
|         $types           = $data['types']; | ||||
|         $query           = $data['query']; | ||||
|         $date            = $this->parameters->get('date') ?? today(config('app.timezone')); | ||||
|         $result          = $this->adminRepository->searchAccount((string)$query, $types, $data['limit']); | ||||
|         $defaultCurrency = app('amount')->getDefaultCurrency(); | ||||
|         $groupedResult   = []; | ||||
|         $allItems        = []; | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $result          = $this->repository->searchAccount($queryParameters['query'], $queryParameters['account_types'], $queryParameters['size']); | ||||
|         $return          = []; | ||||
| 
 | ||||
|         /** @var Account $account */ | ||||
|         foreach ($result as $account) { | ||||
|             $nameWithBalance = $account->name; | ||||
|             $currency        = $this->repository->getAccountCurrency($account) ?? $defaultCurrency; | ||||
| 
 | ||||
|             if (in_array($account->accountType->type, $this->balanceTypes, true)) { | ||||
|                 $balance         = app('steam')->balance($account, $date); | ||||
|                 $nameWithBalance = sprintf('%s (%s)', $account->name, app('amount')->formatAnything($currency, $balance, false)); | ||||
|             } | ||||
|             $type            = (string)trans(sprintf('firefly.%s', $account->accountType->type)); | ||||
|             $groupedResult[$type] ??= [ | ||||
|                 'group ' => $type, | ||||
|                 'items'  => [], | ||||
|             ]; | ||||
|             $allItems[]      = [ | ||||
|                 'id'                      => (string)$account->id, | ||||
|                 'value'                   => (string)$account->id, | ||||
|                 'name'                    => $account->name, | ||||
|                 'name_with_balance'       => $nameWithBalance, | ||||
|                 'label'                   => $nameWithBalance, | ||||
|                 'type'                    => $account->accountType->type, | ||||
|                 'currency_id'             => (string)$currency->id, | ||||
|                 'currency_name'           => $currency->name, | ||||
|                 'currency_code'           => $currency->code, | ||||
|                 'currency_symbol'         => $currency->symbol, | ||||
|                 'currency_decimal_places' => $currency->decimal_places, | ||||
|             ]; | ||||
|             $return[] = $this->parseAccount($account); | ||||
|         } | ||||
| 
 | ||||
|         usort( | ||||
|             $allItems, | ||||
|             static function (array $left, array $right): int { | ||||
|                 $order    = [AccountType::ASSET, AccountType::REVENUE, AccountType::EXPENSE]; | ||||
|                 $posLeft  = (int)array_search($left['type'], $order, true); | ||||
|                 $posRight = (int)array_search($right['type'], $order, true); | ||||
|         return response()->json($return); | ||||
|     } | ||||
| 
 | ||||
|                 return $posLeft - $posRight; | ||||
|     private function parseAccount(Account $account): array | ||||
|     { | ||||
|         $currency = $this->repository->getAccountCurrency($account); | ||||
| 
 | ||||
|         return [ | ||||
|             'id'    => (string) $account->id, | ||||
|             'title' => $account->name, | ||||
|             'meta'  => [ | ||||
|                 'type'                    => $account->accountType->type, | ||||
|                 'currency_id'             => null === $currency ? null : (string) $currency->id, | ||||
|                 'currency_code'           => $currency?->code, | ||||
|                 'currency_symbol'         => $currency?->symbol, | ||||
|                 'currency_decimal_places' => $currency?->decimal_places, | ||||
|                 'account_balances'        => $this->getAccountBalances($account), | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     private function getAccountBalances(Account $account): array | ||||
|     { | ||||
|         $return   = []; | ||||
|         $balances = $this->repository->getAccountBalances($account); | ||||
| 
 | ||||
|         /** @var AccountBalance $balance */ | ||||
|         foreach ($balances as $balance) { | ||||
|             try { | ||||
|                 $return[] = $this->parseAccountBalance($balance); | ||||
|             } catch (FireflyException $e) { | ||||
|                 Log::error(sprintf('Could not parse convert account balance: %s', $e->getMessage())); | ||||
|             } | ||||
|         ); | ||||
|         } | ||||
| 
 | ||||
|         return response()->json($allItems); | ||||
|         return $return; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     private function parseAccountBalance(AccountBalance $balance): array | ||||
|     { | ||||
|         $currency = $balance->transactionCurrency; | ||||
| 
 | ||||
|         return [ | ||||
|             'title'                          => $balance->title, | ||||
|             'native_amount'                  => $this->converter->convert($currency, $this->default, today(), $balance->balance), | ||||
|             'amount'                         => app('steam')->bcround($balance->balance, $currency->decimal_places), | ||||
|             'currency_id'                    => (string) $currency->id, | ||||
|             'currency_code'                  => $currency->code, | ||||
|             'currency_symbol'                => $currency->symbol, | ||||
|             'currency_decimal_places'        => $currency->decimal_places, | ||||
|             'native_currency_id'             => (string) $this->default->id, | ||||
|             'native_currency_code'           => $this->default->code, | ||||
|             'native_currency_symbol'         => $this->default->symbol, | ||||
|             'native_currency_decimal_places' => $this->default->decimal_places, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -45,11 +45,7 @@ class CategoryController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(CategoryRepositoryInterface::class); | ||||
| 
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -57,23 +53,18 @@ class CategoryController extends Controller | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  Documentation for this endpoint: | ||||
|      *  TODO list of checks | ||||
|      *  1. use dates from ParameterBag | ||||
|      *  2. Request validates dates | ||||
|      *  3. Request includes user_group_id | ||||
|      *  4. Endpoint is documented. | ||||
|      *  5. Collector uses user_group_id | ||||
|      * Documentation: https://api-docs.firefly-iii.org/?urls.primaryName=2.1.0%20(v2)#/autocomplete/getCategoriesAC
 | ||||
|      */ | ||||
|     public function categories(AutocompleteRequest $request): JsonResponse | ||||
|     { | ||||
|         $data     = $request->getData(); | ||||
|         $result   = $this->repository->searchCategory($data['query'], $this->parameters->get('limit')); | ||||
|         $filtered = $result->map( | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $result          = $this->repository->searchCategory($queryParameters['query'], $queryParameters['size']); | ||||
|         $filtered        = $result->map( | ||||
|             static function (Category $item) { | ||||
|                 return [ | ||||
|                     'id'   => (string)$item->id, | ||||
|                     'name' => $item->name, | ||||
|                     'id'    => (string)$item->id, | ||||
|                     'title' => $item->name, | ||||
|                     'meta'  => [], | ||||
|                 ]; | ||||
|             } | ||||
|         ); | ||||
|   | ||||
| @@ -45,11 +45,7 @@ class TagController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(TagRepositoryInterface::class); | ||||
| 
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -57,25 +53,20 @@ class TagController extends Controller | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  Documentation for this endpoint: | ||||
|      *  TODO list of checks | ||||
|      *  1. use dates from ParameterBag | ||||
|      *  2. Request validates dates | ||||
|      *  3. Request includes user_group_id | ||||
|      *  4. Endpoint is documented. | ||||
|      *  5. Collector uses user_group_id | ||||
|      * Documentation: https://api-docs.firefly-iii.org/?urls.primaryName=2.1.0%20(v2)#/autocomplete/getTagsAC
 | ||||
|      */ | ||||
|     public function tags(AutocompleteRequest $request): JsonResponse | ||||
|     { | ||||
|         $data     = $request->getData(); | ||||
|         $result   = $this->repository->searchTag($data['query'], $data['limit']); | ||||
|         $filtered = $result->map( | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $result          = $this->repository->searchTag($queryParameters['query'], $queryParameters['size']); | ||||
|         $filtered        = $result->map( | ||||
|             static function (Tag $item) { | ||||
|                 return [ | ||||
|                     'id'    => (string)$item->id, | ||||
|                     'name'  => $item->tag, | ||||
|                     'value' => (string)$item->id, | ||||
|                     'id'    => (string) $item->id, | ||||
|                     'title' => $item->tag, | ||||
|                     'value' => (string) $item->id, | ||||
|                     'label' => $item->tag, | ||||
|                     'meta'  => [], | ||||
|                 ]; | ||||
|             } | ||||
|         ); | ||||
|   | ||||
| @@ -45,11 +45,7 @@ class TransactionController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(JournalRepositoryInterface::class); | ||||
| 
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -57,30 +53,25 @@ class TransactionController extends Controller | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  Documentation for this endpoint: | ||||
|      *  TODO list of checks | ||||
|      *  1. use dates from ParameterBag | ||||
|      *  2. Request validates dates | ||||
|      *  3. Request includes user_group_id | ||||
|      *  4. Endpoint is documented. | ||||
|      *  5. Collector uses user_group_id | ||||
|      * Documentation: https://api-docs.firefly-iii.org/?urls.primaryName=2.1.0%20(v2)#/autocomplete/getTransactionsAC
 | ||||
|      */ | ||||
|     public function transactionDescriptions(AutocompleteRequest $request): JsonResponse | ||||
|     { | ||||
|         $data     = $request->getData(); | ||||
|         $result   = $this->repository->searchJournalDescriptions($data['query'], $data['limit']); | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $result          = $this->repository->searchJournalDescriptions($queryParameters['query'], $queryParameters['size']); | ||||
| 
 | ||||
|         // limit and unique
 | ||||
|         $filtered = $result->unique('description'); | ||||
|         $array    = []; | ||||
|         $filtered        = $result->unique('description'); | ||||
|         $array           = []; | ||||
| 
 | ||||
|         /** @var TransactionJournal $journal */ | ||||
|         foreach ($filtered as $journal) { | ||||
|             $array[] = [ | ||||
|                 'id'                   => (string)$journal->id, | ||||
|                 'transaction_group_id' => (string)$journal->transaction_group_id, | ||||
|                 'name'                 => $journal->description, | ||||
|                 'description'          => $journal->description, | ||||
|                 'id'    => (string) $journal->id, | ||||
|                 'title' => $journal->description, | ||||
|                 'meta'  => [ | ||||
|                     'transaction_group_id' => (string) $journal->transaction_group_id, | ||||
|                 ], | ||||
|             ]; | ||||
|         } | ||||
| 
 | ||||
|   | ||||
| @@ -24,18 +24,17 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Controllers\Chart; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use FireflyIII\Api\V2\Controllers\Controller; | ||||
| use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest; | ||||
| use FireflyIII\Api\V2\Request\Chart\ChartRequest; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Models\AccountType; | ||||
| use FireflyIII\Models\TransactionCurrency; | ||||
| use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; | ||||
| use FireflyIII\Support\Chart\ChartData; | ||||
| use FireflyIII\Support\Http\Api\CleansChartData; | ||||
| use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter; | ||||
| use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Collection; | ||||
| 
 | ||||
| /** | ||||
|  * Class AccountController | ||||
| @@ -43,9 +42,12 @@ use Illuminate\Support\Collection; | ||||
| class AccountController extends Controller | ||||
| { | ||||
|     use CleansChartData; | ||||
|     use CollectsAccountsFromFilter; | ||||
|     use ValidatesUserGroupTrait; | ||||
| 
 | ||||
|     private AccountRepositoryInterface $repository; | ||||
|     private ChartData                  $chartData; | ||||
|     private TransactionCurrency        $default; | ||||
| 
 | ||||
|     public function __construct() | ||||
|     { | ||||
| @@ -53,10 +55,9 @@ class AccountController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(AccountRepositoryInterface::class); | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
|                 $this->chartData  = new ChartData(); | ||||
|                 $this->default    = app('amount')->getDefaultCurrency(); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -64,107 +65,76 @@ class AccountController extends Controller | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This endpoint is documented at | ||||
|      * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v2)#/charts/getChartAccountOverview
 | ||||
|      * | ||||
|      * The native currency is the preferred currency on the page /currencies. | ||||
|      * | ||||
|      * If a transaction has foreign currency = native currency, the foreign amount will be used, no conversion | ||||
|      * will take place. | ||||
|      * | ||||
|      * TODO validate and set user_group_id from request | ||||
|      * TODO fix documentation | ||||
|      * | ||||
|      * @throws FireflyException | ||||
|      * | ||||
|      * @SuppressWarnings(PHPMD.UnusedFormalParameter) | ||||
|      */ | ||||
|     public function dashboard(DashboardChartRequest $request): JsonResponse | ||||
|     public function dashboard(ChartRequest $request): JsonResponse | ||||
|     { | ||||
|         /** @var Carbon $start */ | ||||
|         $start     = $this->parameters->get('start'); | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $accounts        = $this->getAccountList($queryParameters); | ||||
| 
 | ||||
|         /** @var Carbon $end */ | ||||
|         $end       = $this->parameters->get('end'); | ||||
|         $end->endOfDay(); | ||||
| 
 | ||||
|         /** @var TransactionCurrency $default */ | ||||
|         $default   = app('amount')->getDefaultCurrency(); | ||||
|         $params    = $request->getAll(); | ||||
| 
 | ||||
|         /** @var Collection $accounts */ | ||||
|         $accounts  = $params['accounts']; | ||||
|         $chartData = []; | ||||
| 
 | ||||
|         // user's preferences
 | ||||
|         if (0 === $accounts->count()) { | ||||
|             $defaultSet = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT])->pluck('id')->toArray(); | ||||
|             $frontPage  = app('preferences')->get('frontPageAccounts', $defaultSet); | ||||
| 
 | ||||
|             if (!(is_array($frontPage->data) && count($frontPage->data) > 0)) { | ||||
|                 $frontPage->data = $defaultSet; | ||||
|                 $frontPage->save(); | ||||
|             } | ||||
| 
 | ||||
|             $accounts   = $this->repository->getAccountsById($frontPage->data); | ||||
|         } | ||||
| 
 | ||||
|         // both options are overruled by "preselected"
 | ||||
|         if ('all' === $params['preselected']) { | ||||
|             $accounts = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); | ||||
|         } | ||||
|         if ('assets' === $params['preselected']) { | ||||
|             $accounts = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); | ||||
|         } | ||||
|         if ('liabilities' === $params['preselected']) { | ||||
|             $accounts = $this->repository->getAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); | ||||
|         } | ||||
|         // move date to end of day
 | ||||
|         $queryParameters['start']->startOfDay(); | ||||
|         $queryParameters['end']->endOfDay(); | ||||
| 
 | ||||
|         // loop each account, and collect info:
 | ||||
|         /** @var Account $account */ | ||||
|         foreach ($accounts as $account) { | ||||
|             $currency          = $this->repository->getAccountCurrency($account); | ||||
|             if (null === $currency) { | ||||
|                 $currency = $default; | ||||
|             } | ||||
|             $currentSet        = [ | ||||
|                 'label'                          => $account->name, | ||||
|                 // the currency that belongs to the account.
 | ||||
|                 'currency_id'                    => (string)$currency->id, | ||||
|                 'currency_code'                  => $currency->code, | ||||
|                 'currency_symbol'                => $currency->symbol, | ||||
|                 'currency_decimal_places'        => $currency->decimal_places, | ||||
| 
 | ||||
|                 // the default currency of the user (could be the same!)
 | ||||
|                 'native_currency_id'             => (string)$default->id, | ||||
|                 'native_currency_code'           => $default->code, | ||||
|                 'native_currency_symbol'         => $default->symbol, | ||||
|                 'native_currency_decimal_places' => $default->decimal_places, | ||||
|                 'start'                          => $start->toAtomString(), | ||||
|                 'end'                            => $end->toAtomString(), | ||||
|                 'period'                         => '1D', | ||||
|                 'entries'                        => [], | ||||
|                 'native_entries'                 => [], | ||||
|             ]; | ||||
|             $currentStart      = clone $start; | ||||
|             $range             = app('steam')->balanceInRange($account, $start, clone $end, $currency); | ||||
|             $rangeConverted    = app('steam')->balanceInRangeConverted($account, $start, clone $end, $default); | ||||
| 
 | ||||
|             $previous          = array_values($range)[0]; | ||||
|             $previousConverted = array_values($rangeConverted)[0]; | ||||
|             while ($currentStart <= $end) { | ||||
|                 $format                               = $currentStart->format('Y-m-d'); | ||||
|                 $label                                = $currentStart->toAtomString(); | ||||
|                 $balance                              = array_key_exists($format, $range) ? $range[$format] : $previous; | ||||
|                 $balanceConverted                     = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted; | ||||
|                 $previous                             = $balance; | ||||
|                 $previousConverted                    = $balanceConverted; | ||||
| 
 | ||||
|                 $currentStart->addDay(); | ||||
|                 $currentSet['entries'][$label]        = $balance; | ||||
|                 $currentSet['native_entries'][$label] = $balanceConverted; | ||||
|             } | ||||
|             $chartData[]       = $currentSet; | ||||
|             $this->renderAccountData($queryParameters, $account); | ||||
|         } | ||||
| 
 | ||||
|         return response()->json($this->clean($chartData)); | ||||
|         return response()->json($this->chartData->render()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     private function renderAccountData(array $params, Account $account): void | ||||
|     { | ||||
|         $currency          = $this->repository->getAccountCurrency($account); | ||||
|         if (null === $currency) { | ||||
|             $currency = $this->default; | ||||
|         } | ||||
|         $currentSet        = [ | ||||
|             'label'                          => $account->name, | ||||
| 
 | ||||
|             // the currency that belongs to the account.
 | ||||
|             'currency_id'                    => (string) $currency->id, | ||||
|             'currency_code'                  => $currency->code, | ||||
|             'currency_symbol'                => $currency->symbol, | ||||
|             'currency_decimal_places'        => $currency->decimal_places, | ||||
| 
 | ||||
|             // the default currency of the user (could be the same!)
 | ||||
|             'native_currency_id'             => (string) $this->default->id, | ||||
|             'native_currency_code'           => $this->default->code, | ||||
|             'native_currency_symbol'         => $this->default->symbol, | ||||
|             'native_currency_decimal_places' => $this->default->decimal_places, | ||||
|             'date'                           => $params['start']->toAtomString(), | ||||
|             'start'                          => $params['start']->toAtomString(), | ||||
|             'end'                            => $params['end']->toAtomString(), | ||||
|             'period'                         => '1D', | ||||
|             'entries'                        => [], | ||||
|             'native_entries'                 => [], | ||||
|         ]; | ||||
|         $currentStart      = clone $params['start']; | ||||
|         $range             = app('steam')->balanceInRange($account, $params['start'], clone $params['end'], $currency); | ||||
|         $rangeConverted    = app('steam')->balanceInRangeConverted($account, $params['start'], clone $params['end'], $this->default); | ||||
| 
 | ||||
|         $previous          = array_values($range)[0]; | ||||
|         $previousConverted = array_values($rangeConverted)[0]; | ||||
|         while ($currentStart <= $params['end']) { | ||||
|             $format                               = $currentStart->format('Y-m-d'); | ||||
|             $label                                = $currentStart->toAtomString(); | ||||
|             $balance                              = array_key_exists($format, $range) ? $range[$format] : $previous; | ||||
|             $balanceConverted                     = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted; | ||||
|             $previous                             = $balance; | ||||
|             $previousConverted                    = $balanceConverted; | ||||
| 
 | ||||
|             $currentStart->addDay(); | ||||
|             $currentSet['entries'][$label]        = $balance; | ||||
|             $currentSet['native_entries'][$label] = $balanceConverted; | ||||
|         } | ||||
|         $this->chartData->add($currentSet); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,17 +24,18 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Controllers\Chart; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use FireflyIII\Api\V2\Controllers\Controller; | ||||
| use FireflyIII\Api\V2\Request\Chart\BalanceChartRequest; | ||||
| use FireflyIII\Api\V2\Request\Chart\ChartRequest; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Helpers\Collector\GroupCollectorInterface; | ||||
| use FireflyIII\Models\TransactionCurrency; | ||||
| use FireflyIII\Models\TransactionType; | ||||
| use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; | ||||
| use FireflyIII\Support\Chart\ChartData; | ||||
| use FireflyIII\Support\Http\Api\AccountBalanceGrouped; | ||||
| use FireflyIII\Support\Http\Api\CleansChartData; | ||||
| use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Collection; | ||||
| 
 | ||||
| /** | ||||
|  * Class BalanceController | ||||
| @@ -42,6 +43,30 @@ use Illuminate\Support\Collection; | ||||
| class BalanceController extends Controller | ||||
| { | ||||
|     use CleansChartData; | ||||
|     use CollectsAccountsFromFilter; | ||||
| 
 | ||||
|     private AccountRepositoryInterface $repository; | ||||
|     private GroupCollectorInterface    $collector; | ||||
|     private ChartData                  $chartData; | ||||
|     // private TransactionCurrency        $default;
 | ||||
| 
 | ||||
|     public function __construct() | ||||
|     { | ||||
|         parent::__construct(); | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(AccountRepositoryInterface::class); | ||||
|                 $this->collector  = app(GroupCollectorInterface::class); | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 $this->repository->setUserGroup($userGroup); | ||||
|                 $this->collector->setUserGroup($userGroup); | ||||
|                 $this->chartData  = new ChartData(); | ||||
|                 // $this->default    = app('amount')->getDefaultCurrency();
 | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The code is practically a duplicate of ReportController::operations. | ||||
| @@ -52,50 +77,43 @@ class BalanceController extends Controller | ||||
|      * If the transaction being processed is already in native currency OR if the | ||||
|      * foreign amount is in the native currency, the amount will not be converted. | ||||
|      * | ||||
|      * TODO validate and set user_group_id | ||||
|      * TODO collector set group, not user | ||||
|      * | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     public function balance(BalanceChartRequest $request): JsonResponse | ||||
|     public function balance(ChartRequest $request): JsonResponse | ||||
|     { | ||||
|         $params         = $request->getAll(); | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $accounts        = $this->getAccountList($queryParameters); | ||||
| 
 | ||||
|         /** @var Carbon $start */ | ||||
|         $start          = $this->parameters->get('start'); | ||||
| 
 | ||||
|         /** @var Carbon $end */ | ||||
|         $end            = $this->parameters->get('end'); | ||||
|         $end->endOfDay(); | ||||
| 
 | ||||
|         /** @var Collection $accounts */ | ||||
|         $accounts       = $params['accounts']; | ||||
| 
 | ||||
|         /** @var string $preferredRange */ | ||||
|         $preferredRange = $params['period']; | ||||
|         // move date to end of day
 | ||||
|         $queryParameters['start']->startOfDay(); | ||||
|         $queryParameters['end']->endOfDay(); | ||||
| 
 | ||||
|         // prepare for currency conversion and data collection:
 | ||||
|         /** @var TransactionCurrency $default */ | ||||
|         $default        = app('amount')->getDefaultCurrency(); | ||||
|         $default         = app('amount')->getDefaultCurrency(); | ||||
| 
 | ||||
|         // get journals for entire period:
 | ||||
|         /** @var GroupCollectorInterface $collector */ | ||||
|         $collector      = app(GroupCollectorInterface::class); | ||||
|         $collector->setRange($start, $end)->withAccountInformation(); | ||||
|         $collector->setXorAccounts($accounts); | ||||
|         $collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::RECONCILIATION, TransactionType::TRANSFER]); | ||||
|         $journals       = $collector->getExtractedJournals(); | ||||
| 
 | ||||
|         $object         = new AccountBalanceGrouped(); | ||||
|         $object->setPreferredRange($preferredRange); | ||||
|         $this->collector->setRange($queryParameters['start'], $queryParameters['end']) | ||||
|             ->withAccountInformation() | ||||
|             ->setXorAccounts($accounts) | ||||
|             ->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::RECONCILIATION, TransactionType::TRANSFER]) | ||||
|         ; | ||||
|         $journals        = $this->collector->getExtractedJournals(); | ||||
| 
 | ||||
|         $object          = new AccountBalanceGrouped(); | ||||
|         $object->setPreferredRange($queryParameters['period']); | ||||
|         $object->setDefault($default); | ||||
|         $object->setAccounts($accounts); | ||||
|         $object->setJournals($journals); | ||||
|         $object->setStart($start); | ||||
|         $object->setEnd($end); | ||||
|         $object->setStart($queryParameters['start']); | ||||
|         $object->setEnd($queryParameters['end']); | ||||
|         $object->groupByCurrencyAndPeriod(); | ||||
|         $chartData      = $object->convertToChartData(); | ||||
|         $data            = $object->convertToChartData(); | ||||
|         foreach ($data as $entry) { | ||||
|             $this->chartData->add($entry); | ||||
|         } | ||||
| 
 | ||||
|         return response()->json($this->clean($chartData)); | ||||
|         return response()->json($this->chartData->render()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -64,12 +64,9 @@ class BudgetController extends Controller | ||||
|                 $this->blRepository  = app(BudgetLimitRepositoryInterface::class); | ||||
|                 $this->opsRepository = app(OperationsRepositoryInterface::class); | ||||
|                 $this->currency      = app('amount')->getDefaultCurrency(); | ||||
| 
 | ||||
|                 $userGroup           = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                     $this->opsRepository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($userGroup); | ||||
|                 $this->opsRepository->setUserGroup($userGroup); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -124,11 +121,11 @@ class BudgetController extends Controller | ||||
|         foreach ($rows as $row) { | ||||
|             $current  = [ | ||||
|                 'label'                          => $budget->name, | ||||
|                 'currency_id'                    => (string)$row['currency_id'], | ||||
|                 'currency_id'                    => (string) $row['currency_id'], | ||||
|                 'currency_code'                  => $row['currency_code'], | ||||
|                 'currency_name'                  => $row['currency_name'], | ||||
|                 'currency_decimal_places'        => $row['currency_decimal_places'], | ||||
|                 'native_currency_id'             => (string)$row['native_currency_id'], | ||||
|                 'native_currency_id'             => (string) $row['native_currency_id'], | ||||
|                 'native_currency_code'           => $row['native_currency_code'], | ||||
|                 'native_currency_name'           => $row['native_currency_name'], | ||||
|                 'native_currency_decimal_places' => $row['native_currency_decimal_places'], | ||||
| @@ -189,12 +186,12 @@ class BudgetController extends Controller | ||||
|         foreach ($array as $currencyId => $block) { | ||||
|             $this->currencies[$currencyId] ??= TransactionCurrency::find($currencyId); | ||||
|             $return[$currencyId]           ??= [ | ||||
|                 'currency_id'                    => (string)$currencyId, | ||||
|                 'currency_id'                    => (string) $currencyId, | ||||
|                 'currency_code'                  => $block['currency_code'], | ||||
|                 'currency_name'                  => $block['currency_name'], | ||||
|                 'currency_symbol'                => $block['currency_symbol'], | ||||
|                 'currency_decimal_places'        => (int)$block['currency_decimal_places'], | ||||
|                 'native_currency_id'             => (string)$this->currency->id, | ||||
|                 'currency_decimal_places'        => (int) $block['currency_decimal_places'], | ||||
|                 'native_currency_id'             => (string) $this->currency->id, | ||||
|                 'native_currency_code'           => $this->currency->code, | ||||
|                 'native_currency_name'           => $this->currency->name, | ||||
|                 'native_currency_symbol'         => $this->currency->symbol, | ||||
|   | ||||
| @@ -57,10 +57,7 @@ class CategoryController extends Controller | ||||
|             function ($request, $next) { | ||||
|                 $this->accountRepos  = app(AccountRepositoryInterface::class); | ||||
|                 $this->currencyRepos = app(CurrencyRepositoryInterface::class); | ||||
|                 $userGroup           = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->accountRepos->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->accountRepos->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -100,25 +97,25 @@ class CategoryController extends Controller | ||||
| 
 | ||||
|         /** @var array $journal */ | ||||
|         foreach ($journals as $journal) { | ||||
|             $currencyId                    = (int)$journal['currency_id']; | ||||
|             $currencyId                    = (int) $journal['currency_id']; | ||||
|             $currency                      = $currencies[$currencyId] ?? $this->currencyRepos->find($currencyId); | ||||
|             $currencies[$currencyId]       = $currency; | ||||
|             $categoryName                  = null === $journal['category_name'] ? (string)trans('firefly.no_category') : $journal['category_name']; | ||||
|             $categoryName                  = null === $journal['category_name'] ? (string) trans('firefly.no_category') : $journal['category_name']; | ||||
|             $amount                        = app('steam')->positive($journal['amount']); | ||||
|             $nativeAmount                  = $converter->convert($default, $currency, $journal['date'], $amount); | ||||
|             $key                           = sprintf('%s-%s', $categoryName, $currency->code); | ||||
|             if ((int)$journal['foreign_currency_id'] === $default->id) { | ||||
|             if ((int) $journal['foreign_currency_id'] === $default->id) { | ||||
|                 $nativeAmount = app('steam')->positive($journal['foreign_amount']); | ||||
|             } | ||||
|             // create arrays
 | ||||
|             $return[$key] ??= [ | ||||
|                 'label'                          => $categoryName, | ||||
|                 'currency_id'                    => (string)$currency->id, | ||||
|                 'currency_id'                    => (string) $currency->id, | ||||
|                 'currency_code'                  => $currency->code, | ||||
|                 'currency_name'                  => $currency->name, | ||||
|                 'currency_symbol'                => $currency->symbol, | ||||
|                 'currency_decimal_places'        => $currency->decimal_places, | ||||
|                 'native_currency_id'             => (string)$default->id, | ||||
|                 'native_currency_id'             => (string) $default->id, | ||||
|                 'native_currency_code'           => $default->code, | ||||
|                 'native_currency_name'           => $default->name, | ||||
|                 'native_currency_symbol'         => $default->symbol, | ||||
| @@ -138,7 +135,7 @@ class CategoryController extends Controller | ||||
| 
 | ||||
|         // order by native amount
 | ||||
|         usort($return, static function (array $a, array $b) { | ||||
|             return (float)$a['native_amount'] < (float)$b['native_amount'] ? 1 : -1; | ||||
|             return (float) $a['native_amount'] < (float) $b['native_amount'] ? 1 : -1; | ||||
|         }); | ||||
|         $converter->summarize(); | ||||
| 
 | ||||
|   | ||||
| @@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers; | ||||
| use Carbon\Carbon; | ||||
| use Carbon\Exceptions\InvalidDateException; | ||||
| use Carbon\Exceptions\InvalidFormatException; | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; | ||||
| use FireflyIII\Transformers\V2\AbstractTransformer; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| @@ -55,6 +56,7 @@ class Controller extends BaseController | ||||
| 
 | ||||
|     protected const string CONTENT_TYPE = 'application/vnd.api+json'; | ||||
|     protected ParameterBag $parameters; | ||||
|     protected array $acceptedRoles      = [UserRoleEnum::READ_ONLY]; | ||||
| 
 | ||||
|     public function __construct() | ||||
|     { | ||||
| @@ -120,6 +122,9 @@ class Controller extends BaseController | ||||
|                     $obj = null; | ||||
|                 } | ||||
|             } | ||||
|             if (null !== $date && 'end' === $field) { | ||||
|                 $obj->endOfDay(); | ||||
|             } | ||||
|             $bag->set($field, $obj); | ||||
|         } | ||||
| 
 | ||||
| @@ -152,6 +157,9 @@ class Controller extends BaseController | ||||
|     { | ||||
|         $manager  = new Manager(); | ||||
|         $baseUrl  = request()->getSchemeAndHttpHost().'/api/v2'; | ||||
| 
 | ||||
|         // TODO add stuff to path?
 | ||||
| 
 | ||||
|         $manager->setSerializer(new JsonApiSerializer($baseUrl)); | ||||
| 
 | ||||
|         $objects  = $paginator->getCollection(); | ||||
|   | ||||
							
								
								
									
										111
									
								
								app/Api/V2/Controllers/JsonApi/AccountController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								app/Api/V2/Controllers/JsonApi/AccountController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| <?php | ||||
| /* | ||||
|  * AccountController.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Controllers\JsonApi; | ||||
| 
 | ||||
| use FireflyIII\Http\Controllers\Controller; | ||||
| use FireflyIII\JsonApi\V2\Accounts\AccountCollectionQuery; | ||||
| use FireflyIII\JsonApi\V2\Accounts\AccountSchema; | ||||
| use FireflyIII\JsonApi\V2\Accounts\AccountSingleQuery; | ||||
| use FireflyIII\Models\Account; | ||||
| use Illuminate\Contracts\Support\Responsable; | ||||
| use Illuminate\Http\Response; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Core\Responses\DataResponse; | ||||
| use LaravelJsonApi\Laravel\Http\Controllers\Actions; | ||||
| 
 | ||||
| /** | ||||
|  * Class AccountController | ||||
|  * | ||||
|  * This class handles api/v2 requests for accounts. | ||||
|  * Most stuff is default stuff. | ||||
|  */ | ||||
| class AccountController extends Controller | ||||
| { | ||||
|     use Actions\AttachRelationship; | ||||
|     use Actions\Destroy; | ||||
|     use Actions\DetachRelationship; | ||||
| 
 | ||||
|     use Actions\FetchMany; | ||||
|     // use Actions\FetchOne;
 | ||||
|     use Actions\FetchRelated; | ||||
|     use Actions\FetchRelationship; | ||||
|     use Actions\Store; | ||||
|     use Actions\Update; | ||||
|     use Actions\UpdateRelationship; | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch zero to many JSON API resources. | ||||
|      * | ||||
|      * @return Responsable|Response | ||||
|      */ | ||||
|     public function index(AccountSchema $schema, AccountCollectionQuery $request) | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $models = $schema | ||||
|             ->repository() | ||||
|             ->queryAll() | ||||
|             ->withRequest($request) | ||||
|             ->get() | ||||
|         ; | ||||
| 
 | ||||
|         // do something custom...
 | ||||
| 
 | ||||
|         return new DataResponse($models); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch zero to one JSON API resource by id. | ||||
|      * | ||||
|      * @return Responsable|Response | ||||
|      */ | ||||
|     public function show(AccountSchema $schema, AccountSingleQuery $request, Account $account) | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $model = $schema->repository() | ||||
|             ->queryOne($account) | ||||
|             ->withRequest($request) | ||||
|             ->first() | ||||
|         ; | ||||
|         Log::debug(sprintf('%s again!', __METHOD__)); | ||||
| 
 | ||||
|         // do something custom...
 | ||||
| 
 | ||||
|         return new DataResponse($model); | ||||
|     } | ||||
| 
 | ||||
|     //    public function readAccountBalances(AnonymousQuery $query, AccountBalanceSchema $schema, Account $account): Responsable
 | ||||
|     //    {
 | ||||
|     //        $schema = JsonApi::server()->schemas()->schemaFor('account-balances');
 | ||||
|     //
 | ||||
|     //        $models = $schema
 | ||||
|     //            ->repository()
 | ||||
|     //            ->queryAll()
 | ||||
|     //            ->withRequest($query)
 | ||||
|     //            ->withAccount($account)
 | ||||
|     //            ->get()
 | ||||
|     //        ;
 | ||||
|     //
 | ||||
|     //        return DataResponse::make($models);
 | ||||
|     //    }
 | ||||
| } | ||||
| @@ -25,17 +25,19 @@ namespace FireflyIII\Api\V2\Controllers\Model\Account; | ||||
| 
 | ||||
| use FireflyIII\Api\V2\Controllers\Controller; | ||||
| use FireflyIII\Api\V2\Request\Model\Account\IndexRequest; | ||||
| use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest; | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; | ||||
| use FireflyIII\Transformers\V2\AccountTransformer; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Pagination\LengthAwarePaginator; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| class IndexController extends Controller | ||||
| { | ||||
|     public const string RESOURCE_KEY = 'accounts'; | ||||
|     public const string RESOURCE_KEY                  = 'accounts'; | ||||
| 
 | ||||
|     private AccountRepositoryInterface $repository; | ||||
|     protected array                    $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS]; | ||||
| 
 | ||||
|     /** | ||||
|      * AccountController constructor. | ||||
| @@ -48,9 +50,7 @@ class IndexController extends Controller | ||||
|                 $this->repository = app(AccountRepositoryInterface::class); | ||||
|                 // new way of user group validation
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($userGroup); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -58,21 +58,35 @@ class IndexController extends Controller | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * TODO see autocomplete/accountcontroller for list. | ||||
|      * TODO the sort instructions need proper repeatable documentation. | ||||
|      * TODO see autocomplete/account controller for list. | ||||
|      */ | ||||
|     public function index(IndexRequest $request): JsonResponse | ||||
|     { | ||||
|         $this->repository->resetAccountOrder(); | ||||
|         $types        = $request->getAccountTypes(); | ||||
|         $instructions = $request->getSortInstructions('accounts'); | ||||
|         $accounts     = $this->repository->getAccountsByType($types, $instructions); | ||||
|         $pageSize     = $this->parameters->get('limit'); | ||||
|         $count        = $accounts->count(); | ||||
|         $accounts     = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); | ||||
|         $paginator    = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); | ||||
|         $transformer  = new AccountTransformer(); | ||||
|         $types             = $request->getAccountTypes(); | ||||
|         $sorting           = $request->getSortInstructions('accounts'); | ||||
|         $filters           = $request->getFilterInstructions('accounts'); | ||||
|         $accounts          = $this->repository->getAccountsByType($types, $sorting, $filters); | ||||
|         $pageSize          = $this->parameters->get('limit'); | ||||
|         $count             = $accounts->count(); | ||||
| 
 | ||||
|         $this->parameters->set('sort', $instructions); | ||||
|         // depending on the sort parameters, this list must not be split, because the
 | ||||
|         // order is calculated in the account transformer and by that time it's too late.
 | ||||
|         $first             = array_key_first($sorting); | ||||
|         $disablePagination = in_array($first, ['last_activity', 'balance', 'balance_difference'], true); | ||||
|         Log::debug(sprintf('Will disable pagination in account index v2? %s', var_export($disablePagination, true))); | ||||
|         if (!$disablePagination) { | ||||
|             $accounts = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); | ||||
|         } | ||||
|         $paginator         = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); | ||||
|         $transformer       = new AccountTransformer(); | ||||
| 
 | ||||
|         $this->parameters->set('disablePagination', $disablePagination); | ||||
|         $this->parameters->set('pageSize', $pageSize); | ||||
|         $this->parameters->set('sort', $sorting); | ||||
| 
 | ||||
|         $this->parameters->set('filters', $filters); | ||||
|         $transformer->setParameters($this->parameters); // give params to transformer
 | ||||
| 
 | ||||
|         return response() | ||||
| @@ -80,25 +94,4 @@ class IndexController extends Controller | ||||
|             ->header('Content-Type', self::CONTENT_TYPE) | ||||
|         ; | ||||
|     } | ||||
| 
 | ||||
|     public function infiniteList(InfiniteListRequest $request): JsonResponse | ||||
|     { | ||||
|         $this->repository->resetAccountOrder(); | ||||
| 
 | ||||
|         // get accounts of the specified type, and return.
 | ||||
|         $types       = $request->getAccountTypes(); | ||||
| 
 | ||||
|         // get from repository
 | ||||
|         $accounts    = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow()); | ||||
|         $total       = $this->repository->countAccounts($types); | ||||
|         $count       = $request->getEndRow() - $request->getStartRow(); | ||||
|         $paginator   = new LengthAwarePaginator($accounts, $total, $count, $this->parameters->get('page')); | ||||
|         $transformer = new AccountTransformer(); | ||||
|         $transformer->setParameters($this->parameters); // give params to transformer
 | ||||
| 
 | ||||
|         return response() | ||||
|             ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) | ||||
|             ->header('Content-Type', self::CONTENT_TYPE) | ||||
|         ; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,9 @@ declare(strict_types=1); | ||||
| namespace FireflyIII\Api\V2\Controllers\Model\Account; | ||||
| 
 | ||||
| use FireflyIII\Api\V2\Controllers\Controller; | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; | ||||
| use FireflyIII\Transformers\V2\AccountTransformer; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| 
 | ||||
| @@ -36,6 +38,29 @@ use Illuminate\Http\JsonResponse; | ||||
|  */ | ||||
| class ShowController extends Controller | ||||
| { | ||||
|     public const string RESOURCE_KEY                  = 'accounts'; | ||||
| 
 | ||||
|     private AccountRepositoryInterface $repository; | ||||
|     protected array                    $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS]; | ||||
| 
 | ||||
|     /** | ||||
|      * AccountController constructor. | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
|         parent::__construct(); | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(AccountRepositoryInterface::class); | ||||
|                 // new way of user group validation
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 $this->repository->setUserGroup($userGroup); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * TODO this endpoint is not yet reachable. | ||||
|      */ | ||||
|   | ||||
| @@ -45,11 +45,7 @@ class UpdateController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(AccountRepositoryInterface::class); | ||||
|                 // new way of user group validation
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|   | ||||
| @@ -46,12 +46,7 @@ class IndexController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(BillRepositoryInterface::class); | ||||
| 
 | ||||
|                 // new way of user group validation
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|   | ||||
| @@ -46,12 +46,7 @@ class ShowController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(BillRepositoryInterface::class); | ||||
| 
 | ||||
|                 // new way of user group validation
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|   | ||||
| @@ -45,11 +45,7 @@ class SumController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(BillRepositoryInterface::class); | ||||
| 
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|   | ||||
| @@ -46,11 +46,7 @@ class IndexController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(PiggyBankRepositoryInterface::class); | ||||
| 
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->repository->setUserGroup($this->validateUserGroup($request)); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|   | ||||
| @@ -77,13 +77,11 @@ class BasicController extends Controller | ||||
|                 $this->opsRepository     = app(OperationsRepositoryInterface::class); | ||||
| 
 | ||||
|                 $userGroup               = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->abRepository->setUserGroup($userGroup); | ||||
|                     $this->accountRepository->setUserGroup($userGroup); | ||||
|                     $this->billRepository->setUserGroup($userGroup); | ||||
|                     $this->budgetRepository->setUserGroup($userGroup); | ||||
|                     $this->opsRepository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->abRepository->setUserGroup($userGroup); | ||||
|                 $this->accountRepository->setUserGroup($userGroup); | ||||
|                 $this->billRepository->setUserGroup($userGroup); | ||||
|                 $this->budgetRepository->setUserGroup($userGroup); | ||||
|                 $this->opsRepository->setUserGroup($userGroup); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -298,7 +296,7 @@ class BasicController extends Controller | ||||
|             app('log')->debug(sprintf('Amount left is %s', $left)); | ||||
| 
 | ||||
|             // how much left per day?
 | ||||
|             $days                    = (int) $today->diffInDays($end, true) + 1; | ||||
|             $days                    = (int)$today->diffInDays($end, true) + 1; | ||||
|             $perDay                  = '0'; | ||||
|             $perDayNative            = '0'; | ||||
|             if (0 !== $days && bccomp($left, '0') > -1) { | ||||
|   | ||||
| @@ -52,10 +52,8 @@ class NetWorthController extends Controller | ||||
|                 $this->repository = app(AccountRepositoryInterface::class); | ||||
|                 // new way of user group validation
 | ||||
|                 $userGroup        = $this->validateUserGroup($request); | ||||
|                 if (null !== $userGroup) { | ||||
|                     $this->netWorth->setUserGroup($userGroup); | ||||
|                     $this->repository->setUserGroup($userGroup); | ||||
|                 } | ||||
|                 $this->netWorth->setUserGroup($userGroup); | ||||
|                 $this->repository->setUserGroup($userGroup); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|   | ||||
							
								
								
									
										73
									
								
								app/Api/V2/Controllers/UserGroup/IndexController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								app/Api/V2/Controllers/UserGroup/IndexController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| <?php | ||||
| /* | ||||
|  * IndexController.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Controllers\UserGroup; | ||||
| 
 | ||||
| use FireflyIII\Api\V2\Controllers\Controller; | ||||
| use FireflyIII\Api\V2\Request\Model\Account\IndexRequest; | ||||
| use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; | ||||
| use FireflyIII\Transformers\V2\UserGroupTransformer; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Pagination\LengthAwarePaginator; | ||||
| 
 | ||||
| class IndexController extends Controller | ||||
| { | ||||
|     public const string RESOURCE_KEY = 'user_groups'; | ||||
| 
 | ||||
|     private UserGroupRepositoryInterface $repository; | ||||
| 
 | ||||
|     /** | ||||
|      * AccountController constructor. | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
|         parent::__construct(); | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->repository = app(UserGroupRepositoryInterface::class); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * TODO see autocomplete/accountcontroller for list. | ||||
|      */ | ||||
|     public function index(IndexRequest $request): JsonResponse | ||||
|     { | ||||
|         $administrations = $this->repository->get(); | ||||
|         $pageSize        = $this->parameters->get('limit'); | ||||
|         $count           = $administrations->count(); | ||||
|         $administrations = $administrations->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); | ||||
|         $paginator       = new LengthAwarePaginator($administrations, $count, $pageSize, $this->parameters->get('page')); | ||||
|         $transformer     = new UserGroupTransformer(); | ||||
| 
 | ||||
|         $transformer->setParameters($this->parameters); // give params to transformer
 | ||||
| 
 | ||||
|         return response() | ||||
|             ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) | ||||
|             ->header('Content-Type', self::CONTENT_TYPE) | ||||
|         ; | ||||
|     } | ||||
| } | ||||
| @@ -79,4 +79,12 @@ class UpdateController extends Controller | ||||
|             ->header('Content-Type', self::CONTENT_TYPE) | ||||
|         ; | ||||
|     } | ||||
| 
 | ||||
|     public function useUserGroup(UserGroup $userGroup): JsonResponse | ||||
|     { | ||||
|         // group validation is already in place, so can just update the user.
 | ||||
|         $this->repository->useUserGroup($userGroup); | ||||
| 
 | ||||
|         return response()->json([], 204); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -23,47 +23,68 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Request\Autocomplete; | ||||
| 
 | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Models\AccountType; | ||||
| use FireflyIII\JsonApi\Rules\IsValidFilter; | ||||
| use FireflyIII\JsonApi\Rules\IsValidPage; | ||||
| use FireflyIII\Support\Http\Api\AccountFilter; | ||||
| use FireflyIII\Support\Http\Api\ParsesQueryFilters; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| use LaravelJsonApi\Core\Query\QueryParameters; | ||||
| use LaravelJsonApi\Validation\Rule as JsonApiRule; | ||||
| 
 | ||||
| /** | ||||
|  * Class AutocompleteRequest | ||||
|  */ | ||||
| class AutocompleteRequest extends FormRequest | ||||
| { | ||||
|     use AccountFilter; | ||||
|     use ChecksLogin; | ||||
|     use ConvertsDataTypes; | ||||
|     use ParsesQueryFilters; | ||||
| 
 | ||||
|     protected array $acceptedRoles = [UserRoleEnum::MANAGE_TRANSACTIONS]; | ||||
| 
 | ||||
|     public function getData(): array | ||||
|     /** | ||||
|      * Loops over all possible query parameters (these are shared over ALL auto complete requests) | ||||
|      * and returns a validated array of parameters. | ||||
|      * | ||||
|      * The advantage is a single class. But you may also submit "account types" to an endpoint that doesn't use these. | ||||
|      */ | ||||
|     public function getParameters(): array | ||||
|     { | ||||
|         $types = $this->convertString('types'); | ||||
|         $array = []; | ||||
|         if ('' !== $types) { | ||||
|             $array = explode(',', $types); | ||||
|         } | ||||
|         $limit = $this->convertInteger('limit'); | ||||
|         $limit = 0 === $limit ? 10 : $limit; | ||||
| 
 | ||||
|         // remove 'initial balance' and another from allowed types. its internal
 | ||||
|         $array = array_diff($array, [AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION]); | ||||
|         $queryParameters = QueryParameters::cast($this->all()); | ||||
| 
 | ||||
|         return [ | ||||
|             'types' => $array, | ||||
|             'query' => $this->convertString('query'), | ||||
|             'date'  => $this->getCarbonDate('date'), | ||||
|             'limit' => $limit, | ||||
|             'date'          => $this->dateOrToday($queryParameters, 'date'), | ||||
|             'query'         => $this->arrayOfStrings($queryParameters, 'query'), | ||||
|             'size'          => $this->integerFromQueryParams($queryParameters, 'size', 50), | ||||
|             'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')), | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     public function rules(): array | ||||
|     { | ||||
|         return [ | ||||
|             'limit' => 'min:0|max:1337', | ||||
|             'fields'  => JsonApiRule::notSupported(), | ||||
|             'filter'  => ['nullable', 'array', new IsValidFilter(['query', 'date', 'account_types'])], | ||||
|             'include' => JsonApiRule::notSupported(), | ||||
|             'page'    => ['nullable', 'array', new IsValidPage(['size'])], | ||||
|             'sort'    => JsonApiRule::notSupported(), | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     private function getAccountTypeParameter(mixed $types): array | ||||
|     { | ||||
|         if (is_string($types) && str_contains($types, ',')) { | ||||
|             $types = explode(',', $types); | ||||
|         } | ||||
|         if (!is_iterable($types)) { | ||||
|             $types = [$types]; | ||||
|         } | ||||
|         $return = []; | ||||
|         foreach ($types as $type) { | ||||
|             $return = array_merge($return, $this->mapAccountTypes($type)); | ||||
|         } | ||||
| 
 | ||||
|         return array_unique($return); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,6 +24,7 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Request\Chart; | ||||
| 
 | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| @@ -39,6 +40,7 @@ class BalanceChartRequest extends FormRequest | ||||
|     use ChecksLogin; | ||||
|     use ConvertsDataTypes; | ||||
|     use ValidatesUserGroupTrait; | ||||
|     protected array                    $acceptedRoles = [UserRoleEnum::READ_ONLY]; | ||||
| 
 | ||||
|     /** | ||||
|      * Get all data from the request. | ||||
|   | ||||
							
								
								
									
										118
									
								
								app/Api/V2/Request/Chart/ChartRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								app/Api/V2/Request/Chart/ChartRequest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| <?php | ||||
| /* | ||||
|  * DashboardChartRequest.php | ||||
|  * Copyright (c) 2023 james@firefly-iii.org | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Request\Chart; | ||||
| 
 | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\JsonApi\Rules\IsValidFilter; | ||||
| use FireflyIII\Rules\IsFilterValueIn; | ||||
| use FireflyIII\Support\Http\Api\ParsesQueryFilters; | ||||
| use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use Illuminate\Validation\Validator; | ||||
| use LaravelJsonApi\Core\Query\QueryParameters; | ||||
| use LaravelJsonApi\Validation\Rule as JsonApiRule; | ||||
| 
 | ||||
| /** | ||||
|  * Class ChartRequest | ||||
|  */ | ||||
| class ChartRequest extends FormRequest | ||||
| { | ||||
|     use ChecksLogin; | ||||
|     use ConvertsDataTypes; | ||||
|     use ParsesQueryFilters; | ||||
|     use ValidatesUserGroupTrait; | ||||
| 
 | ||||
|     protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; | ||||
| 
 | ||||
|     public function getParameters(): array | ||||
|     { | ||||
|         $queryParameters = QueryParameters::cast($this->all()); | ||||
| 
 | ||||
|         return [ | ||||
|             'start'       => $this->dateOrToday($queryParameters, 'start'), | ||||
|             'end'         => $this->dateOrToday($queryParameters, 'end'), | ||||
|             'preselected' => $this->stringFromQueryParams($queryParameters, 'preselected', 'empty'), | ||||
|             'period'      => $this->stringFromQueryParams($queryParameters, 'period', '1M'), | ||||
|             'accounts'    => $this->arrayOfStrings($queryParameters, 'accounts'), | ||||
|             // preselected heeft maar een paar toegestane waardes, dat moet ook goed gaan.
 | ||||
|             //            'query'         => $this->arrayOfStrings($queryParameters, 'query'),
 | ||||
|             //            'size'          => $this->integerFromQueryParams($queryParameters,'size', 50),
 | ||||
|             //            'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')),
 | ||||
|         ]; | ||||
|         // collect accounts based on this list?
 | ||||
|     } | ||||
| 
 | ||||
|     //        return [
 | ||||
|     //            'accounts'    => $this->getAccountList(),
 | ||||
|     //            'preselected' => $this->convertString('preselected'),
 | ||||
|     //        ];
 | ||||
|     //    }
 | ||||
| 
 | ||||
|     /** | ||||
|      * The rules that the incoming request must be matched against. | ||||
|      */ | ||||
|     public function rules(): array | ||||
|     { | ||||
|         return [ | ||||
|             'fields'  => JsonApiRule::notSupported(), | ||||
|             'filter'  => ['nullable', 'array', | ||||
|                 new IsValidFilter(['start', 'end', 'preselected', 'accounts']), | ||||
|                 new IsFilterValueIn('preselected', config('firefly.preselected_accounts')), | ||||
|             ], | ||||
|             'include' => JsonApiRule::notSupported(), | ||||
|             'page'    => JsonApiRule::notSupported(), | ||||
|             'sort'    => JsonApiRule::notSupported(), | ||||
|         ]; | ||||
| 
 | ||||
|         //        return [
 | ||||
|         //            'start'       => 'required|date|after:1900-01-01|before:2099-12-31',
 | ||||
|         //            'end'         => 'required|date|after_or_equal:start|before:2099-12-31|after:1900-01-01',
 | ||||
|         //            'preselected' => sprintf('in:%s', implode(',', config('firefly.preselected_accounts'))),
 | ||||
|         //            'accounts.*'  => 'exists:accounts,id',
 | ||||
|         //        ];
 | ||||
|     } | ||||
| 
 | ||||
|     public function withValidator(Validator $validator): void | ||||
|     { | ||||
|         $validator->after( | ||||
|             static function (Validator $validator): void { | ||||
|                 // validate transaction query data.
 | ||||
|                 $data = $validator->getData(); | ||||
|                 if (!array_key_exists('accounts', $data)) { | ||||
|                     // $validator->errors()->add('accounts', trans('validation.filled', ['attribute' => 'accounts']));
 | ||||
|                     return; | ||||
|                 } | ||||
|                 if (!is_array($data['accounts'])) { | ||||
|                     $validator->errors()->add('accounts', trans('validation.filled', ['attribute' => 'accounts'])); | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|         if ($validator->fails()) { | ||||
|             Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -23,6 +23,7 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Request\Chart; | ||||
| 
 | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| @@ -39,6 +40,8 @@ class DashboardChartRequest extends FormRequest | ||||
|     use ConvertsDataTypes; | ||||
|     use ValidatesUserGroupTrait; | ||||
| 
 | ||||
|     protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; | ||||
| 
 | ||||
|     /** | ||||
|      * Get all data from the request. | ||||
|      */ | ||||
|   | ||||
| @@ -27,6 +27,7 @@ use Carbon\Carbon; | ||||
| use FireflyIII\Support\Http\Api\AccountFilter; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| use FireflyIII\Support\Request\GetFilterInstructions; | ||||
| use FireflyIII\Support\Request\GetSortInstructions; | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| 
 | ||||
| @@ -40,18 +41,16 @@ class IndexRequest extends FormRequest | ||||
|     use AccountFilter; | ||||
|     use ChecksLogin; | ||||
|     use ConvertsDataTypes; | ||||
|     use GetFilterInstructions; | ||||
|     use GetSortInstructions; | ||||
| 
 | ||||
|     public function getAccountTypes(): array | ||||
|     { | ||||
|         $type = (string)$this->get('type', 'default'); | ||||
|         $type = (string) $this->get('type', 'default'); | ||||
| 
 | ||||
|         return $this->mapAccountTypes($type); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all data from the request. | ||||
|      */ | ||||
|     public function getDate(): Carbon | ||||
|     { | ||||
|         return $this->getCarbonDate('date'); | ||||
| @@ -63,7 +62,9 @@ class IndexRequest extends FormRequest | ||||
|     public function rules(): array | ||||
|     { | ||||
|         return [ | ||||
|             'date' => 'date|after:1900-01-01|before:2099-12-31', | ||||
|             'date'  => 'date|after:1900-01-01|before:2099-12-31', | ||||
|             'start' => 'date|after:1900-01-01|before:2099-12-31|before:end|required_with:end', | ||||
|             'end'   => 'date|after:1900-01-01|before:2099-12-31|after:start|required_with:start', | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,7 @@ namespace FireflyIII\Api\V2\Request\UserGroup; | ||||
| 
 | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Models\UserGroup; | ||||
| use FireflyIII\Rules\IsDefaultUserGroupName; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| @@ -53,7 +54,7 @@ class UpdateRequest extends FormRequest | ||||
|         $userGroup = $this->route()->parameter('userGroup'); | ||||
| 
 | ||||
|         return [ | ||||
|             'title' => sprintf('required|min:1|max:255|unique:user_groups,title,%d', $userGroup->id), | ||||
|             'title' => ['required', 'min:1', 'max:255', sprintf('unique:user_groups,title,%d', $userGroup->id), new IsDefaultUserGroupName($userGroup)], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -73,6 +73,7 @@ class CorrectDatabase extends Command | ||||
|             // new!
 | ||||
|             'firefly-iii:unify-group-accounts', | ||||
|             'firefly-iii:trigger-credit-recalculation', | ||||
|             'firefly-iii:migrate-preferences', | ||||
|         ]; | ||||
|         foreach ($commands as $command) { | ||||
|             $this->friendlyLine(sprintf('Now executing command "%s"', $command)); | ||||
|   | ||||
| @@ -50,7 +50,7 @@ class FixFrontpageAccounts extends Command | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         foreach ($users as $user) { | ||||
|             $preference = app('preferences')->getForUser($user, 'frontPageAccounts'); | ||||
|             $preference = app('preferences')->getForUser($user, 'frontpageAccounts'); | ||||
|             if (null !== $preference) { | ||||
|                 $this->fixPreference($preference); | ||||
|             } | ||||
| @@ -83,6 +83,6 @@ class FixFrontpageAccounts extends Command | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         app('preferences')->setForUser($preference->user, 'frontPageAccounts', $fixed); | ||||
|         app('preferences')->setForUser($preference->user, 'frontpageAccounts', $fixed); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -60,15 +60,13 @@ class FixIbans extends Command | ||||
|     { | ||||
|         /** @var Account $account */ | ||||
|         foreach ($accounts as $account) { | ||||
|             $iban = $account->iban; | ||||
|             if (str_contains($iban, ' ')) { | ||||
|                 $iban = app('steam')->filterSpaces((string)$account->iban); | ||||
|                 if ('' !== $iban) { | ||||
|                     $account->iban = $iban; | ||||
|                     $account->save(); | ||||
|                     $this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id)); | ||||
|                     ++$this->count; | ||||
|                 } | ||||
|             $iban    = (string) $account->iban; | ||||
|             $newIban = app('steam')->filterSpaces($iban); | ||||
|             if ('' !== $iban && $iban !== $newIban) { | ||||
|                 $account->iban = $newIban; | ||||
|                 $account->save(); | ||||
|                 $this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id)); | ||||
|                 ++$this->count; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -81,7 +79,7 @@ class FixIbans extends Command | ||||
|         foreach ($accounts as $account) { | ||||
|             $userId = $account->user_id; | ||||
|             $set[$userId] ??= []; | ||||
|             $iban   = (string)$account->iban; | ||||
|             $iban   = (string) $account->iban; | ||||
|             if ('' === $iban) { | ||||
|                 continue; | ||||
|             } | ||||
|   | ||||
| @@ -26,6 +26,8 @@ namespace FireflyIII\Console\Commands\Correction; | ||||
| use FireflyIII\Console\Commands\ShowsFriendlyMessages; | ||||
| use FireflyIII\Models\Transaction; | ||||
| use FireflyIII\Models\TransactionJournal; | ||||
| use FireflyIII\Models\TransactionType; | ||||
| use FireflyIII\Support\Models\AccountBalanceCalculator; | ||||
| use Illuminate\Console\Command; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| @@ -38,61 +40,18 @@ class FixUnevenAmount extends Command | ||||
| 
 | ||||
|     protected $description = 'Fix journals with uneven amounts.'; | ||||
|     protected $signature   = 'firefly-iii:fix-uneven-amount'; | ||||
|     private int $count; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      */ | ||||
|     public function handle(): int | ||||
|     { | ||||
|         $count    = 0; | ||||
|         $journals = \DB::table('transactions') | ||||
|             ->groupBy('transaction_journal_id') | ||||
|             ->whereNull('deleted_at') | ||||
|             ->get(['transaction_journal_id', \DB::raw('SUM(amount) AS the_sum')]) | ||||
|         ; | ||||
| 
 | ||||
|         /** @var \stdClass $entry */ | ||||
|         foreach ($journals as $entry) { | ||||
|             $sum = (string)$entry->the_sum; | ||||
|             if (!is_numeric($sum) | ||||
|                 || '' === $sum // @phpstan-ignore-line
 | ||||
|                 || str_contains($sum, 'e') | ||||
|                 || str_contains($sum, ',')) { | ||||
|                 $message = sprintf( | ||||
|                     'Journal #%d has an invalid sum ("%s"). No sure what to do.', | ||||
|                     $entry->transaction_journal_id, | ||||
|                     $entry->the_sum | ||||
|                 ); | ||||
|                 $this->friendlyWarning($message); | ||||
|                 app('log')->warning($message); | ||||
|                 ++$count; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             $res = -1; | ||||
| 
 | ||||
|             try { | ||||
|                 $res = bccomp($sum, '0'); | ||||
|             } catch (\ValueError $e) { | ||||
|                 $this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum)); | ||||
|                 Log::error($e->getMessage()); | ||||
|                 Log::error($e->getTraceAsString()); | ||||
|             } | ||||
|             if (0 !== $res) { | ||||
|                 $message = sprintf( | ||||
|                     'Sum of journal #%d is %s instead of zero.', | ||||
|                     $entry->transaction_journal_id, | ||||
|                     $entry->the_sum | ||||
|                 ); | ||||
|                 $this->friendlyWarning($message); | ||||
|                 app('log')->warning($message); | ||||
|                 $this->fixJournal($entry->transaction_journal_id); | ||||
|                 ++$count; | ||||
|             } | ||||
|         } | ||||
|         if (0 === $count) { | ||||
|             $this->friendlyPositive('Database amount integrity is OK'); | ||||
|         } | ||||
|         $this->count = 0; | ||||
|         $this->convertOldStyleTransfers(); | ||||
|         $this->fixUnevenAmounts(); | ||||
|         $this->matchCurrencies(); | ||||
|         AccountBalanceCalculator::forceRecalculateAll(); | ||||
| 
 | ||||
|         return 0; | ||||
|     } | ||||
| @@ -118,6 +77,7 @@ class FixUnevenAmount extends Command | ||||
|             ); | ||||
|             Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete(); | ||||
|             TransactionJournal::where('id', $journal->id ?? 0)->forceDelete(); | ||||
|             ++$this->count; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| @@ -139,14 +99,194 @@ class FixUnevenAmount extends Command | ||||
| 
 | ||||
|             Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete(); | ||||
|             TransactionJournal::where('id', $journal->id ?? 0)->forceDelete(); | ||||
|             ++$this->count; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // may still be able to salvage this journal if it is a transfer with foreign currency info
 | ||||
|         if ($this->isForeignCurrencyTransfer($journal)) { | ||||
|             Log::debug(sprintf('Can skip foreign currency transfer #%d.', $journal->id)); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $message             = sprintf('Sum of journal #%d is not zero, journal is broken and now fixed.', $journal->id); | ||||
| 
 | ||||
|         $this->friendlyWarning($message); | ||||
|         app('log')->warning($message); | ||||
| 
 | ||||
|         $destination->amount = $amount; | ||||
|         $destination->save(); | ||||
| 
 | ||||
|         $message             = sprintf('Corrected amount in transaction journal #%d', $param); | ||||
|         $this->friendlyInfo($message); | ||||
|         ++$this->count; | ||||
|     } | ||||
| 
 | ||||
|     private function fixUnevenAmounts(): void | ||||
|     { | ||||
|         $journals = \DB::table('transactions') | ||||
|             ->groupBy('transaction_journal_id') | ||||
|             ->whereNull('deleted_at') | ||||
|             ->get(['transaction_journal_id', \DB::raw('SUM(amount) AS the_sum')]) | ||||
|         ; | ||||
| 
 | ||||
|         /** @var \stdClass $entry */ | ||||
|         foreach ($journals as $entry) { | ||||
|             $sum = (string) $entry->the_sum; | ||||
|             if (!is_numeric($sum) | ||||
|                 || '' === $sum // @phpstan-ignore-line
 | ||||
|                 || str_contains($sum, 'e') | ||||
|                 || str_contains($sum, ',')) { | ||||
|                 $message = sprintf( | ||||
|                     'Journal #%d has an invalid sum ("%s"). No sure what to do.', | ||||
|                     $entry->transaction_journal_id, | ||||
|                     $entry->the_sum | ||||
|                 ); | ||||
|                 $this->friendlyWarning($message); | ||||
|                 app('log')->warning($message); | ||||
|                 ++$this->count; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             $res = -1; | ||||
| 
 | ||||
|             try { | ||||
|                 $res = bccomp($sum, '0'); | ||||
|             } catch (\ValueError $e) { | ||||
|                 $this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum)); | ||||
|                 Log::error($e->getMessage()); | ||||
|                 Log::error($e->getTraceAsString()); | ||||
|             } | ||||
|             if (0 !== $res) { | ||||
|                 $this->fixJournal($entry->transaction_journal_id); | ||||
|             } | ||||
|         } | ||||
|         if (0 === $this->count) { | ||||
|             $this->friendlyPositive('Database amount integrity is OK'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function matchCurrencies(): void | ||||
|     { | ||||
|         $journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id') | ||||
|             ->where('transactions.transaction_currency_id', '!=', \DB::raw('transaction_journals.transaction_currency_id')) | ||||
|             ->get(['transaction_journals.*']) | ||||
|         ; | ||||
| 
 | ||||
|         $count    = 0; | ||||
| 
 | ||||
|         /** @var TransactionJournal $journal */ | ||||
|         foreach ($journals as $journal) { | ||||
|             if (!$this->isForeignCurrencyTransfer($journal)) { | ||||
|                 Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]); | ||||
|                 ++$count; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             Log::debug(sprintf('Can skip foreign currency transfer #%d.', $journal->id)); | ||||
|         } | ||||
|         if (0 === $count) { | ||||
|             $this->friendlyPositive('Journal currency integrity is OK'); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count())); | ||||
|     } | ||||
| 
 | ||||
|     private function isForeignCurrencyTransfer(TransactionJournal $journal): bool | ||||
|     { | ||||
|         if (TransactionType::TRANSFER !== $journal->transactionType->type) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         /** @var Transaction $destination */ | ||||
|         $destination = $journal->transactions()->where('amount', '>', 0)->first(); | ||||
| 
 | ||||
|         /** @var Transaction $source */ | ||||
|         $source      = $journal->transactions()->where('amount', '<', 0)->first(); | ||||
| 
 | ||||
|         // safety catch on NULL should not be necessary, we just had that catch.
 | ||||
|         // source amount = dest foreign amount
 | ||||
|         // source currency = dest foreign currency
 | ||||
|         // dest amount = source foreign currency
 | ||||
|         // dest currency = source foreign currency
 | ||||
| 
 | ||||
|         //        Log::debug(sprintf('[a] %s', bccomp(app('steam')->positive($source->amount), app('steam')->positive($destination->foreign_amount))));
 | ||||
|         //        Log::debug(sprintf('[b] %s', bccomp(app('steam')->positive($destination->amount), app('steam')->positive($source->foreign_amount))));
 | ||||
|         //        Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true)));
 | ||||
|         //        Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int)  $source->foreign_currency_id, true)));
 | ||||
| 
 | ||||
|         if (0 === bccomp(app('steam')->positive($source->amount), app('steam')->positive($destination->foreign_amount)) | ||||
|             && $source->transaction_currency_id === $destination->foreign_currency_id | ||||
|             && 0 === bccomp(app('steam')->positive($destination->amount), app('steam')->positive($source->foreign_amount)) | ||||
|             && (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id | ||||
|         ) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     private function convertOldStyleTransfers(): void | ||||
|     { | ||||
|         Log::debug('convertOldStyleTransfers()'); | ||||
|         // select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different.
 | ||||
|         $transactions = Transaction::distinct() | ||||
|             ->whereNotNull('foreign_currency_id') | ||||
|             ->whereNotNull('foreign_amount')->get(['transactions.transaction_journal_id']) | ||||
|         ; | ||||
|         $count        = 0; | ||||
| 
 | ||||
|         Log::debug(sprintf('Found %d potential journal(s)', $transactions->count())); | ||||
| 
 | ||||
|         /** @var Transaction $transaction */ | ||||
|         foreach ($transactions as $transaction) { | ||||
|             /** @var null|TransactionJournal $journal */ | ||||
|             $journal     = TransactionJournal::find($transaction->transaction_journal_id); | ||||
|             if (null === $journal) { | ||||
|                 Log::debug('Found no journal, continue.'); | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             // needs to be a transfer.
 | ||||
|             if (TransactionType::TRANSFER !== $journal->transactionType->type) { | ||||
|                 Log::debug('Must be a transfer, continue.'); | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             /** @var null|Transaction $destination */ | ||||
|             $destination = $journal->transactions()->where('amount', '>', 0)->first(); | ||||
| 
 | ||||
|             /** @var null|Transaction $source */ | ||||
|             $source      = $journal->transactions()->where('amount', '<', 0)->first(); | ||||
|             if (null === $destination || null === $source) { | ||||
|                 Log::debug('Source or destination transaction is NULL, continue.'); | ||||
| 
 | ||||
|                 // will be picked up later.
 | ||||
|                 continue; | ||||
|             } | ||||
|             if ($source->transaction_currency_id === $destination->transaction_currency_id) { | ||||
|                 Log::debug('Ready to swap data between transactions.'); | ||||
|                 $destination->foreign_currency_id     = $source->transaction_currency_id; | ||||
|                 $destination->foreign_amount          = app('steam')->positive($source->amount); | ||||
|                 $destination->transaction_currency_id = $source->foreign_currency_id; | ||||
|                 $destination->amount                  = app('steam')->positive($source->foreign_amount); | ||||
|                 $destination->balance_dirty           = true; | ||||
|                 $source->balance_dirty                = true; | ||||
|                 $destination->save(); | ||||
|                 $source->save(); | ||||
|                 $this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id)); | ||||
|                 ++$count; | ||||
|             } | ||||
|         } | ||||
|         if (0 === $count) { | ||||
|             $this->friendlyPositive('No "old style" foreign currency transfers.'); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										66
									
								
								app/Console/Commands/Correction/MigratePreferences.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								app/Console/Commands/Correction/MigratePreferences.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| /* | ||||
|  * MigratePreferences.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| namespace FireflyIII\Console\Commands\Correction; | ||||
| 
 | ||||
| use FireflyIII\Models\Preference; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Console\Command; | ||||
| use Symfony\Component\Console\Command\Command as CommandAlias; | ||||
| 
 | ||||
| class MigratePreferences extends Command | ||||
| { | ||||
|     protected $description = 'Give Firefly III preferences a user group ID so they can be made administration specific.'; | ||||
| 
 | ||||
|     protected $signature   = 'firefly-iii:migrate-preferences'; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      */ | ||||
|     public function handle(): int | ||||
|     { | ||||
|         $items = config('firefly.admin_specific_prefs'); | ||||
|         $users = User::get(); | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         foreach ($users as $user) { | ||||
|             $count = 0; | ||||
|             foreach ($items as $item) { | ||||
|                 $preference = Preference::where('name', $item)->where('user_id', $user->id)->first(); | ||||
|                 if (null === $preference) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (null !== $preference->user_group_id) { | ||||
|                     $preference->user_group_id = $user->user_group_id; | ||||
|                     $preference->save(); | ||||
|                     ++$count; | ||||
|                 } | ||||
|             } | ||||
|             if ($count > 0) { | ||||
|                 $this->info(sprintf('Migrated %d preference(s) for user #%d ("%s").', $count, $user->id, $user->email)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return CommandAlias::SUCCESS; | ||||
|     } | ||||
| } | ||||
| @@ -191,7 +191,7 @@ class ExportData extends Command | ||||
|                 $this->friendlyError(sprintf('%s date "%s" must be formatted YYYY-MM-DD. Field will be ignored.', $field, $this->option('start'))); | ||||
|                 $error = true; | ||||
|             } | ||||
|             if (false === $date) { | ||||
|             if (null === $date) { | ||||
|                 $this->friendlyError(sprintf('%s date "%s" must be formatted YYYY-MM-DD.', $field, $this->option('start'))); | ||||
| 
 | ||||
|                 throw new FireflyException(sprintf('%s date "%s" must be formatted YYYY-MM-DD.', $field, $this->option('start'))); | ||||
|   | ||||
| @@ -115,7 +115,7 @@ class UpdateGroupInformation extends Command | ||||
|             return; | ||||
|         } | ||||
|         if (0 !== $result) { | ||||
|             $this->friendlyPositive(sprintf('User #%d: Moved %d %s objects to the correct group.', $user->id, $result, str_replace('FireflyIII\\Models\\', '', $className))); | ||||
|             $this->friendlyPositive(sprintf('User #%d: Moved %d %s objects to the correct group.', $user->id, $result, str_replace('FireflyIII\Models\\', '', $className))); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -111,7 +111,7 @@ class ForceDecimalSize extends Command | ||||
|     { | ||||
|         // switch stuff based on database connection:
 | ||||
|         $this->operator          = 'REGEXP'; | ||||
|         $this->regularExpression = '\'\\\\.[\\\\d]{%d}[1-9]+\''; | ||||
|         $this->regularExpression = '\'\\\.[\\\d]{%d}[1-9]+\''; | ||||
|         $this->cast              = 'CHAR'; | ||||
|         if ('pgsql' === config('database.default')) { | ||||
|             $this->operator          = 'SIMILAR TO'; | ||||
| @@ -119,7 +119,7 @@ class ForceDecimalSize extends Command | ||||
|             $this->cast              = 'TEXT'; | ||||
|         } | ||||
|         if ('sqlite' === config('database.default')) { | ||||
|             $this->regularExpression = '"\\.[\d]{%d}[1-9]+"'; | ||||
|             $this->regularExpression = '"\.[\d]{%d}[1-9]+"'; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @@ -307,7 +307,7 @@ class ForceDecimalSize extends Command | ||||
|             /** @var string $field */ | ||||
|             foreach ($fields as $field) { | ||||
|                 $value   = $item->{$field}; | ||||
|                 if (null === $value) { | ||||
|                 if (null === $value || '' === $value) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 // fix $field by rounding it down correctly.
 | ||||
|   | ||||
| @@ -32,24 +32,13 @@ class LaravelPassportKeys extends Command | ||||
| { | ||||
|     use ShowsFriendlyMessages; | ||||
| 
 | ||||
|     /** | ||||
|      * The name and signature of the console command. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $signature   = 'firefly-iii:laravel-passport-keys'; | ||||
| 
 | ||||
|     /** | ||||
|      * The console command description. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $description = 'Calls the Laravel "passport:keys" but doesn\'t exit 1.'; | ||||
|     protected $signature   = 'firefly-iii:laravel-passport-keys'; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      */ | ||||
|     public function handle() | ||||
|     public function handle(): int | ||||
|     { | ||||
|         Artisan::call('passport:keys --no-interaction', []); | ||||
|         $result = Artisan::output(); | ||||
|   | ||||
| @@ -285,7 +285,7 @@ class ApplyRules extends Command | ||||
|         if (null !== $endString && '' !== $endString) { | ||||
|             $inputEnd = Carbon::createFromFormat('Y-m-d', $endString); | ||||
|         } | ||||
|         if (false === $inputEnd || false === $inputStart) { | ||||
|         if (null === $inputEnd || null === $inputStart) { | ||||
|             Log::error('Could not parse start or end date in verifyInputDate().'); | ||||
| 
 | ||||
|             return; | ||||
|   | ||||
							
								
								
									
										70
									
								
								app/Console/Commands/Upgrade/CorrectAccountBalance.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								app/Console/Commands/Upgrade/CorrectAccountBalance.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| <?php | ||||
| /* | ||||
|  * CorrectAccountBalance.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Console\Commands\Upgrade; | ||||
| 
 | ||||
| use FireflyIII\Console\Commands\ShowsFriendlyMessages; | ||||
| use FireflyIII\Support\Models\AccountBalanceCalculator; | ||||
| use Illuminate\Console\Command; | ||||
| 
 | ||||
| /** | ||||
|  * Class CorrectionSkeleton | ||||
|  */ | ||||
| class CorrectAccountBalance extends Command | ||||
| { | ||||
|     use ShowsFriendlyMessages; | ||||
|     public const string CONFIG_NAME = '610_correct_balances'; | ||||
|     protected $description          = 'Recalculate all account balance amounts'; | ||||
|     protected $signature            = 'firefly-iii:correct-account-balance {--F|force : Force the execution of this command.}'; | ||||
| 
 | ||||
|     public function handle(): int | ||||
|     { | ||||
|         if ($this->isExecuted() && true !== $this->option('force')) { | ||||
|             $this->friendlyInfo('This command has already been executed.'); | ||||
| 
 | ||||
|             return 0; | ||||
|         } | ||||
|         $this->friendlyWarning('This command has been disabled.'); | ||||
|         $this->markAsExecuted(); | ||||
| 
 | ||||
|         //        $this->correctBalanceAmounts();
 | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     private function correctBalanceAmounts(): void | ||||
|     { | ||||
|         AccountBalanceCalculator::recalculateAll(); | ||||
|     } | ||||
| 
 | ||||
|     private function isExecuted(): bool | ||||
|     { | ||||
|         $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); | ||||
| 
 | ||||
|         return (bool)$configVar?->data; | ||||
|     } | ||||
| 
 | ||||
|     private function markAsExecuted(): void | ||||
|     { | ||||
|         app('fireflyconfig')->set(self::CONFIG_NAME, true); | ||||
|     } | ||||
| } | ||||
| @@ -90,7 +90,7 @@ class OtherCurrenciesCorrections extends Command | ||||
|     { | ||||
|         $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); | ||||
|         if (null !== $configVar) { | ||||
|             return (bool)$configVar->data; | ||||
|             return (bool) $configVar->data; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
| @@ -120,7 +120,7 @@ class OtherCurrenciesCorrections extends Command | ||||
|         $this->journalRepos->setUser($journal->user); | ||||
|         $this->cliRepos->setUser($journal->user); | ||||
| 
 | ||||
|         $leadTransaction                  = $this->getLeadTransaction($journal); | ||||
|         $leadTransaction = $this->getLeadTransaction($journal); | ||||
| 
 | ||||
|         if (null === $leadTransaction) { | ||||
|             $this->friendlyError(sprintf('Could not reliably determine which transaction is in the lead for transaction journal #%d.', $journal->id)); | ||||
| @@ -128,8 +128,9 @@ class OtherCurrenciesCorrections extends Command | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $account                          = $leadTransaction->account; | ||||
|         $currency                         = $this->getCurrency($account); | ||||
|         $account         = $leadTransaction->account; | ||||
|         $currency        = $this->getCurrency($account); | ||||
|         $isMultiCurrency = $this->isMultiCurrency($account); | ||||
|         if (null === $currency) { | ||||
|             $this->friendlyError( | ||||
|                 sprintf( | ||||
| @@ -145,14 +146,14 @@ class OtherCurrenciesCorrections extends Command | ||||
|         } | ||||
|         // fix each transaction:
 | ||||
|         $journal->transactions->each( | ||||
|             static function (Transaction $transaction) use ($currency): void { | ||||
|             static function (Transaction $transaction) use ($currency, $isMultiCurrency): void { | ||||
|                 if (null === $transaction->transaction_currency_id) { | ||||
|                     $transaction->transaction_currency_id = $currency->id; | ||||
|                     $transaction->save(); | ||||
|                 } | ||||
| 
 | ||||
|                 // when mismatch in transaction:
 | ||||
|                 if ($transaction->transaction_currency_id !== $currency->id) { | ||||
|                 if ($transaction->transaction_currency_id !== $currency->id && !$isMultiCurrency) { | ||||
|                     $transaction->foreign_currency_id     = $transaction->transaction_currency_id; | ||||
|                     $transaction->foreign_amount          = $transaction->amount; | ||||
|                     $transaction->transaction_currency_id = $currency->id; | ||||
| @@ -161,7 +162,9 @@ class OtherCurrenciesCorrections extends Command | ||||
|             } | ||||
|         ); | ||||
|         // also update the journal, of course:
 | ||||
|         $journal->transaction_currency_id = $currency->id; | ||||
|         if (!$isMultiCurrency) { | ||||
|             $journal->transaction_currency_id = $currency->id; | ||||
|         } | ||||
|         ++$this->count; | ||||
|         $journal->save(); | ||||
|     } | ||||
| @@ -239,4 +242,14 @@ class OtherCurrenciesCorrections extends Command | ||||
|     { | ||||
|         app('fireflyconfig')->set(self::CONFIG_NAME, true); | ||||
|     } | ||||
| 
 | ||||
|     private function isMultiCurrency(Account $account): bool | ||||
|     { | ||||
|         $value = $this->accountRepos->getMetaValue($account, 'is_multi_currency', false); | ||||
|         if (false === $value || null === $value) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return '1' === $value; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -65,10 +65,12 @@ class UpgradeDatabase extends Command | ||||
|             'firefly-iii:budget-limit-periods', | ||||
|             'firefly-iii:migrate-rule-actions', | ||||
|             'firefly-iii:restore-oauth-keys', | ||||
|             'firefly-iii:correct-account-balance', | ||||
|             // also just in case, some integrity commands:
 | ||||
|             'firefly-iii:create-group-memberships', | ||||
|             'firefly-iii:upgrade-group-information', | ||||
|             'firefly-iii:upgrade-currency-preferences', | ||||
|             'firefly-iii:correct-database', | ||||
|         ]; | ||||
|         $args     = []; | ||||
|         if ($this->option('force')) { | ||||
|   | ||||
							
								
								
									
										49
									
								
								app/Entities/AccountBalance.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/Entities/AccountBalance.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| <?php | ||||
| /* | ||||
|  * AccountBalance.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Entities; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| 
 | ||||
| class AccountBalance | ||||
| { | ||||
|     public string $id; | ||||
|     public string $amount; | ||||
|     public string $currencyId; | ||||
| 
 | ||||
|     public static function fromArray(): self | ||||
|     { | ||||
|         $balance             = new self(); | ||||
|         $balance->id         = (string) random_int(1, 1000); | ||||
|         $balance->name       = (string) random_int(1, 1000); | ||||
|         $balance->amount     = (string) random_int(1, 1000); | ||||
|         $balance->currencyId = '1'; | ||||
| 
 | ||||
|         return $balance; | ||||
|     } | ||||
| 
 | ||||
|     public function getAccount(): Account | ||||
|     { | ||||
|         return Account::inRandomOrder()->first(); | ||||
|     } | ||||
| } | ||||
| @@ -41,7 +41,6 @@ enum UserRoleEnum: string | ||||
|     // manage other financial objects:
 | ||||
|     case MANAGE_BUDGETS       = 'mng_budgets'; | ||||
|     case MANAGE_PIGGY_BANKS   = 'mng_piggies'; | ||||
|     case MANAGE_REPETITIONS   = 'mng_reps'; | ||||
|     case MANAGE_SUBSCRIPTIONS = 'mng_subscriptions'; | ||||
|     case MANAGE_RULES         = 'mng_rules'; | ||||
|     case MANAGE_RECURRING     = 'mng_recurring'; | ||||
| @@ -51,7 +50,7 @@ enum UserRoleEnum: string | ||||
|     // view and generate reports
 | ||||
|     case VIEW_REPORTS         = 'view_reports'; | ||||
| 
 | ||||
|     // view memberships. needs FULL to manage them.
 | ||||
|     // view memberships AND roles. needs FULL to manage them.
 | ||||
|     case VIEW_MEMBERSHIPS     = 'view_memberships'; | ||||
| 
 | ||||
|     // everything the creator can, except remove/change original creator and delete group
 | ||||
|   | ||||
| @@ -25,6 +25,7 @@ declare(strict_types=1); | ||||
| namespace FireflyIII\Exceptions; | ||||
| 
 | ||||
| use FireflyIII\Jobs\MailError; | ||||
| use Illuminate\Auth\Access\AuthorizationException; | ||||
| use Illuminate\Auth\AuthenticationException; | ||||
| use Illuminate\Database\QueryException; | ||||
| use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; | ||||
| @@ -35,6 +36,8 @@ use Illuminate\Session\TokenMismatchException; | ||||
| use Illuminate\Support\Arr; | ||||
| use Illuminate\Validation\ValidationException as LaravelValidationException; | ||||
| use Laravel\Passport\Exceptions\OAuthServerException as LaravelOAuthException; | ||||
| use LaravelJsonApi\Core\Exceptions\JsonApiException; | ||||
| use LaravelJsonApi\Exceptions\ExceptionParser; | ||||
| use League\OAuth2\Server\Exception\OAuthServerException; | ||||
| use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| @@ -62,8 +65,19 @@ class Handler extends ExceptionHandler | ||||
|             HttpException::class, | ||||
|             SuspiciousOperationException::class, | ||||
|             BadHttpHeaderException::class, | ||||
|             JsonApiException::class, | ||||
|         ]; | ||||
| 
 | ||||
|     /** | ||||
|      * Register the exception handling callbacks for the application. | ||||
|      */ | ||||
|     public function register(): void | ||||
|     { | ||||
|         $this->renderable( | ||||
|             ExceptionParser::make()->renderable() | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Render an exception into an HTTP response. It's complex but lucky for us, we never use it because | ||||
|      * Firefly III never crashes. | ||||
| @@ -78,15 +92,23 @@ class Handler extends ExceptionHandler | ||||
|     public function render($request, \Throwable $e): Response | ||||
|     { | ||||
|         $expectsJson = $request->expectsJson(); | ||||
|         // if the user requests anything /api/, assume the user wants to see JSON.
 | ||||
|         if (str_starts_with($request->getRequestUri(), '/api/')) { | ||||
|             app('log')->debug('API endpoint, always assume user wants JSON.'); | ||||
|             $expectsJson = true; | ||||
|         } | ||||
| 
 | ||||
|         app('log')->debug('Now in Handler::render()'); | ||||
| 
 | ||||
|         if ($e instanceof JsonApiException) { | ||||
|             // ignore it: controller will handle it.
 | ||||
| 
 | ||||
|             app('log')->debug(sprintf( | ||||
|                 'Return to parent to handle JsonApiException(%d)', | ||||
|                 $e->getCode() | ||||
|             )); | ||||
| 
 | ||||
|             return parent::render($request, $e); | ||||
|         } | ||||
| 
 | ||||
|         if ($e instanceof LaravelValidationException && $expectsJson) { | ||||
|             // ignore it: controller will handle it.
 | ||||
| 
 | ||||
|             app('log')->debug(sprintf('Return to parent to handle LaravelValidationException(%d)', $e->status)); | ||||
| 
 | ||||
|             return parent::render($request, $e); | ||||
| @@ -98,6 +120,13 @@ class Handler extends ExceptionHandler | ||||
|             return response()->json(['message' => 'Resource not found', 'exception' => 'NotFoundHttpException'], 404); | ||||
|         } | ||||
| 
 | ||||
|         if ($e instanceof AuthorizationException && $expectsJson) { | ||||
|             // somehow Laravel handler does not catch this:
 | ||||
|             app('log')->debug('Return JSON unauthorized error.'); | ||||
| 
 | ||||
|             return response()->json(['message' => $e->getMessage(), 'exception' => 'AuthorizationException'], 401); | ||||
|         } | ||||
| 
 | ||||
|         if ($e instanceof AuthenticationException && $expectsJson) { | ||||
|             // somehow Laravel handler does not catch this:
 | ||||
|             app('log')->debug('Return JSON unauthenticated error.'); | ||||
|   | ||||
| @@ -43,7 +43,7 @@ class AttachmentFactory | ||||
|     public function create(array $data): ?Attachment | ||||
|     { | ||||
|         // append if necessary.
 | ||||
|         $model      = !str_contains($data['attachable_type'], 'FireflyIII') ? sprintf('FireflyIII\\Models\\%s', $data['attachable_type']) | ||||
|         $model      = !str_contains($data['attachable_type'], 'FireflyIII') ? sprintf('FireflyIII\Models\%s', $data['attachable_type']) | ||||
|             : $data['attachable_type']; | ||||
| 
 | ||||
|         // get journal instead of transaction.
 | ||||
|   | ||||
| @@ -47,6 +47,7 @@ use FireflyIII\Support\NullArrayObject; | ||||
| use FireflyIII\User; | ||||
| use FireflyIII\Validation\AccountValidator; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class TransactionJournalFactory | ||||
| @@ -261,8 +262,24 @@ class TransactionJournalFactory | ||||
|         $transactionFactory->setForeignCurrency($foreignCurrency); | ||||
|         $transactionFactory->setReconciled($row['reconciled'] ?? false); | ||||
| 
 | ||||
|         // if the foreign currency is set and is different, and the transaction type is a transfer,
 | ||||
|         // Firefly III will save the foreign currency information in such a way that both
 | ||||
|         // asset accounts can look at the "amount" and "transaction_currency_id" column and
 | ||||
|         // see the currency they expect to see.
 | ||||
|         $amount                = (string)$row['amount']; | ||||
|         $foreignAmount         = (string)$row['foreign_amount']; | ||||
|         if (null !== $foreignCurrency && $foreignCurrency->id !== $currency->id | ||||
|         && TransactionType::TRANSFER === $type->type | ||||
|         ) { | ||||
|             $transactionFactory->setCurrency($foreignCurrency); | ||||
|             $transactionFactory->setForeignCurrency($currency); | ||||
|             $amount        = (string)$row['foreign_amount']; | ||||
|             $foreignAmount = (string)$row['amount']; | ||||
|             Log::debug('Swap native/foreign amounts in transfer for new save method.'); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             $transactionFactory->createPositive((string)$row['amount'], (string)$row['foreign_amount']); | ||||
|             $transactionFactory->createPositive($amount, $foreignAmount); | ||||
|         } catch (FireflyException $e) { | ||||
|             app('log')->error(sprintf('Exception creating positive transaction: %s', $e->getMessage())); | ||||
|             $this->forceTrDelete($negative); | ||||
|   | ||||
| @@ -405,7 +405,7 @@ class UserEventHandler | ||||
|             } | ||||
|             // clean up old entries (6 months)
 | ||||
|             $carbon = Carbon::createFromFormat('Y-m-d H:i:s', $preference[$index]['time']); | ||||
|             if (false !== $carbon && $carbon->diffInMonths(today(), true) > 6) { | ||||
|             if (null !== $carbon && $carbon->diffInMonths(today(), true) > 6) { | ||||
|                 app('log')->debug(sprintf('Entry for %s is very old, remove it.', $row['ip'])); | ||||
|                 unset($preference[$index]); | ||||
|             } | ||||
|   | ||||
| @@ -24,6 +24,8 @@ declare(strict_types=1); | ||||
| namespace FireflyIII\Handlers\Observer; | ||||
| 
 | ||||
| use FireflyIII\Models\Transaction; | ||||
| use FireflyIII\Support\Models\AccountBalanceCalculator; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class TransactionObserver | ||||
| @@ -35,4 +37,22 @@ class TransactionObserver | ||||
|         app('log')->debug('Observe "deleting" of a transaction.'); | ||||
|         $transaction?->transactionJournal?->delete(); | ||||
|     } | ||||
| 
 | ||||
|     public function updated(Transaction $transaction): void | ||||
|     { | ||||
|         Log::debug('Observe "updated" of a transaction.'); | ||||
|         if (1 === bccomp($transaction->amount, '0')) { | ||||
|             Log::debug('Trigger recalculateForJournal'); | ||||
|             AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function created(Transaction $transaction): void | ||||
|     { | ||||
|         Log::debug('Observe "created" of a transaction.'); | ||||
|         if (1 === bccomp($transaction->amount, '0')) { | ||||
|             Log::debug('Trigger recalculateForJournal'); | ||||
|             AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -717,7 +717,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $internalReference)); | ||||
|         $this->query->where('journal_meta.data', '=', sprintf('%s', json_encode($internalReference))); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
|   | ||||
| @@ -805,7 +805,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     return 'zzz'; | ||||
|                 } | ||||
| 
 | ||||
|                 exit('here we are'); | ||||
|                 exit('here we are 2'); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|   | ||||
| @@ -145,13 +145,13 @@ class CreateController extends Controller | ||||
|         Log::channel('audit')->info('Stored new account.', $data); | ||||
| 
 | ||||
|         // update preferences if necessary:
 | ||||
|         $frontPage = app('preferences')->get('frontPageAccounts', [])->data; | ||||
|         if (!is_array($frontPage)) { | ||||
|             $frontPage = []; | ||||
|         $frontpage = app('preferences')->get('frontpageAccounts', [])->data; | ||||
|         if (!is_array($frontpage)) { | ||||
|             $frontpage = []; | ||||
|         } | ||||
|         if (AccountType::ASSET === $account->accountType->type) { | ||||
|             $frontPage[] = $account->id; | ||||
|             app('preferences')->set('frontPageAccounts', $frontPage); | ||||
|             $frontpage[] = $account->id; | ||||
|             app('preferences')->set('frontpageAccounts', $frontpage); | ||||
|         } | ||||
| 
 | ||||
|         // store attachment(s):
 | ||||
|   | ||||
| @@ -106,7 +106,7 @@ class ShowController extends Controller | ||||
|         $periods          = $this->getAccountPeriodOverview($account, $firstTransaction, $end); | ||||
| 
 | ||||
|         // if layout = v2, overrule the page title.
 | ||||
|         if ('v1' !== config('firefly.layout')) { | ||||
|         if ('v1' !== config('view.layout')) { | ||||
|             $subTitle = (string)trans('firefly.all_journals_for_account', ['name' => $account->name]); | ||||
|         } | ||||
| 
 | ||||
|   | ||||
| @@ -31,6 +31,7 @@ use Illuminate\Contracts\View\Factory; | ||||
| use Illuminate\Foundation\Auth\SendsPasswordResetEmails; | ||||
| use Illuminate\Http\RedirectResponse; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use Illuminate\View\View; | ||||
| 
 | ||||
| /** | ||||
| @@ -106,6 +107,8 @@ class ForgotPasswordController extends Controller | ||||
|         } | ||||
|         $host           = request()->host(); | ||||
|         if ($configuredHost !== $host) { | ||||
|             Log::error(sprintf('Host header is "%s", APP_URL is "%s".', $host, $configuredHost)); | ||||
| 
 | ||||
|             throw new FireflyException('The Host-header does not match the host in the APP_URL environment variable. Please make sure these match. See also: https://bit.ly/FF3-host-header'); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -121,7 +121,7 @@ class LoginController extends Controller | ||||
| 
 | ||||
|         // Copied directly from AuthenticatesUsers, but with logging added:
 | ||||
|         // If the login attempt was unsuccessful we will increment the number of attempts
 | ||||
|         // to login and redirect the user back to the login form. Of course, when this
 | ||||
|         // to log in and redirect the user back to the login form. Of course, when this
 | ||||
|         // user surpasses their maximum number of attempts they will get locked out.
 | ||||
|         $this->incrementLoginAttempts($request); | ||||
|         Log::channel('audit')->warning(sprintf('Login failed. Attempt for user "%s" failed.', $request->get($this->username()))); | ||||
| @@ -233,7 +233,7 @@ class LoginController extends Controller | ||||
|         $storeInCookie     = config('google2fa.store_in_cookie', false); | ||||
|         if (false !== $storeInCookie) { | ||||
|             $cookieName = config('google2fa.cookie_name', 'google2fa_token'); | ||||
|             request()->cookies->set($cookieName, 'invalid'); | ||||
|             \Cookie::queue(\Cookie::make($cookieName, 'invalid-'.time())); | ||||
|         } | ||||
|         $usernameField     = $this->username(); | ||||
| 
 | ||||
|   | ||||
| @@ -128,7 +128,7 @@ class BudgetLimitController extends Controller | ||||
|         $start    = Carbon::createFromFormat('Y-m-d', $request->get('start')); | ||||
|         $end      = Carbon::createFromFormat('Y-m-d', $request->get('end')); | ||||
| 
 | ||||
|         if (false === $start || false === $end) { | ||||
|         if (null === $start || null === $end) { | ||||
|             return response()->json([]); | ||||
|         } | ||||
| 
 | ||||
| @@ -185,7 +185,7 @@ class BudgetLimitController extends Controller | ||||
|             $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']           = 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']); | ||||
| @@ -235,12 +235,12 @@ class BudgetLimitController extends Controller | ||||
|             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)$this->activeDaysLeft($limit->start_date, $limit->end_date); | ||||
|         // left per day:
 | ||||
|         $array['left_per_day']           = bcdiv(bcadd($array['spent'], $array['amount']), $array['days_left']); | ||||
|         $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); | ||||
|   | ||||
| @@ -301,14 +301,14 @@ class AccountController extends Controller | ||||
|         $end            = clone session('end', today(config('app.timezone'))->endOfMonth()); | ||||
|         $defaultSet     = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray(); | ||||
|         app('log')->debug('Default set is ', $defaultSet); | ||||
|         $frontPage      = app('preferences')->get('frontPageAccounts', $defaultSet); | ||||
|         $frontPageArray = !is_array($frontPage->data) ? [] : $frontPage->data; | ||||
|         app('log')->debug('Frontpage preference set is ', $frontPageArray); | ||||
|         if (0 === count($frontPageArray)) { | ||||
|             app('preferences')->set('frontPageAccounts', $defaultSet); | ||||
|         $frontpage      = app('preferences')->get('frontpageAccounts', $defaultSet); | ||||
|         $frontpageArray = !is_array($frontpage->data) ? [] : $frontpage->data; | ||||
|         app('log')->debug('Frontpage preference set is ', $frontpageArray); | ||||
|         if (0 === count($frontpageArray)) { | ||||
|             app('preferences')->set('frontpageAccounts', $defaultSet); | ||||
|             app('log')->debug('frontpage set is empty!'); | ||||
|         } | ||||
|         $accounts       = $repository->getAccountsById($frontPageArray); | ||||
|         $accounts       = $repository->getAccountsById($frontpageArray); | ||||
| 
 | ||||
|         return response()->json($this->accountBalanceChart($accounts, $start, $end)); | ||||
|     } | ||||
|   | ||||
| @@ -116,8 +116,8 @@ class CategoryController extends Controller | ||||
|             return response()->json($cache->get()); | ||||
|         } | ||||
| 
 | ||||
|         $frontPageGenerator = new FrontpageChartGenerator($start, $end); | ||||
|         $chartData          = $frontPageGenerator->generate(); | ||||
|         $frontpageGenerator = new FrontpageChartGenerator($start, $end); | ||||
|         $chartData          = $frontpageGenerator->generate(); | ||||
|         $data               = $this->generator->multiSet($chartData); | ||||
|         $cache->store($data); | ||||
| 
 | ||||
|   | ||||
| @@ -70,7 +70,7 @@ abstract class Controller extends BaseController | ||||
|         $logoutUrl        = config('firefly.custom_logout_url'); | ||||
| 
 | ||||
|         // overrule v2 layout back to v1.
 | ||||
|         if ('true' === request()->get('force_default_layout') && 'v2' === config('firefly.layout')) { | ||||
|         if ('true' === request()->get('force_default_layout') && 'v2' === config('view.layout')) { | ||||
|             app('view')->getFinder()->setPaths([realpath(base_path('resources/views'))]); // @phpstan-ignore-line
 | ||||
|         } | ||||
| 
 | ||||
|   | ||||
| @@ -30,6 +30,7 @@ use FireflyIII\Http\Middleware\IsDemoUser; | ||||
| use FireflyIII\Models\AccountType; | ||||
| use FireflyIII\Models\TransactionType; | ||||
| use FireflyIII\Support\Http\Controllers\GetConfigurationData; | ||||
| use FireflyIII\Support\Models\AccountBalanceCalculator; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\View\Factory; | ||||
| use Illuminate\Http\RedirectResponse; | ||||
| @@ -86,14 +87,15 @@ class DebugController extends Controller | ||||
|     { | ||||
|         app('preferences')->mark(); | ||||
|         $request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range', 'temp-mfa-secret', 'temp-mfa-codes']); | ||||
|         app('log')->debug('Call cache:clear...'); | ||||
| 
 | ||||
|         Artisan::call('cache:clear'); | ||||
|         app('log')->debug('Call config:clear...'); | ||||
|         Artisan::call('config:clear'); | ||||
|         app('log')->debug('Call route:clear...'); | ||||
|         Artisan::call('route:clear'); | ||||
|         app('log')->debug('Call twig:clean...'); | ||||
|         Artisan::call('view:clear'); | ||||
| 
 | ||||
|         // also do some recalculations.
 | ||||
|         Artisan::call('firefly-iii:trigger-credit-recalculation'); | ||||
|         AccountBalanceCalculator::forceRecalculateAll(); | ||||
| 
 | ||||
|         try { | ||||
|             Artisan::call('twig:clean'); | ||||
| @@ -101,7 +103,6 @@ class DebugController extends Controller | ||||
|             throw new FireflyException($e->getMessage(), 0, $e); | ||||
|         } | ||||
| 
 | ||||
|         app('log')->debug('Call view:clear...'); | ||||
|         Artisan::call('view:clear'); | ||||
| 
 | ||||
|         return redirect(route('index')); | ||||
|   | ||||
| @@ -65,7 +65,7 @@ class HomeController extends Controller | ||||
|         $stringEnd     = ''; | ||||
| 
 | ||||
|         try { | ||||
|             $stringStart = e((string)$request->get('start')); | ||||
|             $stringStart = e((string) $request->get('start')); | ||||
|             $start       = Carbon::createFromFormat('Y-m-d', $stringStart); | ||||
|         } catch (InvalidFormatException $e) { | ||||
|             app('log')->error(sprintf('Start: could not parse date string "%s" so ignore it.', $stringStart)); | ||||
| @@ -73,16 +73,16 @@ class HomeController extends Controller | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             $stringEnd = e((string)$request->get('end')); | ||||
|             $stringEnd = e((string) $request->get('end')); | ||||
|             $end       = Carbon::createFromFormat('Y-m-d', $stringEnd); | ||||
|         } catch (InvalidFormatException $e) { | ||||
|             app('log')->error(sprintf('End could not parse date string "%s" so ignore it.', $stringEnd)); | ||||
|             $end = Carbon::now()->endOfMonth(); | ||||
|         } | ||||
|         if (false === $start) { | ||||
|         if (null === $start) { | ||||
|             $start = Carbon::now()->startOfMonth(); | ||||
|         } | ||||
|         if (false === $end) { | ||||
|         if (null === $end) { | ||||
|             $end = Carbon::now()->endOfMonth(); | ||||
|         } | ||||
| 
 | ||||
| @@ -92,7 +92,7 @@ class HomeController extends Controller | ||||
|         app('log')->debug('dateRange: Received dateRange', ['start' => $stringStart, 'end' => $stringEnd, 'label' => $request->get('label')]); | ||||
|         // check if the label is "everything" or "Custom range" which will betray
 | ||||
|         // a possible problem with the budgets.
 | ||||
|         if ($label === (string)trans('firefly.everything') || $label === (string)trans('firefly.customRange')) { | ||||
|         if ($label === (string) trans('firefly.everything') || $label === (string) trans('firefly.customRange')) { | ||||
|             $isCustomRange = true; | ||||
|             app('log')->debug('Range is now marked as "custom".'); | ||||
|         } | ||||
| @@ -100,7 +100,7 @@ class HomeController extends Controller | ||||
|         $diff          = $start->diffInDays($end, true) + 1; | ||||
| 
 | ||||
|         if ($diff > 366) { | ||||
|             $request->session()->flash('warning', (string)trans('firefly.warning_much_data', ['days' => (int)$diff])); | ||||
|             $request->session()->flash('warning', (string) trans('firefly.warning_much_data', ['days' => (int) $diff])); | ||||
|         } | ||||
| 
 | ||||
|         $request->session()->put('is_custom_range', $isCustomRange); | ||||
| @@ -120,19 +120,35 @@ class HomeController extends Controller | ||||
|      */ | ||||
|     public function index(AccountRepositoryInterface $repository): mixed | ||||
|     { | ||||
|         $types          = config('firefly.accountTypesByIdentifier.asset'); | ||||
|         $count          = $repository->count($types); | ||||
|         $types = config('firefly.accountTypesByIdentifier.asset'); | ||||
|         $count = $repository->count($types); | ||||
|         Log::channel('audit')->info('User visits homepage.'); | ||||
| 
 | ||||
|         if (0 === $count) { | ||||
|             return redirect(route('new-user.index')); | ||||
|         } | ||||
|         $subTitle       = (string)trans('firefly.welcome_back'); | ||||
| 
 | ||||
|         if ('v1' === (string) config('view.layout')) { | ||||
|             return $this->indexV1($repository); | ||||
|         } | ||||
|         if ('v2' === (string) config('view.layout')) { | ||||
|             return $this->indexV2(); | ||||
|         } | ||||
| 
 | ||||
|         throw new FireflyException('Invalid layout configuration'); | ||||
|     } | ||||
| 
 | ||||
|     private function indexV1(AccountRepositoryInterface $repository): mixed | ||||
|     { | ||||
|         $types          = config('firefly.accountTypesByIdentifier.asset'); | ||||
|         $pageTitle      = (string) trans('firefly.main_dashboard_page_title'); | ||||
|         $count          = $repository->count($types); | ||||
|         $subTitle       = (string) trans('firefly.welcome_back'); | ||||
|         $transactions   = []; | ||||
|         $frontPage      = app('preferences')->getFresh('frontPageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray()); | ||||
|         $frontPageArray = $frontPage->data; | ||||
|         if (!is_array($frontPageArray)) { | ||||
|             $frontPageArray = []; | ||||
|         $frontpage      = app('preferences')->getFresh('frontpageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray()); | ||||
|         $frontpageArray = $frontpage->data; | ||||
|         if (!is_array($frontpageArray)) { | ||||
|             $frontpageArray = []; | ||||
|         } | ||||
| 
 | ||||
|         /** @var Carbon $start */ | ||||
| @@ -140,13 +156,11 @@ class HomeController extends Controller | ||||
| 
 | ||||
|         /** @var Carbon $end */ | ||||
|         $end            = session('end', today(config('app.timezone'))->endOfMonth()); | ||||
|         $accounts       = $repository->getAccountsById($frontPageArray); | ||||
|         $accounts       = $repository->getAccountsById($frontpageArray); | ||||
|         $today          = today(config('app.timezone')); | ||||
|         $accounts       = $accounts->sortBy('order'); // sort frontpage accounts by order
 | ||||
| 
 | ||||
|         // sort frontpage accounts by order
 | ||||
|         $accounts       = $accounts->sortBy('order'); | ||||
| 
 | ||||
|         app('log')->debug('Frontpage accounts are ', $frontPageArray); | ||||
|         app('log')->debug('Frontpage accounts are ', $frontpageArray); | ||||
| 
 | ||||
|         /** @var BillRepositoryInterface $billRepository */ | ||||
|         $billRepository = app(BillRepositoryInterface::class); | ||||
| @@ -164,6 +178,21 @@ class HomeController extends Controller | ||||
|         $user           = auth()->user(); | ||||
|         event(new RequestedVersionCheckStatus($user)); | ||||
| 
 | ||||
|         return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today')); | ||||
|         return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today', 'pageTitle')); | ||||
|     } | ||||
| 
 | ||||
|     private function indexV2(): mixed | ||||
|     { | ||||
|         $subTitle  = (string) trans('firefly.welcome_back'); | ||||
|         $pageTitle = (string) trans('firefly.main_dashboard_page_title'); | ||||
| 
 | ||||
|         $start     = session('start', today(config('app.timezone'))->startOfMonth()); | ||||
|         $end       = session('end', today(config('app.timezone'))->endOfMonth()); | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user      = auth()->user(); | ||||
|         event(new RequestedVersionCheckStatus($user)); | ||||
| 
 | ||||
|         return view('index', compact('subTitle', 'start', 'end', 'pageTitle')); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -130,7 +130,7 @@ class BoxController extends Controller | ||||
|                 $boxTitle         = (string)trans('firefly.left_to_spend'); | ||||
|                 $activeDaysLeft   = $this->activeDaysLeft($start, $end);   // see method description.
 | ||||
|                 $display          = 1;                                     // not overspent
 | ||||
|                 $leftPerDayAmount = 0 === (int) $activeDaysLeft ? $leftToSpendAmount : bcdiv($leftToSpendAmount, (string)$activeDaysLeft); | ||||
|                 $leftPerDayAmount = 0 === $activeDaysLeft ? $leftToSpendAmount : bcdiv($leftToSpendAmount, (string)$activeDaysLeft); | ||||
|                 app('log')->debug(sprintf('Left to spend per day is %s', $leftPerDayAmount)); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -81,6 +81,8 @@ class ReconcileController extends Controller | ||||
|         if ($end->lt($start)) { | ||||
|             [$start, $end] = [$end, $start]; | ||||
|         } | ||||
|         $end->endOfDay(); | ||||
|         $start->startOfDay(); | ||||
| 
 | ||||
|         $route           = route('accounts.reconcile.submit', [$account->id, $start->format('Ymd'), $end->format('Ymd')]); | ||||
|         $selectedIds     = $request->get('journals') ?? []; | ||||
|   | ||||
| @@ -81,7 +81,12 @@ class RecurrenceController extends Controller | ||||
|         $skip                          = $skip < 0 || $skip > 31 ? 0 : $skip; | ||||
|         $weekend                       = $weekend < 1 || $weekend > 4 ? 1 : $weekend; | ||||
| 
 | ||||
|         if (false === $start || false === $end || false === $firstDate || false === $endDate) { | ||||
|         if (null === $endDate) { | ||||
|             // safety catch:
 | ||||
|             $endDate = now()->addYear(); | ||||
|         } | ||||
| 
 | ||||
|         if (null === $start || null === $end || null === $firstDate) { | ||||
|             return response()->json(); | ||||
|         } | ||||
| 
 | ||||
| @@ -112,7 +117,7 @@ class RecurrenceController extends Controller | ||||
|         $actualEnd                     = clone $end; | ||||
| 
 | ||||
|         if ('until_date' === $endsAt) { | ||||
|             $actualEnd   = $endDate ?? clone $end; | ||||
|             $actualEnd   = $endDate; | ||||
|             $occurrences = $this->recurring->getOccurrencesInRange($repetition, $actualStart, $actualEnd); | ||||
|         } | ||||
|         if ('times' === $endsAt) { | ||||
| @@ -155,7 +160,7 @@ class RecurrenceController extends Controller | ||||
|         } catch (InvalidFormatException $e) { | ||||
|             $date = Carbon::today(config('app.timezone')); | ||||
|         } | ||||
|         if (false === $date) { | ||||
|         if (null === $date) { | ||||
|             return response()->json(); | ||||
|         } | ||||
|         $date->startOfDay(); | ||||
|   | ||||
| @@ -115,7 +115,7 @@ class NewUserController extends Controller | ||||
| 
 | ||||
|         // store frontpage preferences:
 | ||||
|         $accounts      = $this->repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray(); | ||||
|         app('preferences')->set('frontPageAccounts', $accounts); | ||||
|         app('preferences')->set('frontpageAccounts', $accounts); | ||||
| 
 | ||||
|         // mark.
 | ||||
|         app('preferences')->mark(); | ||||
|   | ||||
| @@ -89,10 +89,10 @@ class PreferencesController extends Controller | ||||
|         /** @var array<int, int> $accountIds */ | ||||
|         $accountIds            = $accounts->pluck('id')->toArray(); | ||||
|         $viewRange             = app('navigation')->getViewRange(false); | ||||
|         $frontPageAccountsPref = app('preferences')->get('frontPageAccounts', $accountIds); | ||||
|         $frontPageAccounts     = $frontPageAccountsPref->data; | ||||
|         if (!is_array($frontPageAccounts)) { | ||||
|             $frontPageAccounts = $accountIds; | ||||
|         $frontpageAccountsPref = app('preferences')->get('frontpageAccounts', $accountIds); | ||||
|         $frontpageAccounts     = $frontpageAccountsPref->data; | ||||
|         if (!is_array($frontpageAccounts)) { | ||||
|             $frontpageAccounts = $accountIds; | ||||
|         } | ||||
|         $language              = app('steam')->getLanguage(); | ||||
|         $languages             = config('firefly.languages'); | ||||
| @@ -120,7 +120,7 @@ class PreferencesController extends Controller | ||||
|         // list of locales also has "equal" which makes it equal to whatever the language is.
 | ||||
| 
 | ||||
|         try { | ||||
|             $locales = json_decode((string)file_get_contents(resource_path(sprintf('lang/%s/locales.json', $language))), true, 512, JSON_THROW_ON_ERROR); | ||||
|             $locales = json_decode((string)file_get_contents(resource_path(sprintf('locales/%s/locales.json', $language))), true, 512, JSON_THROW_ON_ERROR); | ||||
|         } catch (\JsonException $e) { | ||||
|             app('log')->error($e->getMessage()); | ||||
|             $locales = []; | ||||
| @@ -128,8 +128,8 @@ class PreferencesController extends Controller | ||||
|         $locales               = ['equal' => (string)trans('firefly.equal_to_language')] + $locales; | ||||
|         // an important fallback is that the frontPageAccount array gets refilled automatically
 | ||||
|         // when it turns up empty.
 | ||||
|         if (0 === count($frontPageAccounts)) { | ||||
|             $frontPageAccounts = $accountIds; | ||||
|         if (0 === count($frontpageAccounts)) { | ||||
|             $frontpageAccounts = $accountIds; | ||||
|         } | ||||
| 
 | ||||
|         // for the demo user, the slackUrl is automatically emptied.
 | ||||
| @@ -139,7 +139,7 @@ class PreferencesController extends Controller | ||||
|             $slackUrl = ''; | ||||
|         } | ||||
| 
 | ||||
|         return view('preferences.index', compact('language', 'groupedAccounts', 'isDocker', 'frontPageAccounts', 'languages', 'darkMode', 'availableDarkModes', 'notifications', 'slackUrl', 'locales', 'locale', 'tjOptionalFields', 'viewRange', 'customFiscalYear', 'listPageSize', 'fiscalYearStart')); | ||||
|         return view('preferences.index', compact('language', 'groupedAccounts', 'isDocker', 'frontpageAccounts', 'languages', 'darkMode', 'availableDarkModes', 'notifications', 'slackUrl', 'locales', 'locale', 'tjOptionalFields', 'viewRange', 'customFiscalYear', 'listPageSize', 'fiscalYearStart')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -155,12 +155,12 @@ class PreferencesController extends Controller | ||||
|     public function postIndex(Request $request) | ||||
|     { | ||||
|         // front page accounts
 | ||||
|         $frontPageAccounts = []; | ||||
|         if (is_array($request->get('frontPageAccounts')) && count($request->get('frontPageAccounts')) > 0) { | ||||
|             foreach ($request->get('frontPageAccounts') as $id) { | ||||
|                 $frontPageAccounts[] = (int)$id; | ||||
|         $frontpageAccounts = []; | ||||
|         if (is_array($request->get('frontpageAccounts')) && count($request->get('frontpageAccounts')) > 0) { | ||||
|             foreach ($request->get('frontpageAccounts') as $id) { | ||||
|                 $frontpageAccounts[] = (int)$id; | ||||
|             } | ||||
|             app('preferences')->set('frontPageAccounts', $frontPageAccounts); | ||||
|             app('preferences')->set('frontpageAccounts', $frontpageAccounts); | ||||
|         } | ||||
| 
 | ||||
|         // extract notifications:
 | ||||
| @@ -223,8 +223,8 @@ class PreferencesController extends Controller | ||||
| 
 | ||||
|         // same for locale:
 | ||||
|         if (!auth()->user()->hasRole('demo')) { | ||||
|             /** @var Preference $locale */ | ||||
|             $locale = $request->get('locale'); | ||||
|             $locale = (string) $request->get('locale'); | ||||
|             $locale = '' === $locale ? null : $locale; | ||||
|             app('preferences')->set('locale', $locale); | ||||
|         } | ||||
| 
 | ||||
|   | ||||
| @@ -23,7 +23,6 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Http\Controllers; | ||||
| 
 | ||||
| use Auth; | ||||
| use FireflyIII\Events\UserChangedEmail; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Exceptions\ValidationException; | ||||
| @@ -467,9 +466,7 @@ class ProfileController extends Controller | ||||
|         if (is_array($secret)) { | ||||
|             $secret = null; | ||||
|         } | ||||
|         if (is_int($secret)) { | ||||
|             $secret = (string)$secret; | ||||
|         } | ||||
|         $secret     = (string)$secret; | ||||
| 
 | ||||
|         $repository->setMFACode($user, $secret); | ||||
| 
 | ||||
|   | ||||
| @@ -84,6 +84,7 @@ class ShowController extends Controller | ||||
|         $transformer->setParameters(new ParameterBag()); | ||||
| 
 | ||||
|         $array                 = $transformer->transform($recurrence); | ||||
| 
 | ||||
|         $groups                = $this->recurring->getTransactions($recurrence); | ||||
|         $today                 = today(config('app.timezone')); | ||||
|         $array['repeat_until'] = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null; | ||||
|   | ||||
| @@ -23,7 +23,6 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Http\Controllers\Transaction; | ||||
| 
 | ||||
| use Exception; | ||||
| use FireflyIII\Events\UpdatedTransactionGroup; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Http\Controllers\Controller; | ||||
| @@ -65,7 +64,7 @@ class ConvertController extends Controller | ||||
|         $this->middleware( | ||||
|             function ($request, $next) { | ||||
|                 $this->accountRepository = app(AccountRepositoryInterface::class); | ||||
|                 app('view')->share('title', (string)trans('firefly.transactions')); | ||||
|                 app('view')->share('title', (string) trans('firefly.transactions')); | ||||
|                 app('view')->share('mainTitleIcon', 'fa-exchange'); | ||||
| 
 | ||||
|                 return $next($request); | ||||
| @@ -95,7 +94,7 @@ class ConvertController extends Controller | ||||
| 
 | ||||
|         $groupTitle           = $group->title ?? $first->description; | ||||
|         $groupArray           = $transformer->transformObject($group); | ||||
|         $subTitle             = (string)trans('firefly.convert_to_'.$destinationType->type, ['description' => $groupTitle]); | ||||
|         $subTitle             = (string) trans('firefly.convert_to_'.$destinationType->type, ['description' => $groupTitle]); | ||||
|         $subTitleIcon         = 'fa-exchange'; | ||||
| 
 | ||||
|         // get a list of asset accounts and liabilities and stuff, in various combinations:
 | ||||
| @@ -111,7 +110,7 @@ class ConvertController extends Controller | ||||
| 
 | ||||
|         if ($sourceType->type === $destinationType->type) { // cannot convert to its own type.
 | ||||
|             app('log')->debug('This is already a transaction of the expected type..'); | ||||
|             session()->flash('info', (string)trans('firefly.convert_is_already_type_'.$destinationType->type)); | ||||
|             session()->flash('info', (string) trans('firefly.convert_is_already_type_'.$destinationType->type)); | ||||
| 
 | ||||
|             return redirect(route('transactions.show', [$group->id])); | ||||
|         } | ||||
| @@ -147,7 +146,7 @@ class ConvertController extends Controller | ||||
|         // group accounts:
 | ||||
|         /** @var Account $account */ | ||||
|         foreach ($accountList as $account) { | ||||
|             $role                        = (string)$this->accountRepository->getMetaValue($account, 'account_role'); | ||||
|             $role                        = (string) $this->accountRepository->getMetaValue($account, 'account_role'); | ||||
|             $name                        = $account->name; | ||||
|             if ('' === $role) { | ||||
|                 $role = 'no_account_type'; | ||||
| @@ -165,7 +164,7 @@ class ConvertController extends Controller | ||||
|                 $role = 'revenue_account'; | ||||
|             } | ||||
| 
 | ||||
|             $key                         = (string)trans('firefly.opt_group_'.$role); | ||||
|             $key                         = (string) trans('firefly.opt_group_'.$role); | ||||
|             $grouped[$key][$account->id] = $name; | ||||
|         } | ||||
| 
 | ||||
| @@ -184,7 +183,7 @@ class ConvertController extends Controller | ||||
|         // group accounts:
 | ||||
|         /** @var Account $account */ | ||||
|         foreach ($accountList as $account) { | ||||
|             $role                        = (string)$this->accountRepository->getMetaValue($account, 'account_role'); | ||||
|             $role                        = (string) $this->accountRepository->getMetaValue($account, 'account_role'); | ||||
|             $name                        = $account->name; | ||||
|             if ('' === $role) { | ||||
|                 $role = 'no_account_type'; | ||||
| @@ -202,7 +201,7 @@ class ConvertController extends Controller | ||||
|                 $role = 'expense_account'; | ||||
|             } | ||||
| 
 | ||||
|             $key                         = (string)trans('firefly.opt_group_'.$role); | ||||
|             $key                         = (string) trans('firefly.opt_group_'.$role); | ||||
|             $grouped[$key][$account->id] = $name; | ||||
|         } | ||||
| 
 | ||||
| @@ -225,7 +224,7 @@ class ConvertController extends Controller | ||||
|             $balance                     = app('steam')->balance($account, today()); | ||||
|             $currency                    = $this->accountRepository->getAccountCurrency($account) ?? $defaultCurrency; | ||||
|             $role                        = 'l_'.$account->accountType->type; | ||||
|             $key                         = (string)trans('firefly.opt_group_'.$role); | ||||
|             $key                         = (string) trans('firefly.opt_group_'.$role); | ||||
|             $grouped[$key][$account->id] = $account->name.' ('.app('amount')->formatAnything($currency, $balance, false).')'; | ||||
|         } | ||||
| 
 | ||||
| @@ -247,12 +246,12 @@ class ConvertController extends Controller | ||||
|         foreach ($accountList as $account) { | ||||
|             $balance                     = app('steam')->balance($account, today()); | ||||
|             $currency                    = $this->accountRepository->getAccountCurrency($account) ?? $defaultCurrency; | ||||
|             $role                        = (string)$this->accountRepository->getMetaValue($account, 'account_role'); | ||||
|             $role                        = (string) $this->accountRepository->getMetaValue($account, 'account_role'); | ||||
|             if ('' === $role) { | ||||
|                 $role = 'no_account_type'; | ||||
|             } | ||||
| 
 | ||||
|             $key                         = (string)trans('firefly.opt_group_'.$role); | ||||
|             $key                         = (string) trans('firefly.opt_group_'.$role); | ||||
|             $grouped[$key][$account->id] = $account->name.' ('.app('amount')->formatAnything($currency, $balance, false).')'; | ||||
|         } | ||||
| 
 | ||||
| @@ -285,7 +284,7 @@ class ConvertController extends Controller | ||||
|         // correct transfers:
 | ||||
|         $group->refresh(); | ||||
| 
 | ||||
|         session()->flash('success', (string)trans('firefly.converted_to_'.$destinationType->type)); | ||||
|         session()->flash('success', (string) trans('firefly.converted_to_'.$destinationType->type)); | ||||
|         event(new UpdatedTransactionGroup($group, true, true)); | ||||
| 
 | ||||
|         return redirect(route('transactions.show', [$group->id])); | ||||
| @@ -306,11 +305,11 @@ class ConvertController extends Controller | ||||
|         $destinationId    = $data['destination_id'][$journal->id] ?? null; | ||||
|         $destinationName  = $data['destination_name'][$journal->id] ?? null; | ||||
| 
 | ||||
|         // double check its not an empty string.
 | ||||
|         $sourceId         = '' === $sourceId || null === $sourceId ? null : (int)$sourceId; | ||||
|         $sourceName       = '' === $sourceName ? null : (string)$sourceName; | ||||
|         $destinationId    = '' === $destinationId || null === $destinationId ? null : (int)$destinationId; | ||||
|         $destinationName  = '' === $destinationName ? null : (string)$destinationName; | ||||
|         // double check it's not an empty string.
 | ||||
|         $sourceId         = '' === $sourceId || null === $sourceId ? null : (int) $sourceId; | ||||
|         $sourceName       = '' === $sourceName ? null : (string) $sourceName; | ||||
|         $destinationId    = '' === $destinationId || null === $destinationId ? null : (int) $destinationId; | ||||
|         $destinationName  = '' === $destinationName ? null : (string) $destinationName; | ||||
|         $validSource      = $validator->validateSource(['id' => $sourceId, 'name' => $sourceName]); | ||||
|         $validDestination = $validator->validateDestination(['id' => $destinationId, 'name' => $destinationName]); | ||||
| 
 | ||||
| @@ -331,6 +330,19 @@ class ConvertController extends Controller | ||||
|             'type'             => $transactionType->type, | ||||
|         ]; | ||||
| 
 | ||||
|         // also set the currency to the currency of the source account, in case you're converting a deposit into a transfer.
 | ||||
|         if (TransactionType::TRANSFER === $transactionType->type && TransactionType::DEPOSIT === $journal->transactionType->type) { | ||||
|             $source         = $this->accountRepository->find((int) $sourceId); | ||||
|             $sourceCurrency = $this->accountRepository->getAccountCurrency($source); | ||||
|             $dest           = $this->accountRepository->find((int) $destinationId); | ||||
|             $destCurrency   = $this->accountRepository->getAccountCurrency($dest); | ||||
|             if (null !== $sourceCurrency && null !== $destCurrency && $sourceCurrency->code !== $destCurrency->code) { | ||||
|                 $update['currency_id']         = $sourceCurrency->id; | ||||
|                 $update['foreign_currency_id'] = $destCurrency->id; | ||||
|                 $update['foreign_amount']      = '1'; // not the best solution but at this point the amount is hard to get.
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** @var JournalUpdateService $service */ | ||||
|         $service          = app(JournalUpdateService::class); | ||||
|         $service->setTransactionJournal($journal); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user