Compare commits
	
		
			No commits in common. "master" and "feature_form" have entirely different histories.
		
	
	
		
			master
			...
			feature_fo
		
	
		
							
								
								
									
										4
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								.env
									
									
									
									
									
								
							@ -18,7 +18,3 @@
 | 
			
		||||
APP_ENV=dev
 | 
			
		||||
APP_SECRET=f27f6cb11fb594fcb5ab4591ec0aac77
 | 
			
		||||
###< symfony/framework-bundle ###
 | 
			
		||||
 | 
			
		||||
###> sentry/sentry-symfony ###
 | 
			
		||||
SENTRY_DSN=
 | 
			
		||||
###< sentry/sentry-symfony ###
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								README.md
									
									
									
									
									
								
							@ -1,34 +1,3 @@
 | 
			
		||||
symfony_example_app
 | 
			
		||||
===================
 | 
			
		||||
 | 
			
		||||
Generátor QR kódů pro platby z https://topapi.top-test.cz
 | 
			
		||||
 | 
			
		||||
Quick setup
 | 
			
		||||
-----------
 | 
			
		||||
```bash
 | 
			
		||||
# git clone https://git.nanobyte.cz/nanobyte-public/symfony_example_app.git
 | 
			
		||||
# cd symfony_example_app
 | 
			
		||||
# docker compose run --build php-fpm composer install
 | 
			
		||||
# docker compose up --build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
- aplikace je dostupná na http://localhost:8000/
 | 
			
		||||
- na adrese http://localhost:3000/explore je běžící grafana (s loki a tempo)
 | 
			
		||||
- aplikace je nastavená (i na lokálu, běžně bych to nastavil až na devech/stage/PROD) aby posílala logy do lokiho a tracing do tempa pomocí otel protokolu
 | 
			
		||||
 | 
			
		||||
TODO:
 | 
			
		||||
-----
 | 
			
		||||
- [ ] Chybí speciální slovník nebo vypnutí slovníku pro testy
 | 
			
		||||
- [ ] V reálný aplikaci bych použil Mockery, nicméně tady mě to přijde zbytečný
 | 
			
		||||
- [ ] Nastavení cache ideálně v memcached/redis etc.
 | 
			
		||||
- [ ] Vyhezkat OTEL logs, OTEL tracing
 | 
			
		||||
- [ ] CI pipelines
 | 
			
		||||
- [ ] k8s deployment
 | 
			
		||||
- [ ] prometheus country na počet requestů/api (počet 200OK/500ERR)
 | 
			
		||||
- [ ] Je dost na zvážení zda nezobrazit výsledek remote validace (response_create_400.json) a nenechat uživatele špatné hodnoty opravit. Ovšem znamená to že lokální validátory jsou špatně, chybu by bylo vhodné zalogovat do Sentry (do logu etc.) a opravit ji...
 | 
			
		||||
- [ ] Na produkci bych statický content rozhodně netlačil přes app container ale přes static nginx container (asset-map:compile -> copy do nginx static containeru)
 | 
			
		||||
- [ ] xdebug v dockeru
 | 
			
		||||
 | 
			
		||||
Poznámky
 | 
			
		||||
--------
 | 
			
		||||
- Nejsem kodér (a javascript developer), nevypadá to nijak extra ;-)
 | 
			
		||||
 | 
			
		||||
@ -10,27 +10,22 @@
 | 
			
		||||
        "ext-iconv": "*",
 | 
			
		||||
        "grpc/grpc": "^1.57",
 | 
			
		||||
        "guzzlehttp/promises": "*",
 | 
			
		||||
        "nubium/this-should-never-happen-exception": "^1.0",
 | 
			
		||||
        "nyholm/psr7": "*",
 | 
			
		||||
        "open-telemetry/exporter-otlp": "^1.0",
 | 
			
		||||
        "open-telemetry/opentelemetry-auto-symfony": "^1.0@beta",
 | 
			
		||||
        "open-telemetry/opentelemetry-logger-monolog": "^1.0",
 | 
			
		||||
        "open-telemetry/opentelemetry-propagation-server-timing": "^0.0.1",
 | 
			
		||||
        "open-telemetry/opentelemetry-propagation-traceresponse": "^0.0.2",
 | 
			
		||||
        "open-telemetry/sdk": "^1.0",
 | 
			
		||||
        "open-telemetry/transport-grpc": "^1.0",
 | 
			
		||||
        "php-http/httplug": "*",
 | 
			
		||||
        "sentry/sentry-symfony": "^4.13",
 | 
			
		||||
        "symfony/asset": "7.0.*",
 | 
			
		||||
        "symfony/asset-mapper": "7.0.*",
 | 
			
		||||
        "symfony/cache": "7.0.*",
 | 
			
		||||
        "symfony/console": "7.0.*",
 | 
			
		||||
        "symfony/dotenv": "7.0.*",
 | 
			
		||||
        "symfony/flex": "^2",
 | 
			
		||||
        "symfony/form": "7.0.*",
 | 
			
		||||
        "symfony/framework-bundle": "7.0.*",
 | 
			
		||||
        "symfony/http-client": "*",
 | 
			
		||||
        "symfony/monolog-bundle": "^3.10",
 | 
			
		||||
        "symfony/options-resolver": "7.0.*",
 | 
			
		||||
        "symfony/runtime": "7.0.*",
 | 
			
		||||
        "symfony/translation": "7.0.*",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1493
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1493
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -5,6 +5,4 @@ return [
 | 
			
		||||
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
 | 
			
		||||
    Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
 | 
			
		||||
    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
 | 
			
		||||
    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
 | 
			
		||||
    Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
framework:
 | 
			
		||||
    cache:
 | 
			
		||||
        # Unique name of your app: used to compute stable namespaces for cache keys.
 | 
			
		||||
        prefix_seed: ovlach/symfony_example_app
 | 
			
		||||
        #prefix_seed: your_vendor_name/app_name
 | 
			
		||||
 | 
			
		||||
        # The "app" cache stores to the filesystem by default.
 | 
			
		||||
        # The data in this cache should persist between deploys.
 | 
			
		||||
 | 
			
		||||
@ -1,68 +0,0 @@
 | 
			
		||||
monolog:
 | 
			
		||||
    channels:
 | 
			
		||||
        - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
 | 
			
		||||
 | 
			
		||||
when@dev:
 | 
			
		||||
    monolog:
 | 
			
		||||
        handlers:
 | 
			
		||||
            otel: # Tohle bych v realny aplikaci nepouzil na devu
 | 
			
		||||
                type: service
 | 
			
		||||
                id: App\Bridge\Monolog\Handler\SymfonyOtelHandler
 | 
			
		||||
 | 
			
		||||
            main:
 | 
			
		||||
                type: stream
 | 
			
		||||
                path: "%kernel.logs_dir%/%kernel.environment%.log"
 | 
			
		||||
                level: debug
 | 
			
		||||
                channels: ["!event"]
 | 
			
		||||
            # uncomment to get logging in your browser
 | 
			
		||||
            # you may have to allow bigger header sizes in your Web server configuration
 | 
			
		||||
            #firephp:
 | 
			
		||||
            #    type: firephp
 | 
			
		||||
            #    level: info
 | 
			
		||||
            #chromephp:
 | 
			
		||||
            #    type: chromephp
 | 
			
		||||
            #    level: info
 | 
			
		||||
            console:
 | 
			
		||||
                type: console
 | 
			
		||||
                process_psr_3_messages: false
 | 
			
		||||
                channels: ["!event", "!doctrine", "!console"]
 | 
			
		||||
 | 
			
		||||
when@test:
 | 
			
		||||
    monolog:
 | 
			
		||||
        handlers:
 | 
			
		||||
            main:
 | 
			
		||||
                type: fingers_crossed
 | 
			
		||||
                action_level: error
 | 
			
		||||
                handler: nested
 | 
			
		||||
                excluded_http_codes: [404, 405]
 | 
			
		||||
                channels: ["!event"]
 | 
			
		||||
            nested:
 | 
			
		||||
                type: stream
 | 
			
		||||
                path: "%kernel.logs_dir%/%kernel.environment%.log"
 | 
			
		||||
                level: debug
 | 
			
		||||
 | 
			
		||||
when@prod:
 | 
			
		||||
    monolog:
 | 
			
		||||
        handlers:
 | 
			
		||||
            otel:
 | 
			
		||||
                type: service
 | 
			
		||||
                id: App\Bridge\Monolog\Handler\SymfonyOtelHandler
 | 
			
		||||
            main:
 | 
			
		||||
                type: fingers_crossed
 | 
			
		||||
                action_level: error
 | 
			
		||||
                handler: nested
 | 
			
		||||
                excluded_http_codes: [404, 405]
 | 
			
		||||
                buffer_size: 50 # How many messages should be saved? Prevent memory leaks
 | 
			
		||||
            nested:
 | 
			
		||||
                type: stream
 | 
			
		||||
                path: php://stderr
 | 
			
		||||
                level: debug
 | 
			
		||||
                formatter: monolog.formatter.json
 | 
			
		||||
            console:
 | 
			
		||||
                type: console
 | 
			
		||||
                process_psr_3_messages: false
 | 
			
		||||
                channels: ["!event", "!doctrine"]
 | 
			
		||||
            deprecation:
 | 
			
		||||
                type: stream
 | 
			
		||||
                channels: [deprecation]
 | 
			
		||||
                path: php://stderr
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
when@prod:
 | 
			
		||||
    sentry:
 | 
			
		||||
        dsn: '%env(SENTRY_DSN)%'
 | 
			
		||||
        # this hooks into critical paths of the framework (and vendors) to perform
 | 
			
		||||
        # automatic instrumentation (there might be some performance penalty)
 | 
			
		||||
        # https://docs.sentry.io/platforms/php/guides/symfony/performance/instrumentation/automatic-instrumentation/
 | 
			
		||||
        tracing:
 | 
			
		||||
            enabled: false
 | 
			
		||||
 | 
			
		||||
#        If you are using Monolog, you also need this additional configuration to log the errors correctly:
 | 
			
		||||
#        https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration
 | 
			
		||||
#        register_error_listener: false
 | 
			
		||||
#        register_error_handler: false
 | 
			
		||||
 | 
			
		||||
#    monolog:
 | 
			
		||||
#        handlers:
 | 
			
		||||
#            sentry:
 | 
			
		||||
#                type: sentry
 | 
			
		||||
#                level: !php/const Monolog\Logger::ERROR
 | 
			
		||||
#                hub_id: Sentry\State\HubInterface
 | 
			
		||||
 | 
			
		||||
#    Uncomment these lines to register a log message processor that resolves PSR-3 placeholders
 | 
			
		||||
#    https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration
 | 
			
		||||
#    services:
 | 
			
		||||
#        Monolog\Processor\PsrLogMessageProcessor:
 | 
			
		||||
#            tags: { name: monolog.processor, handler: sentry }
 | 
			
		||||
@ -5,11 +5,7 @@
 | 
			
		||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
 | 
			
		||||
parameters:
 | 
			
		||||
    env(SYMFONY_EXAMPLE_APP_CURRENCIES): '["CZK", "EUR", "USD", "GBP"]'
 | 
			
		||||
    env(SYMFONY_EXAMPLE_APP_USETRENO_USERNAME): 'developer'
 | 
			
		||||
    env(SYMFONY_EXAMPLE_APP_USETRENO_PASSWORD): 'developer123'
 | 
			
		||||
    app.currencies: '%env(json:SYMFONY_EXAMPLE_APP_CURRENCIES)%'
 | 
			
		||||
    app.usetreno.username: '%env(string:SYMFONY_EXAMPLE_APP_USETRENO_USERNAME)%'
 | 
			
		||||
    app.usetreno.password: '%env(string:SYMFONY_EXAMPLE_APP_USETRENO_PASSWORD)%'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
    # default configuration for services in *this* file
 | 
			
		||||
@ -31,31 +27,13 @@ services:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $available_currencies: '%app.currencies%'
 | 
			
		||||
 | 
			
		||||
    App\Service\StubQRCodeProvider:
 | 
			
		||||
    App\Service\StubQRCodeGenerator:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $imagePath: '%kernel.project_dir%/assets/images/wip.png'
 | 
			
		||||
 | 
			
		||||
    App\Service\Remote\UsetrenoHttpClient:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $username: '%app.usetreno.username%'
 | 
			
		||||
            $password: '%app.usetreno.password%'
 | 
			
		||||
            $retryCount: 2
 | 
			
		||||
            $retryWaitSeconds: 0.5
 | 
			
		||||
 | 
			
		||||
    App\Service\Remote\UsetrenoQRCodeProvider:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $retryCount: 2
 | 
			
		||||
            $retryWaitSeconds: 0.5
 | 
			
		||||
 | 
			
		||||
    App\Service\CachedQRCodeProvider:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $innerGenerator: '@App\Service\Remote\UsetrenoQRCodeProvider'
 | 
			
		||||
            $cacheDuration: 'PT60S'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    App\Service\CurrencyListerInterface: '@App\Service\StaticCurrencyLister'
 | 
			
		||||
    App\Service\QRCodeQROptionsProviderInterface: '@App\Service\QRCodeQROptionsDefaultProvider'
 | 
			
		||||
    App\Service\QRCodeProviderInterface: '@App\Service\CachedQRCodeProvider'
 | 
			
		||||
    App\Service\QRCodeGeneratorInterface: '@App\Service\StubQRCodeGenerator'
 | 
			
		||||
 | 
			
		||||
    # add more service definitions when explicit configuration is needed
 | 
			
		||||
    # please note that last definitions always *replace* previous ones
 | 
			
		||||
 | 
			
		||||
@ -11,40 +11,22 @@ processors:
 | 
			
		||||
    actions:
 | 
			
		||||
      - action: insert
 | 
			
		||||
        key: loki.attribute.labels
 | 
			
		||||
        value: context, code.filepath, code.namespace, code.function, code.lineno, http.request.method, http.request.body.size, url.full, url.scheme, url.path, http.route, http.response.status_code
 | 
			
		||||
        value: log.file, log.line, log.module_path, http.request_id, http.uri
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: context
 | 
			
		||||
        key: context
 | 
			
		||||
        from_attribute: log.file
 | 
			
		||||
        key: log.file
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: code.namespace
 | 
			
		||||
        key: code.namespace
 | 
			
		||||
        from_attribute: log.line
 | 
			
		||||
        key: log.line
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: code.function
 | 
			
		||||
        key: code.function
 | 
			
		||||
        from_attribute: log.module_path
 | 
			
		||||
        key: log.module_path
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: code.lineno
 | 
			
		||||
        key: code.lineno
 | 
			
		||||
        from_attribute: http.request_id
 | 
			
		||||
        key: http.request_id
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: http.request.method
 | 
			
		||||
        key: http.request.method
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: http.request.body.size
 | 
			
		||||
        key: http.request.body.size
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: http.response.status_code
 | 
			
		||||
        key: http.response.status_code
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: url.full
 | 
			
		||||
        key: url.full
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: url.scheme
 | 
			
		||||
        key: url.scheme
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: url.path
 | 
			
		||||
        key: url.path
 | 
			
		||||
      - action: insert
 | 
			
		||||
        from_attribute: http.route
 | 
			
		||||
        key: http.route
 | 
			
		||||
        from_attribute: http.uri
 | 
			
		||||
        key: http.uri
 | 
			
		||||
      - action: insert
 | 
			
		||||
        key: loki.format
 | 
			
		||||
        value: raw
 | 
			
		||||
 | 
			
		||||
@ -1,33 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Bridge\Monolog\Handler;
 | 
			
		||||
 | 
			
		||||
use Monolog\Formatter\FormatterInterface;
 | 
			
		||||
use Monolog\Formatter\JsonFormatter;
 | 
			
		||||
use Monolog\Handler\AbstractHandler;
 | 
			
		||||
use Monolog\Handler\FormattableHandlerTrait;
 | 
			
		||||
use Monolog\LogRecord;
 | 
			
		||||
use OpenTelemetry\API\Globals;
 | 
			
		||||
use OpenTelemetry\Contrib\Logs\Monolog\Handler;
 | 
			
		||||
use Psr\Log\LogLevel;
 | 
			
		||||
use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter;
 | 
			
		||||
 | 
			
		||||
final class SymfonyOtelHandler extends AbstractHandler
 | 
			
		||||
{
 | 
			
		||||
    protected readonly Handler $innerHandler;
 | 
			
		||||
 | 
			
		||||
    public function __construct() {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->innerHandler = new Handler(
 | 
			
		||||
            Globals::loggerProvider(),
 | 
			
		||||
            LogLevel::INFO, //or `Logger::INFO`, or `Level::Info` depending on monolog version
 | 
			
		||||
            true,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function handle(LogRecord $record): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->innerHandler->handle($record);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Controller;
 | 
			
		||||
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\Routing\Attribute\Route;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used for testing error handling (sentry...)
 | 
			
		||||
 */
 | 
			
		||||
class ExceptionController extends AbstractController
 | 
			
		||||
{
 | 
			
		||||
    #[Route("give-me-error-please/exception")]
 | 
			
		||||
    public function makeException(): void {
 | 
			
		||||
        throw new \InvalidArgumentException("There is exception");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,10 +2,9 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Controller;
 | 
			
		||||
 | 
			
		||||
use App\Entity\Input\QRCode\QRCode;
 | 
			
		||||
use App\Entity\QRCode\QRCode;
 | 
			
		||||
use App\Form\Type\QRCodeType;
 | 
			
		||||
use App\Service\DTO\QRCodeEntityConverter;
 | 
			
		||||
use App\Service\QRCodeProviderInterface;
 | 
			
		||||
use App\Service\QRCodeGeneratorInterface;
 | 
			
		||||
use App\Service\QRCodeQROptionsProviderInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
@ -13,11 +12,11 @@ use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\Routing\Attribute\Route;
 | 
			
		||||
 | 
			
		||||
class IndexController extends AbstractController {
 | 
			
		||||
    public function __construct(private readonly QRCodeQROptionsProviderInterface $qrCodeQROptionsFactory, private readonly QRCodeEntityConverter $codeEntityConverter) {}
 | 
			
		||||
    public function __construct(private readonly QRCodeQROptionsProviderInterface $qrCodeQROptionsFactory) {}
 | 
			
		||||
    #[Route('/', name: 'homepage')]
 | 
			
		||||
    public function indexAction(
 | 
			
		||||
        Request                 $request,
 | 
			
		||||
        QRCodeProviderInterface $qrCodeGenerator,
 | 
			
		||||
        Request $request,
 | 
			
		||||
        QRCodeGeneratorInterface $qrCodeGenerator,
 | 
			
		||||
    ): Response
 | 
			
		||||
    {
 | 
			
		||||
        $qrCodeImage = null;
 | 
			
		||||
@ -34,7 +33,7 @@ class IndexController extends AbstractController {
 | 
			
		||||
                    do session, nicmene u takovehleho typu aplikace me to prijde naopak kontraproduktivni.
 | 
			
		||||
                    a zadani o tom mlci. Navic v pripade ze aplikace bude provozovana ve vice instancich
 | 
			
		||||
                    by se musela resit memcache, redis... */
 | 
			
		||||
                $qrCodeImage = $qrCodeGenerator->generateQRCodeFromEntity($this->codeEntityConverter->convert($qrCode));
 | 
			
		||||
                $qrCodeImage = $qrCodeGenerator->generateQRCodeFromEntity($qrCode);
 | 
			
		||||
                $form = $this->createForm(QRCodeType::class, $this->createQrCodeEntity());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\DTO\QRCode;
 | 
			
		||||
 | 
			
		||||
readonly class QRCode {
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public string $iban,
 | 
			
		||||
        public \DateTimeImmutable $dueDate,
 | 
			
		||||
        public string $message,
 | 
			
		||||
        public QRCodeMoney $money,
 | 
			
		||||
        public QRCodeQROptions $qrOptions,
 | 
			
		||||
        public ?QRCodePaymentIdentification $paymentIdentification)
 | 
			
		||||
    { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\DTO\QRCode;
 | 
			
		||||
 | 
			
		||||
readonly class QRCodeMoney
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(public string $amount, public string $currency) { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\DTO\QRCode;
 | 
			
		||||
 | 
			
		||||
readonly class QRCodePaymentIdentification
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(public ?string $variableSymbol, public ?string $specificSymbol, public ?string $constantSymbol) { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\DTO\QRCode;
 | 
			
		||||
 | 
			
		||||
readonly class QRCodeQROptions
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(public int $scale, public int $margin)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Input\QRCode;
 | 
			
		||||
namespace App\Entity\QRCode;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Input\QRCode;
 | 
			
		||||
namespace App\Entity\QRCode;
 | 
			
		||||
 | 
			
		||||
use App\Validator\Currency;
 | 
			
		||||
use App\Validator\Money;
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Input\QRCode;
 | 
			
		||||
namespace App\Entity\QRCode;
 | 
			
		||||
 | 
			
		||||
use App\Validator\BankPaymentIdentificationNumber;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Payment identification for QR code
 | 
			
		||||
@ -12,17 +13,17 @@ class QRCodePaymentIdentification {
 | 
			
		||||
    #[BankPaymentIdentificationNumber(
 | 
			
		||||
        message: 'messages.not_variable_symbol',
 | 
			
		||||
    )]
 | 
			
		||||
    private ?string $variableSymbol = null;
 | 
			
		||||
    private ?string $variableSymbol;
 | 
			
		||||
 | 
			
		||||
    #[BankPaymentIdentificationNumber(
 | 
			
		||||
        message: 'messages.not_specific_symbol',
 | 
			
		||||
    )]
 | 
			
		||||
    private ?string $specificSymbol = null;
 | 
			
		||||
    private ?string $specificSymbol;
 | 
			
		||||
 | 
			
		||||
    #[BankPaymentIdentificationNumber(
 | 
			
		||||
        message: 'messages.not_constant_symbol',
 | 
			
		||||
    )]  // https://www.hyponamiru.cz/en/glossary/constant-symbol/
 | 
			
		||||
    private ?string $constantSymbol = null;
 | 
			
		||||
    private ?string $constantSymbol;
 | 
			
		||||
 | 
			
		||||
    public function getVariableSymbol(): ?string
 | 
			
		||||
    {
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Input\QRCode;
 | 
			
		||||
namespace App\Entity\QRCode;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Remote\Usetreno;
 | 
			
		||||
 | 
			
		||||
readonly class AuthRequest
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(public string $username, public string $password) {}
 | 
			
		||||
}
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Remote\Usetreno\Edge;
 | 
			
		||||
 | 
			
		||||
/* Bohuzel jsem se nedozvedel z API co je optional a co je required. Tudiz jsem vychazel z toho ze vsechno je required az na EdgeQRCodeMoney.*
 | 
			
		||||
    ktery uz z principu veci by mel byt optional (cizi staty nepodporuji variable, constant, specific...)
 | 
			
		||||
 */
 | 
			
		||||
readonly class EdgeQRCode {
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public string $iban,
 | 
			
		||||
        public string $dueDate,
 | 
			
		||||
        public string $message,
 | 
			
		||||
        public EdgeQRCodeMoney $money,
 | 
			
		||||
        public EdgeQRCodeQROptions $qrOptions,
 | 
			
		||||
        public EdgeQRCodePaymentIdentification $paymentIdentification)
 | 
			
		||||
    { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Remote\Usetreno\Edge;
 | 
			
		||||
 | 
			
		||||
readonly class EdgeQRCodeMoney
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(public string $amount, public string $currency) { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Remote\Usetreno\Edge;
 | 
			
		||||
 | 
			
		||||
readonly class EdgeQRCodePaymentIdentification
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(public string $variableSymbol, public string $specificSymbol, public string $constantSymbol) { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Entity\Remote\Usetreno\Edge;
 | 
			
		||||
 | 
			
		||||
readonly class EdgeQRCodeQROptions
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(public int $scale, public int $margin)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,7 +3,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Form\Type;
 | 
			
		||||
 | 
			
		||||
use App\Entity\Input\QRCode\QRCodeMoney;
 | 
			
		||||
use App\Entity\QRCode\QRCodeMoney;
 | 
			
		||||
use App\Service\CurrencyListerInterface;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 | 
			
		||||
@ -23,7 +23,7 @@ class QRCodeMoneyType extends AbstractType
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options): void
 | 
			
		||||
    {
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('amount', NumberType::class, ['invalid_message' => 'messages.not_valid_amount'])
 | 
			
		||||
            ->add('amount', NumberType::class)
 | 
			
		||||
            ->add('currency', ChoiceType::class, [
 | 
			
		||||
                'choices' => array_combine(
 | 
			
		||||
                    iterator_to_array($this->currencyLister->getCurrencies()),
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Form\Type;
 | 
			
		||||
 | 
			
		||||
use App\Entity\Input\QRCode\QRCodePaymentIdentification;
 | 
			
		||||
use App\Entity\QRCode\QRCodePaymentIdentification;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace App\Form\Type;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
use App\Entity\Input\QRCode\QRCode;
 | 
			
		||||
use App\Entity\QRCode\QRCode;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
 | 
			
		||||
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
 | 
			
		||||
interface CacheableQRCodeProviderInterface extends QRCodeProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    // We can't calculate cache key directly from QRCode
 | 
			
		||||
    public function getCacheKey(QRCode $entity): string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,38 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Contracts\Cache\CacheInterface;
 | 
			
		||||
use Symfony\Contracts\Cache\ItemInterface;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
final readonly class CachedQRCodeProvider implements QRCodeProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    private \DateInterval $cacheDuration;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(private CacheableQRCodeProviderInterface $innerGenerator, private CacheInterface $cache, private LoggerInterface $logger, string $cacheDuration)
 | 
			
		||||
    {
 | 
			
		||||
        $this->cacheDuration = new \DateInterval($cacheDuration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function generateQRCodeFromEntity(QRCode $entity): string
 | 
			
		||||
    {
 | 
			
		||||
        $key = $this->innerGenerator->getCacheKey($entity);
 | 
			
		||||
        return $this->cache->get($key, function(ItemInterface $c) use ($entity) {
 | 
			
		||||
            $this->logger->debug("cache miss for key " . $c->getKey(), [
 | 
			
		||||
                'entity' => $entity,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            $c->expiresAfter($this->cacheDuration);
 | 
			
		||||
 | 
			
		||||
            return $this->innerGenerator->generateQRCodeFromEntity($entity);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,59 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Service\DTO;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodeMoney;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodePaymentIdentification;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodeQROptions;
 | 
			
		||||
use Nubium\Exception\ThisShouldNeverHappenException;
 | 
			
		||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class QRCodeEntityConverter
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private ValidatorInterface $validator) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function convert(\App\Entity\Input\QRCode\QRCode $code): QRCode {
 | 
			
		||||
        if (count($this->validator->validate($code)) !== 0) {
 | 
			
		||||
            throw new \InvalidArgumentException("QRCode entity is not valid");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new QRCode(
 | 
			
		||||
            $code->getIban() ?? throw new ThisShouldNeverHappenException("iban not set"),
 | 
			
		||||
            \DateTimeImmutable::createFromMutable($code->getDueDate() ?? throw new ThisShouldNeverHappenException("due date not set")),
 | 
			
		||||
            $code->getMessage() ?? throw new ThisShouldNeverHappenException("message not set"),
 | 
			
		||||
            $this->convertMoney($code->getMoney() ?? throw new ThisShouldNeverHappenException("money not set")),
 | 
			
		||||
            $this->convertCodeOptions($code->getCodeQROptions() ?? throw new ThisShouldNeverHappenException("codeQROptions not set")),
 | 
			
		||||
            $this->convertIdentification($code->getPaymentIdentification())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function convertMoney(\App\Entity\Input\QRCode\QRCodeMoney $money): QRCodeMoney
 | 
			
		||||
    {
 | 
			
		||||
        return new QRCodeMoney(
 | 
			
		||||
            $money->getAmount() ?? throw new ThisShouldNeverHappenException("amount not set"),
 | 
			
		||||
            $money->getCurrency() ?? throw new ThisShouldNeverHappenException("currency not set")
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function convertCodeOptions(\App\Entity\Input\QRCode\QRCodeQROptions $options): QRCodeQROptions
 | 
			
		||||
    {
 | 
			
		||||
        return new QRCodeQROptions(
 | 
			
		||||
            $options->getScale(),
 | 
			
		||||
            $options->getMargin()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function convertIdentification(?\App\Entity\Input\QRCode\QRCodePaymentIdentification $ident): ?QRCodePaymentIdentification {
 | 
			
		||||
        return match ($ident) {
 | 
			
		||||
            null => null,
 | 
			
		||||
            default => new QRCodePaymentIdentification(
 | 
			
		||||
                $ident->getVariableSymbol(),
 | 
			
		||||
                $ident->getSpecificSymbol(),
 | 
			
		||||
                $ident->getConstantSymbol()
 | 
			
		||||
            )
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Exception;
 | 
			
		||||
 | 
			
		||||
class QRCodeGeneratorException extends \RuntimeException
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -3,16 +3,14 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use App\Service\Exception\QRCodeGeneratorException;
 | 
			
		||||
use App\Entity\QRCode\QRCode;
 | 
			
		||||
 | 
			
		||||
interface QRCodeProviderInterface
 | 
			
		||||
interface QRCodeGeneratorInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Generates QR code from entity
 | 
			
		||||
     * @param QRCode $entity
 | 
			
		||||
     * @return string base64 encoded PNG
 | 
			
		||||
     * @throws QRCodeGeneratorException
 | 
			
		||||
     */
 | 
			
		||||
    public function generateQRCodeFromEntity(QRCode $entity): string;
 | 
			
		||||
}
 | 
			
		||||
@ -3,18 +3,15 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service;
 | 
			
		||||
 | 
			
		||||
use App\Entity\Input\QRCode\QRCodeQROptions;
 | 
			
		||||
use App\Entity\QRCode\QRCodeQROptions;
 | 
			
		||||
 | 
			
		||||
readonly final class QRCodeQROptionsDefaultProvider implements QRCodeQROptionsProviderInterface {
 | 
			
		||||
    const DEFAULT_SCALE = 8;
 | 
			
		||||
    const DEFAULT_MARGIN = 0;
 | 
			
		||||
 | 
			
		||||
    private QRCodeQROptions $qrCodeDefaultOptions;
 | 
			
		||||
 | 
			
		||||
    public function __construct() {
 | 
			
		||||
        $this->qrCodeDefaultOptions = new QRCodeQROptions(
 | 
			
		||||
            self::DEFAULT_SCALE,
 | 
			
		||||
            self::DEFAULT_MARGIN
 | 
			
		||||
            1,
 | 
			
		||||
            0
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service;
 | 
			
		||||
 | 
			
		||||
use App\Entity\Input\QRCode\QRCodeQROptions;
 | 
			
		||||
use App\Entity\QRCode\QRCodeQROptions;
 | 
			
		||||
 | 
			
		||||
interface QRCodeQROptionsProviderInterface {
 | 
			
		||||
    public function getDefault(): QRCodeQROptions;
 | 
			
		||||
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote\Edge\Exceptions;
 | 
			
		||||
 | 
			
		||||
class MissingParameterException extends \RuntimeException
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote\Edge;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodeMoney;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodePaymentIdentification;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodeQROptions;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCode;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCodeMoney;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCodePaymentIdentification;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCodeQROptions;
 | 
			
		||||
use App\Service\Remote\Edge\Exceptions\MissingParameterException;
 | 
			
		||||
 | 
			
		||||
class QRCodeEntityConverter
 | 
			
		||||
{
 | 
			
		||||
    public function __construct() { }
 | 
			
		||||
 | 
			
		||||
    public function convert(QRCode $code): EdgeQRCode {
 | 
			
		||||
        return new EdgeQRCode(
 | 
			
		||||
            $code->iban,
 | 
			
		||||
            $code->dueDate->format('Y-m-d'),
 | 
			
		||||
            $code->message,
 | 
			
		||||
            $this->convertMoney($code->money),
 | 
			
		||||
            $this->convertCodeOptions($code->qrOptions),
 | 
			
		||||
            $this->convertIdentification($code->paymentIdentification)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function convertMoney(QRCodeMoney $money): EdgeQRCodeMoney
 | 
			
		||||
    {
 | 
			
		||||
        return new EdgeQRCodeMoney(
 | 
			
		||||
            $money->amount,
 | 
			
		||||
            $money->currency
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function convertCodeOptions(QRCodeQROptions $options): EdgeQRCodeQROptions
 | 
			
		||||
    {
 | 
			
		||||
        return new EdgeQRCodeQROptions(
 | 
			
		||||
            $options->scale,
 | 
			
		||||
            $options->margin
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function convertIdentification(?QRCodePaymentIdentification $ident): EdgeQRCodePaymentIdentification {
 | 
			
		||||
        // OMG: proc? symboly v ostatnich statech nejsou, prijde me lepsi posilat NULL, nebo empty string ;-)
 | 
			
		||||
        return match ($ident) {
 | 
			
		||||
            null => new EdgeQRCodePaymentIdentification(
 | 
			
		||||
                "0",
 | 
			
		||||
                "0",
 | 
			
		||||
                "0"
 | 
			
		||||
            ),
 | 
			
		||||
            default => new EdgeQRCodePaymentIdentification(
 | 
			
		||||
                $ident->variableSymbol ?? "0",
 | 
			
		||||
                $ident->specificSymbol ?? "0",
 | 
			
		||||
                $ident->constantSymbol ?? "0"
 | 
			
		||||
            )
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote\Exception;
 | 
			
		||||
 | 
			
		||||
class AuthorizeException extends \RuntimeException
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote\Exception;
 | 
			
		||||
 | 
			
		||||
use App\Service\Exception\QRCodeGeneratorException;
 | 
			
		||||
 | 
			
		||||
class UsetrenoQRCodeException extends QRCodeGeneratorException
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote\Exception;
 | 
			
		||||
 | 
			
		||||
class UsetrenoQRCodeRemoteServerErrorException extends UsetrenoQRCodeException
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,58 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use Exception;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
 | 
			
		||||
trait RetryingFailClientTrait
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Lepsi zopakovat request kvuli drobnemu vypadku site nebo sluzby (u nedestruktivni operace)
 | 
			
		||||
     * nez hodit klientovi rovnou 500
 | 
			
		||||
     *
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param float $sleep
 | 
			
		||||
     * @param array<string> $catchableExceptions
 | 
			
		||||
     * @param LoggerInterface $logger
 | 
			
		||||
     * @param callable $callback
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    protected function retryingFailRequest(
 | 
			
		||||
        int $count,
 | 
			
		||||
        float $sleep,
 | 
			
		||||
        array $catchableExceptions,
 | 
			
		||||
        LoggerInterface $logger,
 | 
			
		||||
        callable $callback
 | 
			
		||||
    ): mixed {
 | 
			
		||||
        for ($i = 0; ; $i++) {
 | 
			
		||||
            try {
 | 
			
		||||
                return $callback();
 | 
			
		||||
            } catch (Exception $e) {
 | 
			
		||||
                foreach ($catchableExceptions as $exceptionClass) {
 | 
			
		||||
                    if ($e instanceof $exceptionClass) {
 | 
			
		||||
                        $logger->error("transport: fail request retrying... got catchable exception", [
 | 
			
		||||
                            'exception' => $e,
 | 
			
		||||
                            'try' => $i
 | 
			
		||||
                        ]);
 | 
			
		||||
 | 
			
		||||
                        usleep((int) ($sleep * 1_000_000));
 | 
			
		||||
 | 
			
		||||
                        if ($i == $count) {
 | 
			
		||||
                            throw $e;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        continue 2;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                throw $e;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // phpstan fail
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,160 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use App\Entity\Remote\Usetreno\AuthRequest;
 | 
			
		||||
use App\Service\Remote\Exception\AuthorizeException;
 | 
			
		||||
use Nubium\Exception\ThisShouldNeverHappenException;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\HttpClient\Exception\TransportException;
 | 
			
		||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\ResponseInterface;
 | 
			
		||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
 | 
			
		||||
 | 
			
		||||
class UsetrenoHttpClient implements HttpClientInterface
 | 
			
		||||
{
 | 
			
		||||
    use RetryingFailClientTrait;
 | 
			
		||||
 | 
			
		||||
    const AUTHORIZE_API = "https://topapi.top-test.cz/chameleon/api/v1/token";
 | 
			
		||||
 | 
			
		||||
    protected ?string $authorizationToken = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(protected HttpClientInterface $innerClient, protected readonly LoggerInterface $logger, protected readonly string $username,
 | 
			
		||||
                                protected readonly string $password,
 | 
			
		||||
                                protected readonly float $retryWaitSeconds,
 | 
			
		||||
                                protected readonly int $retryCount) { }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $method
 | 
			
		||||
     * @param string $url
 | 
			
		||||
     * @param array<string, mixed> $options
 | 
			
		||||
     * @return ResponseInterface
 | 
			
		||||
     * @throws ClientExceptionInterface
 | 
			
		||||
     * @throws RedirectionExceptionInterface
 | 
			
		||||
     * @throws ServerExceptionInterface
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function request(string $method, string $url, array $options = []): ResponseInterface {
 | 
			
		||||
        if ($this->authorizationToken === null) {
 | 
			
		||||
            $this->authorize();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->innerClient->request($method, $url, $options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param ResponseInterface|iterable<array-key, ResponseInterface> $responses One or more responses created by the current HTTP client
 | 
			
		||||
     * @param float|null $timeout
 | 
			
		||||
     * @return ResponseStreamInterface
 | 
			
		||||
     * @throws ClientExceptionInterface
 | 
			
		||||
     * @throws RedirectionExceptionInterface
 | 
			
		||||
     * @throws ServerExceptionInterface
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function stream(iterable|ResponseInterface $responses, float $timeout = null): ResponseStreamInterface
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->authorizationToken === null) {
 | 
			
		||||
            $this->authorize();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->innerClient->stream($responses, $timeout);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array<string, mixed> $options
 | 
			
		||||
     * @return static(UsetrenoHttpClient)
 | 
			
		||||
     * @throws ClientExceptionInterface
 | 
			
		||||
     * @throws RedirectionExceptionInterface
 | 
			
		||||
     * @throws ServerExceptionInterface
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function withOptions(array $options): static
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->authorizationToken === null) {
 | 
			
		||||
            $this->authorize();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->innerClient = $this->innerClient->withOptions($options);
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws TransportExceptionInterface
 | 
			
		||||
     * @throws ClientExceptionInterface
 | 
			
		||||
     * @throws RedirectionExceptionInterface
 | 
			
		||||
     * @throws ServerExceptionInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected function authorize(): void {
 | 
			
		||||
        $this->logger->debug("trying authorize request", [
 | 
			
		||||
            "AUTHORIZE_API" => static::AUTHORIZE_API
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds,
 | 
			
		||||
            [AuthorizeException::class, TransportException::class], $this->logger, function() {
 | 
			
		||||
            $rq = $this->innerClient->request(
 | 
			
		||||
                "POST",
 | 
			
		||||
                static::AUTHORIZE_API,
 | 
			
		||||
                [
 | 
			
		||||
                    'json' => new AuthRequest($this->username, $this->password),
 | 
			
		||||
                ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $statusCode = $rq->getStatusCode();
 | 
			
		||||
            $responseData = $rq->getContent(false);
 | 
			
		||||
 | 
			
		||||
            if ($statusCode != 200) {
 | 
			
		||||
                $this->logger->error("authorization request status code is not ok", [
 | 
			
		||||
                    "AUTHORIZE_API" => static::AUTHORIZE_API,
 | 
			
		||||
                    "statusCode" => $statusCode,
 | 
			
		||||
                    "content" => $responseData,
 | 
			
		||||
                    "username" => $this->username
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
                throw new AuthorizeException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $responseData;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!is_string($responseData)) {
 | 
			
		||||
            throw new ThisShouldNeverHappenException("responseData is not a string");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->authorizationToken = $this->processAuthorizeResponse($responseData);
 | 
			
		||||
        $this->innerClient = $this->innerClient->withOptions([
 | 
			
		||||
            'headers' => [
 | 
			
		||||
                'Authorization' => "Bearer " . $this->authorizationToken
 | 
			
		||||
            ]
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $responseData
 | 
			
		||||
     * @return string Bearer token
 | 
			
		||||
     * @throws AuthorizeException
 | 
			
		||||
     */
 | 
			
		||||
    protected function processAuthorizeResponse(string $responseData): string {
 | 
			
		||||
        $data = json_decode($responseData);
 | 
			
		||||
 | 
			
		||||
        if (!is_object($data)) {
 | 
			
		||||
            $this->logger->error("authorize: received null response data", [
 | 
			
		||||
                "responseData" => $responseData
 | 
			
		||||
            ]);
 | 
			
		||||
            throw new AuthorizeException("Can't decode json response");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($data->data->token)) {
 | 
			
		||||
            $this->logger->error("authorize: empty token in response data", [
 | 
			
		||||
                "responseData" => $responseData
 | 
			
		||||
            ]);
 | 
			
		||||
            throw new AuthorizeException("Got invalid json response");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $data->data->token;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,119 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use App\Service\CacheableQRCodeProviderInterface;
 | 
			
		||||
use App\Service\Remote\Edge\QRCodeEntityConverter;
 | 
			
		||||
use App\Service\Remote\Exception\UsetrenoQRCodeException;
 | 
			
		||||
use App\Service\Remote\Exception\UsetrenoQRCodeRemoteServerErrorException;
 | 
			
		||||
use Nubium\Exception\ThisShouldNeverHappenException;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\HttpClient\Exception\TransportException;
 | 
			
		||||
 | 
			
		||||
class UsetrenoQRCodeProvider implements CacheableQRCodeProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    use RetryingFailClientTrait;
 | 
			
		||||
 | 
			
		||||
    const QRCODE_API = 'https://topapi.top-test.cz/chameleon/api/v1/qr-code/create-for-bank-account-payment';
 | 
			
		||||
    const QRCODE_METHOD = 'POST';
 | 
			
		||||
 | 
			
		||||
    public function __construct(protected readonly LoggerInterface $logger,
 | 
			
		||||
                                protected readonly UsetrenoHttpClient $client,
 | 
			
		||||
                                protected readonly QRCodeEntityConverter $codeEntityConverter,
 | 
			
		||||
                                protected readonly float $retryWaitSeconds,
 | 
			
		||||
                                protected readonly int $retryCount) { }
 | 
			
		||||
 | 
			
		||||
    public function generateQRCodeFromEntity(QRCode $entity): string
 | 
			
		||||
    {
 | 
			
		||||
        $edgeEntity = $this->codeEntityConverter->convert($entity);
 | 
			
		||||
 | 
			
		||||
        $this->logger->debug("Sending request for QR code", [
 | 
			
		||||
            "entity" => $entity,
 | 
			
		||||
            "edgeEntity" => $edgeEntity
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $responseData = $this->retryingFailRequest($this->retryCount, $this->retryWaitSeconds,
 | 
			
		||||
            [TransportException::class, UsetrenoQRCodeRemoteServerErrorException::class], $this->logger,
 | 
			
		||||
            function() use ($edgeEntity) {
 | 
			
		||||
                $response = $this->client->request(static::QRCODE_METHOD, static::QRCODE_API, [
 | 
			
		||||
                    'json' => $edgeEntity,
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
                $statusCode = $response->getStatusCode();
 | 
			
		||||
                $responseData = $response->getContent(false);
 | 
			
		||||
 | 
			
		||||
                if ($statusCode != 200) {
 | 
			
		||||
                    $this->logger->error("qrcode request status code is not ok", [
 | 
			
		||||
                        "AUTHORIZE_API" => static::QRCODE_API,
 | 
			
		||||
                        "statusCode" => $statusCode,
 | 
			
		||||
                        "content" => $responseData,
 | 
			
		||||
                    ]);
 | 
			
		||||
 | 
			
		||||
                    if ($statusCode > 500) {
 | 
			
		||||
                        throw new UsetrenoQRCodeRemoteServerErrorException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    throw new UsetrenoQRCodeException("Return code is not 200 OK (got: code: $statusCode)");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $this->logger->debug("QRCode generation success", [
 | 
			
		||||
                    "responseContent" => $responseData,
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
                return $responseData;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!is_string($responseData)) {
 | 
			
		||||
            throw new ThisShouldNeverHappenException("responseData is not a string");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return $this->parseBase64String($this->processQRCodeResponseEntity($responseData));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function parseBase64String(string $content): string {
 | 
			
		||||
        $data = explode(';base64,', $content);
 | 
			
		||||
        // check if response is image (we support png only now)
 | 
			
		||||
        if ($data[0] === "image/png") {
 | 
			
		||||
            throw new UsetrenoQRCodeException("Unsupported image type $data[0]");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $data[1];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $responseData
 | 
			
		||||
     * @return string Bearer token
 | 
			
		||||
     * @throws UsetrenoQRCodeException
 | 
			
		||||
     */
 | 
			
		||||
    protected function processQRCodeResponseEntity(string $responseData): string {
 | 
			
		||||
        $data = json_decode($responseData);
 | 
			
		||||
 | 
			
		||||
        if (!is_object($data)) {
 | 
			
		||||
            $this->logger->error("qrcode: received null response data", [
 | 
			
		||||
                "responseData" => $responseData
 | 
			
		||||
            ]);
 | 
			
		||||
            throw new UsetrenoQRCodeException("Can't decode json response");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($data->data->base64Data)) {
 | 
			
		||||
            $this->logger->error("qrcode: empty base64Data in response data", [
 | 
			
		||||
                "responseData" => $responseData
 | 
			
		||||
            ]);
 | 
			
		||||
            throw new UsetrenoQRCodeException("Got invalid json response");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $data->data->base64Data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCacheKey(QRCode $entity): string
 | 
			
		||||
    {
 | 
			
		||||
        $edgeEntity = $this->codeEntityConverter->convert($entity);
 | 
			
		||||
        $encodedEntity = json_encode($edgeEntity);
 | 
			
		||||
        if ($encodedEntity === false) {
 | 
			
		||||
            throw new \RuntimeException("Can't serialize edge entity");
 | 
			
		||||
        }
 | 
			
		||||
        return base64_encode($encodedEntity);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,9 +3,9 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Service;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use App\Entity\QRCode\QRCode;
 | 
			
		||||
 | 
			
		||||
readonly final class StubQRCodeProvider implements QRCodeProviderInterface
 | 
			
		||||
readonly final class StubQRCodeGenerator implements QRCodeGeneratorInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private string $imagePath) {}
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ class BankPaymentIdentificationNumberValidator extends ConstraintValidator
 | 
			
		||||
            throw new UnexpectedValueException($value, "string");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (strlen($value) <= 10 && filter_var($value, FILTER_VALIDATE_INT) !== false && (int) $value > 0) {
 | 
			
		||||
        if (strlen($value) <= 10 && filter_var($value, FILTER_VALIDATE_INT) !== false) {
 | 
			
		||||
            return ;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								symfony.lock
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								symfony.lock
									
									
									
									
									
								
							@ -49,18 +49,6 @@
 | 
			
		||||
            "tests/bootstrap.php"
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    "sentry/sentry-symfony": {
 | 
			
		||||
        "version": "4.13",
 | 
			
		||||
        "recipe": {
 | 
			
		||||
            "repo": "github.com/symfony/recipes-contrib",
 | 
			
		||||
            "branch": "main",
 | 
			
		||||
            "version": "4.6",
 | 
			
		||||
            "ref": "153de5f041f7e8a9c19f3674b800b76be0e6fd90"
 | 
			
		||||
        },
 | 
			
		||||
        "files": [
 | 
			
		||||
            "config/packages/sentry.yaml"
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    "symfony/asset-mapper": {
 | 
			
		||||
        "version": "7.0",
 | 
			
		||||
        "recipe": {
 | 
			
		||||
@ -119,18 +107,6 @@
 | 
			
		||||
            "src/Kernel.php"
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    "symfony/monolog-bundle": {
 | 
			
		||||
        "version": "3.10",
 | 
			
		||||
        "recipe": {
 | 
			
		||||
            "repo": "github.com/symfony/recipes",
 | 
			
		||||
            "branch": "main",
 | 
			
		||||
            "version": "3.7",
 | 
			
		||||
            "ref": "213676c4ec929f046dfde5ea8e97625b81bc0578"
 | 
			
		||||
        },
 | 
			
		||||
        "files": [
 | 
			
		||||
            "config/packages/monolog.yaml"
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    "symfony/phpunit-bridge": {
 | 
			
		||||
        "version": "7.0",
 | 
			
		||||
        "recipe": {
 | 
			
		||||
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
{% extends 'base.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
    <p>
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div style="text-align: center;">
 | 
			
		||||
                <h1>{{ "page not found" | trans }}</h1>
 | 
			
		||||
                <a href="{{ path('homepage') }}">{{ "return to the homepage" | trans }}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </p>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -1,13 +0,0 @@
 | 
			
		||||
{% extends 'base.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
    <p>
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <div style="text-align: center;">
 | 
			
		||||
            <h1>{{ "internal server error" | trans }}</h1>
 | 
			
		||||
            {{ "our developers are doing their best to fix it. please try again later." | trans }}
 | 
			
		||||
            <a href="{{ path('homepage') }}">{{ "return to the homepage" | trans }}</a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    </p>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Tests\Common;
 | 
			
		||||
 | 
			
		||||
use Monolog\Handler\TestHandler;
 | 
			
		||||
use Monolog\Logger;
 | 
			
		||||
 | 
			
		||||
trait LoggerTrait
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Logger
 | 
			
		||||
     */
 | 
			
		||||
    protected function getLogger(): Logger {
 | 
			
		||||
        $logger = new Logger('test');
 | 
			
		||||
        $logger->pushHandler(new TestHandler());
 | 
			
		||||
        return $logger;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,7 +3,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace App\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use App\Service\QRCodeProviderInterface;
 | 
			
		||||
use App\Service\QRCodeGeneratorInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,7 @@ class IndexControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
    private function generateClientForSubmitTest(): KernelBrowser
 | 
			
		||||
    {
 | 
			
		||||
        $mock = $this->createMock(QRCodeProviderInterface::class);
 | 
			
		||||
        $mock = $this->createMock(QRCodeGeneratorInterface::class);
 | 
			
		||||
        $mock->expects($this->any())
 | 
			
		||||
            ->method('generateQRCodeFromEntity')
 | 
			
		||||
            ->will($this->returnValue('foo'));
 | 
			
		||||
@ -85,7 +85,7 @@ class IndexControllerTest extends WebTestCase
 | 
			
		||||
        $client = static::createClient();
 | 
			
		||||
        $client->disableReboot();
 | 
			
		||||
 | 
			
		||||
        self::getContainer()->set('App\Service\QRCodeProviderInterface', $mock);
 | 
			
		||||
        self::getContainer()->set('App\Service\QRCodeGeneratorInterface', $mock);
 | 
			
		||||
 | 
			
		||||
        return $client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,56 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Tests\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use App\Service\Remote\RetryingFailClientTrait;
 | 
			
		||||
use App\Tests\Common\LoggerTrait;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
 | 
			
		||||
class RetryingFailClientTraitTest extends TestCase {
 | 
			
		||||
    use LoggerTrait;
 | 
			
		||||
 | 
			
		||||
    private int $callCount = 0;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::setUp(); // TODO: Change the autogenerated stub
 | 
			
		||||
        $this->callCount = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSuccess() {
 | 
			
		||||
        $trait = new class {
 | 
			
		||||
            use RetryingFailClientTrait {
 | 
			
		||||
                retryingFailRequest as public; // make the method public
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        $result = $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () {
 | 
			
		||||
            $this->callCount = $this->callCount + 1;
 | 
			
		||||
            return 'foo';
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(1, $this->callCount);
 | 
			
		||||
        $this->assertEquals('foo', $result);
 | 
			
		||||
    }
 | 
			
		||||
    public function testRetyingFail() {
 | 
			
		||||
        $trait = new class {
 | 
			
		||||
            use RetryingFailClientTrait {
 | 
			
		||||
                retryingFailRequest as public; // make the method public
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () {
 | 
			
		||||
                $this->callCount = $this->callCount + 1;
 | 
			
		||||
                throw new \RuntimeException("test");
 | 
			
		||||
            });
 | 
			
		||||
        } catch (\RuntimeException) {
 | 
			
		||||
            // do nothing
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(3, $this->callCount);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Tests\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use App\Service\Remote\Exception\AuthorizeException;
 | 
			
		||||
use App\Service\Remote\UsetrenoHttpClient;
 | 
			
		||||
use App\Tests\Common\LoggerTrait;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\HttpClient\MockHttpClient;
 | 
			
		||||
use Symfony\Component\HttpClient\Response\JsonMockResponse;
 | 
			
		||||
 | 
			
		||||
class UsetrenoHttpClientTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use LoggerTrait;
 | 
			
		||||
 | 
			
		||||
    public function testRequestWillDoAutomaticAuthorization() {
 | 
			
		||||
        $authMockResponse = new JsonMockResponse([
 | 
			
		||||
            "data" => [
 | 
			
		||||
                "expires_in" => 1000,
 | 
			
		||||
                "token" => "foobarfoobar"
 | 
			
		||||
            ],
 | 
			
		||||
            "timestamp" => "2024-01-17T22:47:59+01:00",
 | 
			
		||||
            "uri" => "/api/v1/token"
 | 
			
		||||
        ]);
 | 
			
		||||
        $authorizedRequestResponse = clone $authMockResponse;
 | 
			
		||||
        $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]);
 | 
			
		||||
        $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0);
 | 
			
		||||
        $client->request("POST", "https://www.root.cz/");
 | 
			
		||||
        $this->assertEquals("https://topapi.top-test.cz/chameleon/api/v1/token", $authMockResponse->getRequestUrl());
 | 
			
		||||
        $headers = $authorizedRequestResponse->getRequestOptions()['headers'];
 | 
			
		||||
        $this->assertEquals("https://www.root.cz/", $authorizedRequestResponse->getRequestUrl());
 | 
			
		||||
        $this->assertTrue(in_array("Authorization: Bearer foobarfoobar", $headers), "missing bearer authorization header");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testRequestFailedAuthorization() {
 | 
			
		||||
        $this->expectException(AuthorizeException::class);
 | 
			
		||||
        $authMockResponse = new JsonMockResponse([
 | 
			
		||||
            "errors" => [
 | 
			
		||||
                [
 | 
			
		||||
                    "message" => "Bad credentials, please verify that your username/password are correctly set.",
 | 
			
		||||
                    "specificType" => "bad_credentials",
 | 
			
		||||
                    "type" => "security_error"
 | 
			
		||||
                ]
 | 
			
		||||
            ],
 | 
			
		||||
            "timestamp" => "2024-01-17T23:55:25+01:00",
 | 
			
		||||
            "uri" => "/api/v1/token"
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $authorizedRequestResponse = clone $authMockResponse;
 | 
			
		||||
        $mockedClient = new MockHttpClient([$authMockResponse, $authorizedRequestResponse]);
 | 
			
		||||
        $client = new UsetrenoHttpClient($mockedClient, $this->getLogger(), "foo", "bar", 0, 0);
 | 
			
		||||
        $client->request("POST", "https://www.root.cz/");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,118 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Tests\Service\Remote;
 | 
			
		||||
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCode;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodeMoney;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodePaymentIdentification;
 | 
			
		||||
use App\Entity\DTO\QRCode\QRCodeQROptions;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCode;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCodeMoney;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCodePaymentIdentification;
 | 
			
		||||
use App\Entity\Remote\Usetreno\Edge\EdgeQRCodeQROptions;
 | 
			
		||||
use App\Service\Exception\QRCodeGeneratorException;
 | 
			
		||||
use App\Service\Remote\Edge\QRCodeEntityConverter;
 | 
			
		||||
use App\Service\Remote\UsetrenoHttpClient;
 | 
			
		||||
use App\Service\Remote\UsetrenoQRCodeProvider;
 | 
			
		||||
use App\Tests\Common\LoggerTrait;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Contracts\HttpClient\ResponseInterface;
 | 
			
		||||
 | 
			
		||||
class UsetrenoQRCodeProviderTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use LoggerTrait;
 | 
			
		||||
 | 
			
		||||
    public function testSuccessRequest() {
 | 
			
		||||
        $base64Image = "iVBORw0KGgoAAAANSUhEUgAAAEkAAABJCAIAAAD+EZyLAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACzklEQVRoge1a0Y6DMAwLp/v/X+49IFWenXaQZNt1qh8mKCnEJHUC7Git2Zfi59MOvBCb25rY3NbE5rYmNrc1sbmtic1tTWxua2JzWxO/sWnHcehga+04Dvw9B3HWZHx+5oCTQW56ve7xuaEEcGK3OQ3IzD1zAHFueFW96+cIEkaPKWg25pN5nZPiNgdmoIG7Ggc3UfOo15LWGlLq7o42dLwKqbipN5SKeIjCiINqXMIzzs1d4ugraqaO4HSyDIsHO/OKd6/qq8rJGxBcbwegj9ijYJx8UO6fBgSn49yYk3Et0RVClQ2z0RXMvusqZ15jCnSS7i4yIT5UBk9WruuTgnEdcW4kcfjr6iTGAbcpja1OS4LcVN9HnYfGCm2oUzMhdmWVjpBab8gHlcOtbLjrisSkyscQ1GXSd/MkRO1dMu6paCOGODcbrC7KOnWR8pCaySpilqzdqI3KlkAhdQNOZsmngWzc6PLaixA9PQPujixjTgb7SVrupCLUQ9KUUbQpVq4s3UK2druuu8JN67Dnnnae9pgXYXo1z90mmYOud4OntQujnalsJ1LPOJNird0GWqqWkrEbzLvI9sq9I3HDYrJgtOLbsybr3TnZ10Nnhf3xxVqHxK40NHeR0hKM1eje9yXnVmrz+KjqBt1L1n7njJdfrboFuqdA3rHUMw7pJN1gSlE1QFDPbY+5GkP8nQKFoq89HCSnO0ySU3U1H7eynJzHRNvlUbeFsywnJ2XvXjEg+KvNlz3GiooklfuMS8XfcYgDtRdu72JSDKoqQap20xLq47hN3TCVDb0do1MFUPwdh3aJhvb42nPiyZOiUvytQysvHiJflaq2LB/rS1xgNdMbT7EaRaakdhd/x8FDE3rmvY1Ey3mhv4hsX4JJSEddextIP010tee2h1W1+x/im/+Dsbmtic1tTWxua2JzWxOb25rY3NbE5rYmNrc18QcV0HKSoAc9PQAAAABJRU5ErkJggg==";
 | 
			
		||||
 | 
			
		||||
        $successRequest = [
 | 
			
		||||
            "uri" => "/api/v1/qr-code/create-for-bank-account-payment",
 | 
			
		||||
            "timestamp" => "2024-01-16T14:05:42+01:00",
 | 
			
		||||
            "data" => [
 | 
			
		||||
                "base64Data" => "data:image/png;base64,$base64Image"
 | 
			
		||||
            ]
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        [$edgeEntity, $qrCodeEntity] = $this->createQRCodeEntityPair();
 | 
			
		||||
 | 
			
		||||
        $qrCodeProvider = $this->createQRCodeProvider($successRequest, 200, $edgeEntity, $qrCodeEntity);
 | 
			
		||||
        $data = $qrCodeProvider->generateQRCodeFromEntity($qrCodeEntity);
 | 
			
		||||
        $this->assertEquals($base64Image, $data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testFailureRequest() {
 | 
			
		||||
        $this->expectException(QRCodeGeneratorException::class);
 | 
			
		||||
 | 
			
		||||
        $failureRequest = [
 | 
			
		||||
            "error" => "internal server error",
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        [$edgeEntity, $qrCodeEntity] = $this->createQRCodeEntityPair();
 | 
			
		||||
 | 
			
		||||
        $qrCodeProvider = $this->createQRCodeProvider($failureRequest, 500, $edgeEntity, $qrCodeEntity);
 | 
			
		||||
        $data = $qrCodeProvider->generateQRCodeFromEntity($qrCodeEntity);
 | 
			
		||||
        $this->assertEquals($failureRequest["data"]["base64Data"], $data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function createQRCodeProvider(array $response, int $responseCode, EdgeQRCode $edgeEntity, QRCode $entity): UsetrenoQRCodeProvider
 | 
			
		||||
    {
 | 
			
		||||
        $responseMock = $this->createMock(ResponseInterface::class);
 | 
			
		||||
 | 
			
		||||
        $responseMock->expects($this->any())
 | 
			
		||||
            ->method('getContent')
 | 
			
		||||
            ->will($this->returnValue(json_encode($response)));
 | 
			
		||||
        $responseMock->expects($this->any())
 | 
			
		||||
            ->method('getStatusCode')
 | 
			
		||||
            ->will($this->returnValue($responseCode));
 | 
			
		||||
 | 
			
		||||
        $mock = $this->createMock(UsetrenoHttpClient::class);
 | 
			
		||||
        $mock->expects($this->once())
 | 
			
		||||
            ->method('request')
 | 
			
		||||
            ->will($this->returnValue($responseMock));
 | 
			
		||||
 | 
			
		||||
        $converterMock = $this->createMock(QRCodeEntityConverter::class);
 | 
			
		||||
        $converterMock->expects($this->once())
 | 
			
		||||
            ->method('convert')
 | 
			
		||||
            ->will($this->returnValue($edgeEntity));
 | 
			
		||||
 | 
			
		||||
        return new UsetrenoQRCodeProvider($this->getLogger(), $mock, $converterMock, 0, 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function createQRCodeEntityPair() {
 | 
			
		||||
        $edgeEntity = new EdgeQRCode(
 | 
			
		||||
            "CZ0000",
 | 
			
		||||
            (new \DateTime("now"))->format('Y-m-d'),
 | 
			
		||||
            "foo",
 | 
			
		||||
            new EdgeQRCodeMoney(
 | 
			
		||||
                100,
 | 
			
		||||
                "CZK"
 | 
			
		||||
            ),
 | 
			
		||||
            new EdgeQRCodeQROptions(
 | 
			
		||||
                1,
 | 
			
		||||
                1
 | 
			
		||||
            ),
 | 
			
		||||
            new EdgeQRCodePaymentIdentification(
 | 
			
		||||
                "0", "0", "0"
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $qrCodeEntity = new QRCode(
 | 
			
		||||
            "CZ0000",
 | 
			
		||||
            new \DateTimeImmutable("now"),
 | 
			
		||||
            "foo",
 | 
			
		||||
            new QRCodeMoney(
 | 
			
		||||
                100,
 | 
			
		||||
                "CZK"
 | 
			
		||||
            ),
 | 
			
		||||
            new QRCodeQROptions(
 | 
			
		||||
                1,
 | 
			
		||||
                1
 | 
			
		||||
            ),
 | 
			
		||||
            new QRCodePaymentIdentification(
 | 
			
		||||
                "0", "0", "0"
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return [$edgeEntity, $qrCodeEntity];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -35,6 +35,6 @@ class BankPaymentIdentificationNumberValidatorTest extends ValidatorTestCase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function failureDp(): array {
 | 
			
		||||
        return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a'], ['122.1'], ['12345678901'], ['-323']];
 | 
			
		||||
        return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a'], ['122.1'], ['12345678901']];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,6 @@ class MoneyValidatorTest extends ValidatorTestCase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function failureDp(): array {
 | 
			
		||||
        return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a'], ['-223']];
 | 
			
		||||
        return [['122.b'], ['a.a'], ['a'], ['2.040'], ['2,a']];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,3 @@ Generate qr code: "Vygenerovat QR kód"
 | 
			
		||||
Iban: "IBAN"
 | 
			
		||||
QR code generator: "Generátor bankovních QR kódů"
 | 
			
		||||
You can generate another QR code here: "Další QR kód si můžete vygenerovat zde"
 | 
			
		||||
page not found: 'Stránka nenalezena'
 | 
			
		||||
return to the homepage: 'Zpět na homepage'
 | 
			
		||||
"our developers are doing their best to fix it. please try again later.": "Naši programátoři dělají maximum aby to opravili. Zkuste to prosím později."
 | 
			
		||||
internal server error: 'Něco se pokazilo :-('
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user