diff --git a/backend/composer.json b/backend/composer.json index d4d2f4b..2168914 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -12,9 +12,9 @@ "doctrine/doctrine-migrations-bundle": "^4.0", "doctrine/orm": "^3.6.2", "dompdf/dompdf": "^3.1.5", - "easycorp/easyadmin-bundle": "^5.0.1", + "easycorp/easyadmin-bundle": "^5.0.2", "nelmio/cors-bundle": "^2.6.1", - "phpdocumentor/reflection-docblock": "^6.0.2", + "phpdocumentor/reflection-docblock": "^6.0.3", "phpstan/phpdoc-parser": "^2.3.2", "symfony/browser-kit": "8.0.*", "symfony/console": "8.0.*", @@ -107,9 +107,9 @@ "require-dev": { "deptrac/deptrac": "^4.6", "friendsofphp/php-cs-fixer": "^3.94.2", - "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan": "^2.1.42", "phpunit/phpunit": "^13.0.5", - "symfony/maker-bundle": "^1.66.0", + "symfony/maker-bundle": "^1.67.0", "symfony/stopwatch": "8.0.*", "symfony/web-profiler-bundle": "8.0.*" } diff --git a/backend/composer.lock b/backend/composer.lock index 276ee4a..79270a3 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "073962760d6f2de03c46e4ea6beddf89", + "content-hash": "709e034126c4c31b85589c1ffb5b15c5", "packages": [ { "name": "doctrine/collections", @@ -1445,16 +1445,16 @@ }, { "name": "easycorp/easyadmin-bundle", - "version": "v5.0.1", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/EasyCorp/EasyAdminBundle.git", - "reference": "0bd155748233b5bb77c463008813dec325a1ec82" + "reference": "75a421266c93e268849fcc9e97da47038fa09519" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/0bd155748233b5bb77c463008813dec325a1ec82", - "reference": "0bd155748233b5bb77c463008813dec325a1ec82", + "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/75a421266c93e268849fcc9e97da47038fa09519", + "reference": "75a421266c93e268849fcc9e97da47038fa09519", "shasum": "" }, "require": { @@ -1492,6 +1492,7 @@ "require-dev": { "dama/doctrine-test-bundle": "^8.2", "doctrine/doctrine-fixtures-bundle": "^3.4|3.5.x-dev|^4.0", + "moneyphp/money": "^4.8", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.0", "phpstan/phpstan-phpunit": "^2.0", @@ -1539,7 +1540,7 @@ ], "support": { "issues": "https://github.com/EasyCorp/EasyAdminBundle/issues", - "source": "https://github.com/EasyCorp/EasyAdminBundle/tree/v5.0.1" + "source": "https://github.com/EasyCorp/EasyAdminBundle/tree/v5.0.2" }, "funding": [ { @@ -1547,7 +1548,7 @@ "type": "github" } ], - "time": "2026-03-01T18:53:23+00:00" + "time": "2026-03-08T19:34:29+00:00" }, { "name": "masterminds/html5", @@ -1839,16 +1840,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/897b5986ece6b4f9d8413fea345c7d49c757d6bf", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -1898,9 +1899,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-01T18:43:49+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2481,16 +2482,16 @@ }, { "name": "symfony/cache", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "59184fa14658d7724cd9b8743d91c1b1aa618bff" + "reference": "b7b0f4ce5fb57a8dc061d494639e44e2cf7aa30f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/59184fa14658d7724cd9b8743d91c1b1aa618bff", - "reference": "59184fa14658d7724cd9b8743d91c1b1aa618bff", + "url": "https://api.github.com/repos/symfony/cache/zipball/b7b0f4ce5fb57a8dc061d494639e44e2cf7aa30f", + "reference": "b7b0f4ce5fb57a8dc061d494639e44e2cf7aa30f", "shasum": "" }, "require": { @@ -2557,7 +2558,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.0.6" + "source": "https://github.com/symfony/cache/tree/v8.0.7" }, "funding": [ { @@ -2577,7 +2578,7 @@ "type": "tidelift" } ], - "time": "2026-02-21T23:29:37+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/cache-contracts", @@ -2734,16 +2735,16 @@ }, { "name": "symfony/config", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "94ea198de42f93dffa920a098cac3961a82e63b7" + "reference": "9a34c52187112503d02903ab35e6e3783f580c29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/94ea198de42f93dffa920a098cac3961a82e63b7", - "reference": "94ea198de42f93dffa920a098cac3961a82e63b7", + "url": "https://api.github.com/repos/symfony/config/zipball/9a34c52187112503d02903ab35e6e3783f580c29", + "reference": "9a34c52187112503d02903ab35e6e3783f580c29", "shasum": "" }, "require": { @@ -2788,7 +2789,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v8.0.6" + "source": "https://github.com/symfony/config/tree/v8.0.7" }, "funding": [ { @@ -2808,20 +2809,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/console", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "488285876e807a4777f074041d8bb508623419fa" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/488285876e807a4777f074041d8bb508623419fa", - "reference": "488285876e807a4777f074041d8bb508623419fa", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { @@ -2878,7 +2879,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.6" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -2898,20 +2899,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/dependency-injection", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "edd98864a7b9eaaa10f389bd414e7d9e816bb59d" + "reference": "1faaac6dbfe069a92ab9e5c270fa43fc4e1761da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/edd98864a7b9eaaa10f389bd414e7d9e816bb59d", - "reference": "edd98864a7b9eaaa10f389bd414e7d9e816bb59d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/1faaac6dbfe069a92ab9e5c270fa43fc4e1761da", + "reference": "1faaac6dbfe069a92ab9e5c270fa43fc4e1761da", "shasum": "" }, "require": { @@ -2959,7 +2960,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v8.0.6" + "source": "https://github.com/symfony/dependency-injection/tree/v8.0.7" }, "funding": [ { @@ -2979,7 +2980,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-03T07:49:33+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3050,16 +3051,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "ba48ecfce3356d928cb3fe6975963c936a2648c3" + "reference": "649eec3f9cc806e42ee2e7928d05425ed66108d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/ba48ecfce3356d928cb3fe6975963c936a2648c3", - "reference": "ba48ecfce3356d928cb3fe6975963c936a2648c3", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/649eec3f9cc806e42ee2e7928d05425ed66108d4", + "reference": "649eec3f9cc806e42ee2e7928d05425ed66108d4", "shasum": "" }, "require": { @@ -3128,7 +3129,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v8.0.6" + "source": "https://github.com/symfony/doctrine-bridge/tree/v8.0.7" }, "funding": [ { @@ -3148,7 +3149,7 @@ "type": "tidelift" } ], - "time": "2026-02-17T13:07:04+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/doctrine-messenger", @@ -3298,16 +3299,16 @@ }, { "name": "symfony/dotenv", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "94d59769b0ea491dd8b635089e766519d28773d6" + "reference": "23bd13cf3f6cca8b7661548ef958ff4f4aa7c458" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/94d59769b0ea491dd8b635089e766519d28773d6", - "reference": "94d59769b0ea491dd8b635089e766519d28773d6", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/23bd13cf3f6cca8b7661548ef958ff4f4aa7c458", + "reference": "23bd13cf3f6cca8b7661548ef958ff4f4aa7c458", "shasum": "" }, "require": { @@ -3348,7 +3349,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v8.0.6" + "source": "https://github.com/symfony/dotenv/tree/v8.0.7" }, "funding": [ { @@ -3368,7 +3369,7 @@ "type": "tidelift" } ], - "time": "2026-02-13T12:00:38+00:00" + "time": "2026-03-03T07:49:33+00:00" }, { "name": "symfony/error-handler", @@ -3825,16 +3826,16 @@ }, { "name": "symfony/form", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "104947c40b16aea6c7c45c0dc53f4c213940989d" + "reference": "954e17b053dad9fb227ebd90260752e3a46bb06a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/104947c40b16aea6c7c45c0dc53f4c213940989d", - "reference": "104947c40b16aea6c7c45c0dc53f4c213940989d", + "url": "https://api.github.com/repos/symfony/form/zipball/954e17b053dad9fb227ebd90260752e3a46bb06a", + "reference": "954e17b053dad9fb227ebd90260752e3a46bb06a", "shasum": "" }, "require": { @@ -3896,7 +3897,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v8.0.6" + "source": "https://github.com/symfony/form/tree/v8.0.7" }, "funding": [ { @@ -3916,20 +3917,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/framework-bundle", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "86ebd86908edca06e3af5994bc46881575fbe813" + "reference": "6a43d76538d52d4b7660f07054a07f8346f73eae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/86ebd86908edca06e3af5994bc46881575fbe813", - "reference": "86ebd86908edca06e3af5994bc46881575fbe813", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6a43d76538d52d4b7660f07054a07f8346f73eae", + "reference": "6a43d76538d52d4b7660f07054a07f8346f73eae", "shasum": "" }, "require": { @@ -4036,7 +4037,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v8.0.6" + "source": "https://github.com/symfony/framework-bundle/tree/v8.0.7" }, "funding": [ { @@ -4056,20 +4057,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-06T15:40:00+00:00" }, { "name": "symfony/http-foundation", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "7745ff1aad45d855fe25b08969269ef83b1ad8bc" + "reference": "c5ecf7b07408dbc4a87482634307654190954ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7745ff1aad45d855fe25b08969269ef83b1ad8bc", - "reference": "7745ff1aad45d855fe25b08969269ef83b1ad8bc", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c5ecf7b07408dbc4a87482634307654190954ae8", + "reference": "c5ecf7b07408dbc4a87482634307654190954ae8", "shasum": "" }, "require": { @@ -4116,7 +4117,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v8.0.6" + "source": "https://github.com/symfony/http-foundation/tree/v8.0.7" }, "funding": [ { @@ -4136,20 +4137,20 @@ "type": "tidelift" } ], - "time": "2026-02-21T16:28:39+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/http-kernel", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b567e571e74b5774b3d3cb4d35bdafa5f37e51a9" + "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b567e571e74b5774b3d3cb4d35bdafa5f37e51a9", - "reference": "b567e571e74b5774b3d3cb4d35bdafa5f37e51a9", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c04721f45723d8ce049fa3eee378b5a505272ac7", + "reference": "c04721f45723d8ce049fa3eee378b5a505272ac7", "shasum": "" }, "require": { @@ -4220,7 +4221,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v8.0.6" + "source": "https://github.com/symfony/http-kernel/tree/v8.0.7" }, "funding": [ { @@ -4240,7 +4241,7 @@ "type": "tidelift" } ], - "time": "2026-02-26T08:36:42+00:00" + "time": "2026-03-06T16:58:46+00:00" }, { "name": "symfony/intl", @@ -4333,16 +4334,16 @@ }, { "name": "symfony/messenger", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "4be925bf0155d6435d2cdfa63d5ffd277c44ac10" + "reference": "6ba5f08c156cfc95911dbb0da01a9bc390a70fd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/4be925bf0155d6435d2cdfa63d5ffd277c44ac10", - "reference": "4be925bf0155d6435d2cdfa63d5ffd277c44ac10", + "url": "https://api.github.com/repos/symfony/messenger/zipball/6ba5f08c156cfc95911dbb0da01a9bc390a70fd1", + "reference": "6ba5f08c156cfc95911dbb0da01a9bc390a70fd1", "shasum": "" }, "require": { @@ -4399,7 +4400,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v8.0.6" + "source": "https://github.com/symfony/messenger/tree/v8.0.7" }, "funding": [ { @@ -4419,20 +4420,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-04T13:55:34+00:00" }, { "name": "symfony/mime", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "632aef4f15ead4d48c16395e447f2da12543d201" + "reference": "5d26d1958aeeba2ace8cc64a3a93d4f5d8f8022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/632aef4f15ead4d48c16395e447f2da12543d201", - "reference": "632aef4f15ead4d48c16395e447f2da12543d201", + "url": "https://api.github.com/repos/symfony/mime/zipball/5d26d1958aeeba2ace8cc64a3a93d4f5d8f8022b", + "reference": "5d26d1958aeeba2ace8cc64a3a93d4f5d8f8022b", "shasum": "" }, "require": { @@ -4485,7 +4486,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v8.0.6" + "source": "https://github.com/symfony/mime/tree/v8.0.7" }, "funding": [ { @@ -4505,7 +4506,7 @@ "type": "tidelift" } ], - "time": "2026-02-05T16:06:41+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/monolog-bridge", @@ -5480,22 +5481,22 @@ }, { "name": "symfony/property-info", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "97524d06a66ae87c59bf9f137420e843cbe4bea0" + "reference": "e1a6b5d10ee3455ae698c4a3f4ef580b78af27ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/97524d06a66ae87c59bf9f137420e843cbe4bea0", - "reference": "97524d06a66ae87c59bf9f137420e843cbe4bea0", + "url": "https://api.github.com/repos/symfony/property-info/zipball/e1a6b5d10ee3455ae698c4a3f4ef580b78af27ba", + "reference": "e1a6b5d10ee3455ae698c4a3f4ef580b78af27ba", "shasum": "" }, "require": { "php": ">=8.4", "symfony/string": "^7.4|^8.0", - "symfony/type-info": "^7.4.4|^8.0.4" + "symfony/type-info": "^7.4.7|^8.0.7" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2|>=7", @@ -5542,7 +5543,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v8.0.6" + "source": "https://github.com/symfony/property-info/tree/v8.0.7" }, "funding": [ { @@ -5562,7 +5563,7 @@ "type": "tidelift" } ], - "time": "2026-02-13T12:14:15+00:00" + "time": "2026-03-04T15:54:04+00:00" }, { "name": "symfony/routing", @@ -6069,16 +6070,16 @@ }, { "name": "symfony/serializer", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "b923bbb92f84213a927db6ad43576366b7b9ec2a" + "reference": "18bbaf7317e33e7e4bcd7ef281357ec4335fc900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/b923bbb92f84213a927db6ad43576366b7b9ec2a", - "reference": "b923bbb92f84213a927db6ad43576366b7b9ec2a", + "url": "https://api.github.com/repos/symfony/serializer/zipball/18bbaf7317e33e7e4bcd7ef281357ec4335fc900", + "reference": "18bbaf7317e33e7e4bcd7ef281357ec4335fc900", "shasum": "" }, "require": { @@ -6142,7 +6143,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v8.0.6" + "source": "https://github.com/symfony/serializer/tree/v8.0.7" }, "funding": [ { @@ -6162,7 +6163,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/service-contracts", @@ -6584,16 +6585,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "a29b174218f6eb324bf24f60440ac81d17f6ee0d" + "reference": "e0539400f53d8305945c06eba7e8df007402f5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/a29b174218f6eb324bf24f60440ac81d17f6ee0d", - "reference": "a29b174218f6eb324bf24f60440ac81d17f6ee0d", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/e0539400f53d8305945c06eba7e8df007402f5e2", + "reference": "e0539400f53d8305945c06eba7e8df007402f5e2", "shasum": "" }, "require": { @@ -6667,7 +6668,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v8.0.6" + "source": "https://github.com/symfony/twig-bridge/tree/v8.0.7" }, "funding": [ { @@ -6687,7 +6688,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-04T15:37:12+00:00" }, { "name": "symfony/twig-bundle", @@ -6775,16 +6776,16 @@ }, { "name": "symfony/type-info", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "785992c06d07306f963ded3439036f5da9b292fe" + "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/785992c06d07306f963ded3439036f5da9b292fe", - "reference": "785992c06d07306f963ded3439036f5da9b292fe", + "url": "https://api.github.com/repos/symfony/type-info/zipball/3c7de103dd6cb68be24e155838a64ef4a70ae195", + "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195", "shasum": "" }, "require": { @@ -6833,7 +6834,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v8.0.6" + "source": "https://github.com/symfony/type-info/tree/v8.0.7" }, "funding": [ { @@ -6853,7 +6854,7 @@ "type": "tidelift" } ], - "time": "2026-02-20T07:51:53+00:00" + "time": "2026-03-04T13:55:34+00:00" }, { "name": "symfony/uid", @@ -6935,16 +6936,16 @@ }, { "name": "symfony/ux-twig-component", - "version": "v2.32.0", + "version": "v2.33.0", "source": { "type": "git", "url": "https://github.com/symfony/ux-twig-component.git", - "reference": "0a300088327d1b766733fdcd81ae4a77852d6177" + "reference": "f9942e32246fe3fa9d31f60cffc1ada4d274830a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/0a300088327d1b766733fdcd81ae4a77852d6177", - "reference": "0a300088327d1b766733fdcd81ae4a77852d6177", + "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/f9942e32246fe3fa9d31f60cffc1ada4d274830a", + "reference": "f9942e32246fe3fa9d31f60cffc1ada4d274830a", "shasum": "" }, "require": { @@ -6998,7 +6999,7 @@ "twig" ], "support": { - "source": "https://github.com/symfony/ux-twig-component/tree/v2.32.0" + "source": "https://github.com/symfony/ux-twig-component/tree/v2.33.0" }, "funding": [ { @@ -7018,20 +7019,20 @@ "type": "tidelift" } ], - "time": "2025-12-25T09:25:01+00:00" + "time": "2026-03-15T18:48:53+00:00" }, { "name": "symfony/validator", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "64bcfc222dd26443c6c68d442a1e65397c440c78" + "reference": "04f7111e6f246d8211081fdc76e34b1298a9fc27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/64bcfc222dd26443c6c68d442a1e65397c440c78", - "reference": "64bcfc222dd26443c6c68d442a1e65397c440c78", + "url": "https://api.github.com/repos/symfony/validator/zipball/04f7111e6f246d8211081fdc76e34b1298a9fc27", + "reference": "04f7111e6f246d8211081fdc76e34b1298a9fc27", "shasum": "" }, "require": { @@ -7042,7 +7043,8 @@ }, "conflict": { "doctrine/lexer": "<1.1", - "symfony/doctrine-bridge": "<7.4" + "symfony/doctrine-bridge": "<7.4", + "symfony/expression-language": "<7.4" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", @@ -7092,7 +7094,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v8.0.6" + "source": "https://github.com/symfony/validator/tree/v8.0.7" }, "funding": [ { @@ -7112,7 +7114,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-06T13:17:40+00:00" }, { "name": "symfony/var-dumper", @@ -7501,16 +7503,16 @@ }, { "name": "twig/extra-bundle", - "version": "v3.23.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "7a27e784dc56eddfef5e9295829b290ce06f1682" + "reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/7a27e784dc56eddfef5e9295829b290ce06f1682", - "reference": "7a27e784dc56eddfef5e9295829b290ce06f1682", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9", + "reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9", "shasum": "" }, "require": { @@ -7559,7 +7561,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.23.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.24.0" }, "funding": [ { @@ -7571,20 +7573,20 @@ "type": "tidelift" } ], - "time": "2025-12-18T20:46:15+00:00" + "time": "2026-02-07T08:07:38+00:00" }, { "name": "twig/html-extra", - "version": "v3.23.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/html-extra.git", - "reference": "2ef1d0ccaa06d4f4405b330fe6c4b6f7b50fbbc3" + "reference": "313900fb98b371b006a55b1a29241a192634be13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/html-extra/zipball/2ef1d0ccaa06d4f4405b330fe6c4b6f7b50fbbc3", - "reference": "2ef1d0ccaa06d4f4405b330fe6c4b6f7b50fbbc3", + "url": "https://api.github.com/repos/twigphp/html-extra/zipball/313900fb98b371b006a55b1a29241a192634be13", + "reference": "313900fb98b371b006a55b1a29241a192634be13", "shasum": "" }, "require": { @@ -7627,7 +7629,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/html-extra/tree/v3.23.0" + "source": "https://github.com/twigphp/html-extra/tree/v3.24.0" }, "funding": [ { @@ -7639,20 +7641,20 @@ "type": "tidelift" } ], - "time": "2025-12-02T14:45:16+00:00" + "time": "2026-03-17T07:24:08+00:00" }, { "name": "twig/twig", - "version": "v3.23.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" + "reference": "a6769aefb305efef849dc25c9fd1653358c148f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", - "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6769aefb305efef849dc25c9fd1653358c148f0", + "reference": "a6769aefb305efef849dc25c9fd1653358c148f0", "shasum": "" }, "require": { @@ -7662,7 +7664,8 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "phpstan/phpstan": "^2.0", + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -7706,7 +7709,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.23.0" + "source": "https://github.com/twigphp/Twig/tree/v3.24.0" }, "funding": [ { @@ -7718,7 +7721,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T21:00:41+00:00" + "time": "2026-03-17T21:31:11+00:00" }, { "name": "webmozart/assert", @@ -8702,11 +8705,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.40", + "version": "2.1.42", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", - "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", "shasum": "" }, "require": { @@ -8751,7 +8754,7 @@ "type": "github" } ], - "time": "2026-02-23T15:04:35+00:00" + "time": "2026-03-17T14:58:32+00:00" }, { "name": "phpunit/php-code-coverage", @@ -10079,16 +10082,16 @@ }, { "name": "sebastian/environment", - "version": "9.0.0", + "version": "9.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "bb64d08145b021b67d5f253308a498b73ab0461e" + "reference": "e26e9a944bd9d27b3a38a82fc2093d440951bfbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/bb64d08145b021b67d5f253308a498b73ab0461e", - "reference": "bb64d08145b021b67d5f253308a498b73ab0461e", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/e26e9a944bd9d27b3a38a82fc2093d440951bfbe", + "reference": "e26e9a944bd9d27b3a38a82fc2093d440951bfbe", "shasum": "" }, "require": { @@ -10131,7 +10134,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/9.0.0" + "source": "https://github.com/sebastianbergmann/environment/tree/9.0.1" }, "funding": [ { @@ -10151,7 +10154,7 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:43:29+00:00" + "time": "2026-03-15T07:13:02+00:00" }, { "name": "sebastian/exporter", @@ -10790,19 +10793,20 @@ }, { "name": "symfony/maker-bundle", - "version": "v1.66.0", + "version": "v1.67.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "b5b4afa2a570b926682e9f34615a6766dd560ff4" + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/b5b4afa2a570b926682e9f34615a6766dd560ff4", - "reference": "b5b4afa2a570b926682e9f34615a6766dd560ff4", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/6ce8b313845f16bcf385ee3cb31d8b24e30d5516", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516", "shasum": "" }, "require": { + "composer-runtime-api": "^2.1", "doctrine/inflector": "^2.0", "nikic/php-parser": "^5.0", "php": ">=8.1", @@ -10864,7 +10868,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.66.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.67.0" }, "funding": [ { @@ -10884,7 +10888,7 @@ "type": "tidelift" } ], - "time": "2026-02-09T08:55:54+00:00" + "time": "2026-03-18T13:39:06+00:00" }, { "name": "symfony/process", @@ -10953,16 +10957,16 @@ }, { "name": "symfony/web-profiler-bundle", - "version": "v8.0.6", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "e9a49910bacf2c945975b43ac5c0e901a9b6fe4f" + "reference": "141336fd018b9ac77ba4910c04c2ca05c35e7ad2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/e9a49910bacf2c945975b43ac5c0e901a9b6fe4f", - "reference": "e9a49910bacf2c945975b43ac5c0e901a9b6fe4f", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/141336fd018b9ac77ba4910c04c2ca05c35e7ad2", + "reference": "141336fd018b9ac77ba4910c04c2ca05c35e7ad2", "shasum": "" }, "require": { @@ -11014,7 +11018,7 @@ "dev" ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v8.0.6" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v8.0.7" }, "funding": [ { @@ -11034,7 +11038,7 @@ "type": "tidelift" } ], - "time": "2026-02-13T09:57:13+00:00" + "time": "2026-03-04T08:20:53+00:00" }, { "name": "theseer/tokenizer", diff --git a/backend/config/packages/monolog.yaml b/backend/config/packages/monolog.yaml index c631472..97178bc 100644 --- a/backend/config/packages/monolog.yaml +++ b/backend/config/packages/monolog.yaml @@ -4,4 +4,4 @@ monolog: type: stream path: "php://stdout" level: info - formatter: monolog.formatter.json + formatter: App\Instrumentation\PsrLog\CustomLineFormatter \ No newline at end of file diff --git a/backend/migrations/Version20260320090903.php b/backend/migrations/Version20260320090903.php new file mode 100644 index 0000000..40637bf --- /dev/null +++ b/backend/migrations/Version20260320090903.php @@ -0,0 +1,26 @@ +addSql('CREATE TABLE report (id INT AUTO_INCREMENT NOT NULL, uuid CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', status VARCHAR(50) NOT NULL, compensation_id LONGTEXT NOT NULL, checksum VARCHAR(64) NOT NULL, file_path VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', completed_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', error_message LONGTEXT DEFAULT NULL, UNIQUE INDEX UNIQ_D862F276D17F50A6 (uuid), INDEX idx_status (status), INDEX idx_created_at (created_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE report'); + } +} diff --git a/backend/phpstan-baseline.neon b/backend/phpstan-baseline.neon index 7a8dac7..c77b2c1 100644 --- a/backend/phpstan-baseline.neon +++ b/backend/phpstan-baseline.neon @@ -6,6 +6,12 @@ parameters: count: 1 path: public/index.php + - + message: '#^Property App\\Entity\\Report\:\:\$id \(int\|null\) is never assigned int so it can be removed from the property type\.$#' + identifier: property.unusedType + count: 1 + path: src/Entity/Report.php + - message: '#^Property App\\Entity\\User\:\:\$id \(int\|null\) is never assigned int so it can be removed from the property type\.$#' identifier: property.unusedType diff --git a/backend/src/Async/GenerateReportHandler.php b/backend/src/Async/GenerateReportHandler.php new file mode 100644 index 0000000..085311c --- /dev/null +++ b/backend/src/Async/GenerateReportHandler.php @@ -0,0 +1,113 @@ +entityManager->clear(); + + $report = $this->reportRepository->find($message->reportId); + if (!$report instanceof Report) { + throw new \RuntimeException(sprintf('Report with ID %d not found', $message->reportId)); + } + + $report->setStatus(Report::STATUS_GENERATING); + $this->entityManager->persist($report); + $this->entityManager->flush(); + + $expenses = $this->calculator->calculate(); + + if (2 !== count($expenses)) { + throw new \RuntimeException('Expected exactly 2 users in calculation'); + } + + $compensation = Compensation::calculate($expenses[0], $expenses[1]); + + $html = $this->twig->render('report/calculation.html.twig', [ + 'expenses' => $expenses, + 'compensation' => $compensation, + ]); + + $dompdf = new Dompdf($this->createDompdfOptions()); + $dompdf->loadHtml($html); + $dompdf->setPaper('A4', 'portrait'); + $dompdf->render(); + + $pdfContent = $dompdf->output(); + $fileName = sprintf('report-%s.pdf', $report->getUuid()->toRfc4122()); + $filePath = $this->reportsDir.DIRECTORY_SEPARATOR.$fileName; + + Ensure::that(false !== file_put_contents($filePath, $pdfContent)); + + $report->setFilePath($filePath); + $report->setStatus(Report::STATUS_COMPLETED); + $report->setCompletedAt(new \DateTimeImmutable()); + $this->entityManager->flush(); + + $this->instrumentation->getLogging()->info(sprintf( + 'Report generated successfully: %s (%.0fms)', + $fileName, + $timer->getMillisecondsElapsed() + )); + } catch (\Exception $e) { + $this->instrumentation->getLogging()->info(sprintf( + 'Failed to generate report: %s', + $e->getMessage() + )); + + if ($report instanceof Report) { + $report->setStatus(Report::STATUS_FAILED); + $report->setErrorMessage($e->getMessage()); + $report->setCompletedAt(new \DateTimeImmutable()); + $this->entityManager->flush(); + } + + throw $e; + } + } + + private function createDompdfOptions(): Options + { + $options = new Options(); + $options->set('isRemoteEnabled', true); + $options->set('isHtml5ParserEnabled', true); + $options->set('defaultMediaType', 'print'); + $options->set('isFontSubsettingEnabled', true); + $options->set('isPhpEnabled', false); + + return $options; + } +} diff --git a/backend/src/Async/GenerateReportMessage.php b/backend/src/Async/GenerateReportMessage.php new file mode 100644 index 0000000..fa09f02 --- /dev/null +++ b/backend/src/Async/GenerateReportMessage.php @@ -0,0 +1,17 @@ +addOption( + 'days', + 'd', + InputOption::VALUE_OPTIONAL, + 'Remove reports older than N days', + 7 + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $daysOption = $input->getOption('days'); + assert(is_numeric($daysOption), 'Days option is not a number!'); + $days = (int) $daysOption; + $cutoffDate = new \DateTimeImmutable("-{$days} days"); + + $oldReports = $this->reportRepository->findOlderThan($cutoffDate); + + if (empty($oldReports)) { + $output->writeln('No old reports found.'); + + return Command::SUCCESS; + } + + $deletedCount = 0; + foreach ($oldReports as $report) { + $filePath = $report->getFilePath(); + if (null !== $filePath && file_exists($filePath)) { + unlink($filePath); + } + + $this->entityManager->remove($report); + ++$deletedCount; + } + + $this->entityManager->flush(); + + $output->writeln(sprintf('Deleted %d report(s).', $deletedCount)); + + return Command::SUCCESS; + } +} diff --git a/backend/src/Controller/API/ReportController.php b/backend/src/Controller/API/ReportController.php index 03880fa..e5afd47 100644 --- a/backend/src/Controller/API/ReportController.php +++ b/backend/src/Controller/API/ReportController.php @@ -2,67 +2,130 @@ namespace App\Controller\API; -use App\Instrumentation\InstrumentationHolder; -use App\Invariant\Ensure; -use App\SplitFairly\Calculator; -use App\SplitFairly\Compensation; -use Dompdf\Dompdf; -use Dompdf\Options; +use App\Async\GenerateReportMessage; +use App\Entity\Report; +use App\Repository\ReportRepository; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Uid\Uuid; #[Route('/api', name: 'api.')] final class ReportController extends AbstractController { public function __construct( - private readonly Calculator $calculator, + private readonly EntityManagerInterface $entityManager, + private readonly ReportRepository $reportRepository, + private readonly MessageBusInterface $messageBus, ) { } - #[Route('/report/calculation', name: 'report.calculation', methods: ['GET'])] - public function calculationReport(): Response + #[Route('/report/calculation', name: 'report.calculation', methods: ['POST'])] + public function initiate(): JsonResponse { - $expenses = $this->calculator->calculate(); + $compensationId = (new \DateTimeImmutable())->format('Y-m-d'); + $checksum = hash('sha256', $compensationId); - Ensure::that(2 === count($expenses)); + $existingReport = $this->reportRepository->findByCompensationIdAndChecksum( + $compensationId, + $checksum + ); + + if ($existingReport) { + return new JsonResponse([ + 'id' => $existingReport->getUuid()->toRfc4122(), + 'status' => $existingReport->getStatus(), + 'filePath' => $existingReport->getFilePath(), + ]); + } + + $report = new Report($compensationId, $checksum); + $this->entityManager->persist($report); + $this->entityManager->flush(); - $compensation = Compensation::calculate($expenses[0], $expenses[1]); + $reportId = $report->getId(); + if (null === $reportId) { + throw new \RuntimeException('Report ID should not be null after flush'); + } - $html = $this->renderView('report/calculation.html.twig', [ - 'expenses' => $expenses, - 'compensation' => $compensation, - ]); + $this->messageBus->dispatch(new GenerateReportMessage( + $reportId, + $compensationId + )); - $dompdf = new Dompdf($this->createDompdfOptions()); - $dompdf->loadHtml($html); - $dompdf->setPaper('A4', 'portrait'); - $dompdf->render(); + return new JsonResponse([ + 'id' => $report->getUuid()->toRfc4122(), + 'status' => $report->getStatus(), + ], Response::HTTP_ACCEPTED); + } + + #[Route('/report/{id}/status', name: 'report.status', methods: ['GET'])] + public function getStatus(string $id): JsonResponse + { + try { + $uuid = Uuid::fromString($id); + } catch (\Exception) { + return new JsonResponse(['error' => 'Invalid report ID'], Response::HTTP_BAD_REQUEST); + } - $pdfContent = $dompdf->output(); - $fileName = sprintf('split-fairly-report-%s.pdf', (new \DateTimeImmutable())->format('Y-m-d')); + $report = $this->reportRepository->findOneBy(['uuid' => $uuid]); - InstrumentationHolder::getLogging()->info(sprintf('Report generated: %s', $fileName)); + if (!$report) { + return new JsonResponse(['error' => 'Report not found'], Response::HTTP_NOT_FOUND); + } + + $response = [ + 'id' => $report->getUuid()->toRfc4122(), + 'status' => $report->getStatus(), + 'createdAt' => $report->getCreatedAt()->format(\DateTimeInterface::ATOM), + ]; + + if ($report->isCompleted() && $report->getFilePath()) { + $response['downloadUrl'] = sprintf('/api/report/%s/download', $report->getUuid()->toRfc4122()); + } + + if ($report->isFailed()) { + $response['error'] = $report->getErrorMessage(); + } + + return new JsonResponse($response); + } + + #[Route('/report/{id}/download', name: 'report.download', methods: ['GET'])] + public function download(string $id): Response + { + try { + $uuid = Uuid::fromString($id); + } catch (\Exception) { + return new Response('Invalid report ID', Response::HTTP_BAD_REQUEST); + } + + $report = $this->reportRepository->findOneBy(['uuid' => $uuid]); + + if (!$report || !$report->isCompleted() || !$report->getFilePath()) { + return new Response('Report not found or not ready', Response::HTTP_NOT_FOUND); + } + + if (!file_exists($report->getFilePath())) { + return new Response('Report file not found', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $pdfContent = file_get_contents($report->getFilePath()); + + if (false === $pdfContent) { + return new Response('Failed to read report file', Response::HTTP_INTERNAL_SERVER_ERROR); + } return new Response( $pdfContent, Response::HTTP_OK, [ 'Content-Type' => 'application/pdf', - 'Content-Disposition' => sprintf('attachment; filename="%s"', $fileName), + 'Content-Disposition' => 'attachment; filename="split-fairly-calculation.pdf"', ] ); } - - private function createDompdfOptions(): Options - { - $options = new Options(); - $options->set('isRemoteEnabled', true); - $options->set('isHtml5ParserEnabled', true); - $options->set('defaultMediaType', 'print'); - $options->set('isFontSubsettingEnabled', true); - $options->set('isPhpEnabled', false); - - return $options; - } } diff --git a/backend/src/Controller/API/TrackExpenseController.php b/backend/src/Controller/API/TrackExpenseController.php index 69550d0..f61b170 100644 --- a/backend/src/Controller/API/TrackExpenseController.php +++ b/backend/src/Controller/API/TrackExpenseController.php @@ -31,8 +31,8 @@ public function track(#[MapRequestPayload] Expense $expense): JsonResponse ], Response::HTTP_UNAUTHORIZED); } - // Validate Lend expenses: location must be recipient's email and must differ from current user - if ('Lend' === $expense->type) { + // Validate Lent expenses: location must be recipient's email and must differ from current user + if ('Lent' === $expense->type) { $currentUserEmail = $currentUser->getUserIdentifier(); $recipientEmail = $expense->location; diff --git a/backend/src/Entity/Report.php b/backend/src/Entity/Report.php new file mode 100644 index 0000000..f0d2bdf --- /dev/null +++ b/backend/src/Entity/Report.php @@ -0,0 +1,144 @@ +uuid = new UuidV7(); + $this->compensationId = $compensationId; + $this->checksum = $checksum; + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUuid(): Uuid + { + return $this->uuid; + } + + public function getStatus(): string + { + return $this->status; + } + + public function setStatus(string $status): self + { + $this->status = $status; + + return $this; + } + + public function getCompensationId(): string + { + return $this->compensationId; + } + + public function getChecksum(): string + { + return $this->checksum; + } + + public function getFilePath(): ?string + { + return $this->filePath; + } + + public function setFilePath(string $filePath): self + { + $this->filePath = $filePath; + + return $this; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function getCompletedAt(): ?\DateTimeImmutable + { + return $this->completedAt; + } + + public function setCompletedAt(\DateTimeImmutable $completedAt): self + { + $this->completedAt = $completedAt; + + return $this; + } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } + + public function setErrorMessage(string $errorMessage): self + { + $this->errorMessage = $errorMessage; + + return $this; + } + + public function isCompleted(): bool + { + return self::STATUS_COMPLETED === $this->status; + } + + public function isFailed(): bool + { + return self::STATUS_FAILED === $this->status; + } + + public function isPending(): bool + { + return self::STATUS_PENDING === $this->status || self::STATUS_GENERATING === $this->status; + } +} diff --git a/backend/src/Instrumentation/PsrLog/CustomLineFormatter.php b/backend/src/Instrumentation/PsrLog/CustomLineFormatter.php new file mode 100644 index 0000000..500d382 --- /dev/null +++ b/backend/src/Instrumentation/PsrLog/CustomLineFormatter.php @@ -0,0 +1,17 @@ + + */ +class ReportRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Report::class); + } + + /** + * @return Report[] + */ + public function findOlderThan(\DateTimeInterface $date): array + { + /** @var Report[] $result */ + $result = $this->createQueryBuilder('r') + ->andWhere('r.createdAt < :date') + ->setParameter('date', $date) + ->getQuery() + ->getResult(); + + return $result; + } + + public function findByCompensationIdAndChecksum(string $compensationId, string $checksum): ?Report + { + $result = $this->createQueryBuilder('r') + ->andWhere('r.compensationId = :compensationId') + ->andWhere('r.checksum = :checksum') + ->andWhere('r.status = :status') + ->setParameter('compensationId', $compensationId) + ->setParameter('checksum', $checksum) + ->setParameter('status', Report::STATUS_COMPLETED) + ->orderBy('r.createdAt', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + + return $result instanceof Report ? $result : null; + } +} diff --git a/backend/src/SplitFairly/Expense.php b/backend/src/SplitFairly/Expense.php index a8d6301..0ebdf6f 100644 --- a/backend/src/SplitFairly/Expense.php +++ b/backend/src/SplitFairly/Expense.php @@ -18,6 +18,10 @@ public function __construct( Ensure::that(!empty($what)); Ensure::that(!empty($type)); Ensure::that(!empty($location)); + Ensure::that( + in_array($type, array_map(static fn (ExpenseType $e) => $e->value, ExpenseType::cases()), strict: true), + sprintf('Invalid expense type: %s', $type) + ); } public function getId(): Uuid diff --git a/backend/src/SplitFairly/ExpenseType.php b/backend/src/SplitFairly/ExpenseType.php new file mode 100644 index 0000000..823a32a --- /dev/null +++ b/backend/src/SplitFairly/ExpenseType.php @@ -0,0 +1,12 @@ + {% endblock %} {% block body %} +
|
- From -{{ compensation.from }} - |
-
- Pays -{{ compensation.settlement.value|number_format(2) }} {{ compensation.settlement.currency }} - |
-
- To -{{ compensation.to }} - |
-
{{ expenses.userEmail }}
- - {% set expensesByType = {} %} - {% for expense in expenses.expenses %} - {% if expensesByType[expense.type] is not defined %} - {% set expensesByType = expensesByType|merge({(expense.type): []}) %} - {% endif %} - {% set expensesByType = expensesByType|merge({(expense.type): expensesByType[expense.type]|merge([expense])}) %} +