diff --git a/.editorconfig b/.editorconfig index 0b666c6..178a650 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,90 +1,25 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# editorconfig.org - root = true [*] -# Change these settings to your own preference indent_style = space indent_size = 4 -# We recommend you to keep these unchanged end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.feature] -indent_style = space -indent_size = 2 - -[*.js] -indent_style = space -indent_size = 2 - -[*.json] -indent_style = space indent_size = 2 [*.md] -indent_style = space -indent_size = 4 trim_trailing_whitespace = false -[*.neon] -indent_style = tab -indent_size = 4 - -[*.php] -indent_style = space -indent_size = 4 - [*.sh] indent_style = tab -indent_size = 4 [*.{yaml,yml}] -indent_style = space -indent_size = 4 trim_trailing_whitespace = false -[.babelrc] -indent_style = space -indent_size = 2 - -[.gitmodules] -indent_style = tab -indent_size = 4 - -[.php_cs{,.dist}] -indent_style = space -indent_size = 4 - -[composer.json] -indent_style = space -indent_size = 4 - -[docker-compose{,.override}.{yaml,yml}] -indent_style = space +[{webpack.config.js,.eslintrc.js}] indent_size = 2 - -[Dockerfile] -indent_style = tab -indent_size = 4 - -[package.json] -indent_style = space -indent_size = 2 - -[phpspec.yml{,.dist}] -indent_style = space -indent_size = 4 - -[phpstan.neon] -indent_style = tab -indent_size = 4 - -[phpunit.xml{,.dist}] -indent_style = space -indent_size = 4 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 1a3ac93..f7797e5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,12 +1,14 @@ -/spec export-ignore -/tests export-ignore -/.github export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/composer-require-checker.json export-ignore -/easy-coding-standard.yml export-ignore -/phpspec.yml.dist export-ignore -/phpstan.neon export-ignore -/phpstan-baseline.neon export-ignore -/phpunit.xml.dist export-ignore -/README.md export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/CLAUDE.md export-ignore +/README.md export-ignore +/composer-dependency-analyser.php export-ignore +/ecs.php export-ignore +/infection.json.dist export-ignore +/phpstan-baseline.neon export-ignore +/phpstan.neon export-ignore +/phpunit.xml.dist export-ignore +/rector.php export-ignore +/tests export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f2188b9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: Setono diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000..3dc2acd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,36 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: [ "bug" ] +body: + - type: markdown + attributes: + value: Thanks for taking the time to fill out this bug report! + - type: input + id: plugin-version + attributes: + label: Plugin version + description: What version of the plugin are you using? + validations: + required: true + - type: input + id: sylius-version + attributes: + label: Sylius version + description: What version of Sylius are you using? + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ecdc4ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support question? + url: https://sylius-devs.slack.com/archives/C3EGDG9LY + about: Please ask questions on the Sylius support channel. It's okay to tag a developer, but please understand that we also have other obligations ;) + - name: Security issue + url: https://setono.com + about: Please send security vulnerabilities directly to security@setono.com diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 0000000..cf106ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,29 @@ +name: Feature request +description: Write a feature request +title: "[Feature request]: " +labels: [ "enhancement" ] +body: + - type: markdown + attributes: + value: "We really value feature requests. Thank you :tada:" + - type: textarea + id: what-happened + attributes: + label: Describe the proposed solution + description: A clear and concise description of what you want to happen + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here + validations: + required: false diff --git a/.github/workflows/backwards-compatibility-check.yaml b/.github/workflows/backwards-compatibility-check.yaml new file mode 100644 index 0000000..ff29dee --- /dev/null +++ b/.github/workflows/backwards-compatibility-check.yaml @@ -0,0 +1,40 @@ +# See https://github.com/Roave/BackwardCompatibilityCheck + +name: "Backwards Compatibility Check" + +on: + pull_request: ~ + +jobs: + backwards-compatibility-check: + name: "Backwards Compatibility Check" + + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + with: + fetch-depth: 0 + + - name: "Setup PHP, with composer and extensions" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.1" + coverage: "none" + + # The base branch may pin dependencies (e.g. api-platform/core) to versions that have + # since been flagged by security advisories, which would otherwise block the comparison + # install. The plugin's own composer.json sets audit.block-insecure for the head side, but + # the (immutable) base branch needs this set globally. Can be removed once the base carries it. + - name: "Allow installing advisory-affected dependencies for the comparison" + run: "composer config --global audit.block-insecure false" + + - name: "Install tool" + run: "composer global require roave/backward-compatibility-check" + + # --install-development-dependencies is required because the plugin's Sylius dependencies + # (e.g. the SyliusPluginTrait / AbstractFixture parents) come from dev dependencies; without + # it Roave cannot reflect those parents and reports false "could not be found" breaks. + - name: "Check for BC breaks" + run: "~/.composer/vendor/bin/roave-backward-compatibility-check --from=origin/${{ github.event.pull_request.base.ref }} --format=github-actions --install-development-dependencies" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4bba9c3..f77c4bc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,9 @@ name: "build" on: push: branches: + - "1.*.x" # Matches our branch versioning that mirrors Sylius', e.g. '1.12.x' - "master" + - "main" paths-ignore: - "**/*.md" pull_request: @@ -31,7 +33,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v5" - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" @@ -41,7 +43,7 @@ jobs: coverage: "none" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependencies }}" @@ -73,17 +75,18 @@ jobs: php-version: - "8.1" - "8.2" + - "8.3" dependencies: + - "lowest" - "highest" - + symfony: - - "^5.4" - - "^6.0" + - "~6.4.0" steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v5" - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" @@ -91,24 +94,24 @@ jobs: coverage: "none" extensions: "${{ env.PHP_EXTENSIONS }}" php-version: "${{ matrix.php-version }}" - tools: "composer-require-checker, composer-unused, flex" + tools: "flex" + + - name: "Remove require-dev section in composer.json" + run: "composer config --unset require-dev" + + - name: "Add shipmonk/composer-dependency-analyser to composer.json" + run: "composer require --dev --no-install --no-update --no-plugins --no-scripts shipmonk/composer-dependency-analyser" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" env: SYMFONY_REQUIRE: "${{ matrix.symfony }}" with: dependency-versions: "${{ matrix.dependencies }}" - - name: "Run maglnet/composer-require-checker" - run: "composer-require-checker check --config-file=$(pwd)/composer-require-checker.json" + - name: "Run dependency analysis" + run: "vendor/bin/composer-dependency-analyser" - - name: "Run composer-unused/composer-unused" - run: "composer-unused" - - - name: "Run composer audit" - run: "composer audit" - static-code-analysis: name: "Static Code Analysis (PHP${{ matrix.php-version }} | Deps: ${{ matrix.dependencies }} | SF${{ matrix.symfony }})" @@ -119,17 +122,18 @@ jobs: php-version: - "8.1" - "8.2" + - "8.3" dependencies: + - "lowest" - "highest" symfony: - - "^5.4" - - "^6.0" + - "~6.4.0" steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v5" - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" @@ -139,18 +143,18 @@ jobs: php-version: "${{ matrix.php-version }}" tools: "flex" + - name: "Remove sylius/sylius from composer.json" + run: "composer remove --dev --no-install --no-update --no-plugins --no-scripts sylius/sylius" + - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" env: SYMFONY_REQUIRE: "${{ matrix.symfony }}" with: dependency-versions: "${{ matrix.dependencies }}" - - name: "Cache warmup" - run: "(cd tests/Application && bin/console cache:warmup)" - - name: "Static analysis" - run: "vendor/bin/psalm --php-version=${{ matrix.php-version }}" + run: "composer analyse" unit-tests: name: "Unit tests (PHP${{ matrix.php-version }} | Deps: ${{ matrix.dependencies }} | SF${{ matrix.symfony }})" @@ -162,17 +166,18 @@ jobs: php-version: - "8.1" - "8.2" - + - "8.3" + dependencies: + - "lowest" - "highest" symfony: - - "^5.4" - - "^6.0" + - "~6.4.0" steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v5" - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" @@ -183,14 +188,14 @@ jobs: tools: "flex" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" env: SYMFONY_REQUIRE: "${{ matrix.symfony }}" with: dependency-versions: "${{ matrix.dependencies }}" - - name: "Run phpspec" - run: "composer phpspec" + - name: "Run phpunit" + run: "composer phpunit" integration-tests: name: "Integration tests (PHP${{ matrix.php-version }} | Deps: ${{ matrix.dependencies }} | SF${{ matrix.symfony }})" @@ -202,20 +207,21 @@ jobs: php-version: - "8.1" - "8.2" + - "8.3" dependencies: + - "lowest" - "highest" - + symfony: - - "^5.4" - - "^6.0" + - "~6.4.0" steps: - name: "Start MySQL" run: "sudo /etc/init.d/mysql start" - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v5" - name: "Setup PHP, with composer and extensions" uses: "shivammathur/setup-php@v2" @@ -226,14 +232,14 @@ jobs: tools: "flex" - name: "Install composer dependencies" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" env: SYMFONY_REQUIRE: "${{ matrix.symfony }}" with: dependency-versions: "${{ matrix.dependencies }}" - name: "Lint container" - run: '(cd tests/Application && bin/console lint:container)' + run: "(cd tests/Application && bin/console lint:container)" - name: "Create database" run: "(cd tests/Application && bin/console doctrine:database:create)" @@ -242,4 +248,75 @@ jobs: run: "(cd tests/Application && bin/console doctrine:schema:create)" - name: "Validate Doctrine mapping" - run: "(cd tests/Application && bin/console doctrine:schema:validate)" + run: "(cd tests/Application && bin/console doctrine:schema:validate -vvv)" # The verbose flag will show 'missing' SQL statements, if any + + mutation-tests: + name: "Mutation tests" + + runs-on: "ubuntu-latest" + + strategy: + matrix: + php-version: + - "8.3" + + dependencies: + - "highest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v5" + + - name: "Setup PHP, with composer and extensions" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + extensions: "${{ env.PHP_EXTENSIONS }}" + php-version: "${{ matrix.php-version }}" + + - name: "Install composer dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "${{ matrix.dependencies }}" + + - name: "Run infection" + run: "vendor/bin/infection" + env: + STRYKER_DASHBOARD_API_KEY: "${{ secrets.STRYKER_DASHBOARD_API_KEY }}" + + code-coverage: + name: "Code Coverage (PHP${{ matrix.php-version }} | Deps: ${{ matrix.dependencies }})" + + runs-on: "ubuntu-latest" + + strategy: + matrix: + php-version: + - "8.3" + + dependencies: + - "highest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v5" + + - name: "Setup PHP, with composer and extensions" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + extensions: "${{ env.PHP_EXTENSIONS }}" + php-version: "${{ matrix.php-version }}" + + - name: "Install composer dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "${{ matrix.dependencies }}" + + - name: "Collect code coverage with pcov and phpunit/phpunit" + run: "vendor/bin/phpunit --coverage-clover=.build/logs/clover.xml" + + - name: "Send code coverage report to Codecov.io" + uses: "codecov/codecov-action@v4" + with: + token: "${{ secrets.CODECOV_TOKEN }}" diff --git a/.gitignore b/.gitignore index 62bf063..4c0aa2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ +/.build/ /vendor/ -/node_modules +/node_modules/ /composer.lock /tests/Application/yarn.lock -/phpspec.yml /.phpunit.result.cache + +# Symfony CLI https://symfony.com/doc/current/setup/symfony_server.html#different-php-settings-per-project +/.php-version +/php.ini diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d6bbc3e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A Sylius plugin that adds **QuickPay** (a Danish payment gateway, with Klarna support) as a payment method. +It is a thin Sylius/Payum integration layer on top of the lower-level Payum gateway package +[`setono/payum-quickpay`](https://github.com/Setono/PayumQuickPay) — that package owns the actual QuickPay API +client and models (`QuickPayPayment`, `QuickPayPaymentOperation`, `ConfirmPayment`, etc.); this package wires +it into Sylius's checkout, state machine, and admin. + +Targets PHP 8.1+, Symfony ^6.4, Sylius ~1.14. The active development branch is `1.x` (also the default/PR base). + +All dev tooling (PHPStan, PHPUnit, Rector, Infection, ECS, composer-dependency-analyser, composer-normalize) is +delegated to the **`setono/sylius-plugin-pack`** meta-package — it is not listed package-by-package in +`require-dev`. The setup mirrors [`Setono/SyliusPluginSkeleton@1.14.x`](https://github.com/Setono/SyliusPluginSkeleton/tree/1.14.x). + +## Commands + +```bash +composer phpunit # run the PHPUnit test suite (tests/) +composer analyse # PHPStan (level max) — boots the test app via tests/PHPStan/*.php loaders +composer check-style # ECS dry-run (sylius-labs coding standard) +composer fix-style # ECS auto-fix +vendor/bin/phpunit --filter CountryCurrencyMatcherTest # run a single test +vendor/bin/rector process --dry-run # Rector (UP_TO_PHP_81), dry-run +vendor/bin/composer-dependency-analyser # unused/undeclared dependency check +``` + +CI (`.github/workflows/build.yaml`) is the bar a change must pass — jobs: coding-standards +(`composer validate --strict`, `composer normalize --dry-run`, `composer check-style`, +`vendor/bin/rector process --dry-run`, `lint:yaml`, `lint:twig`), dependency-analysis (composer-dependency-analyser), +static-code-analysis (`composer analyse`), unit-tests (`composer phpunit`), integration-tests +(`lint:container`, `doctrine:schema:create`, `doctrine:schema:validate -vvv`), mutation-tests (Infection) and +code-coverage (Codecov). Matrix: PHP 8.1–8.3 × Symfony `~6.4.0` × deps lowest/highest. A separate +`backwards-compatibility-check.yaml` runs Roave BC-check on PRs. + +PHPStan runs at `level: max` with a `phpstan-baseline.neon` capturing the pre-existing `src/` issues inherited from +the old Psalm setup — prefer fixing an issue over leaving it baselined, and shrink the baseline over time. + +## Test application + +`tests/Application/` is a full, bootable Sylius 1.14 app (structure mirrors the skeleton) used as the host for +static analysis, PHPUnit bootstrapping, and integration testing. PHPStan boots it through +`tests/PHPStan/console_application.php` (Symfony) and `tests/PHPStan/object_manager.php` (Doctrine); PHPUnit +bootstraps from `tests/Application/config/bootstrap.php`. The plugin is wired into the app via +`config/packages/setono_sylius_quickpay.yaml` (imports the plugin's `app/config.yaml` + `app/fixtures.yaml`), +`config/routes/setono_sylius_quickpay.yaml`, and `config/validator/Address.xml` (Klarna constraint, see below). + +Run any Symfony console command for the plugin from inside that directory, e.g. +`(cd tests/Application && bin/console debug:container setono_sylius_quickpay)`. For a manual run: +`(cd tests/Application && yarn install && yarn build && bin/console doctrine:database:create && bin/console doctrine:schema:create && bin/console sylius:fixtures:load -n)`. + +## Architecture + +The payment lifecycle is implemented with **Payum's action pattern**. The three actions in `src/Action/` are each +registered in `src/Resources/config/services.xml` with ``, which binds +them to the QuickPay gateway. They use `ApiAwareTrait`/`GatewayAwareTrait` from `setono/payum-quickpay`. + +- **`ConvertPaymentAction`** — turns a Sylius payment into the QuickPay request array (amount, basket, addresses, + callback/continue/cancel URLs). On first run it calls the API to create the QuickPay payment and stashes + `quickpayPayment` + `quickpayPaymentId` into the Sylius payment's `details`. It also mints a Payum *notify token* + whose target URL becomes the QuickPay `callback_url`. +- **`StatusAction`** — maps QuickPay payment state + latest operation onto Payum's status markers + (`markAuthorized`/`markCaptured`/`markRefunded`/`markCanceled`). Default is `markNew` so the payment can be reused + for further operations. +- **`Action/NotifyAction`** (Payum action) — validates the `quickpay-checksum-sha256` header against the raw body, + parses the callback JSON into a `QuickPayPayment`, writes it into the payment details, and dispatches `ConfirmPayment`. + +### Two NotifyActions — don't confuse them +- `src/Controller/NotifyAction.php` is the **HTTP entry point** (route `setono_sylius_quickpay_notify` → + `POST /payment/quickpay/notify`, in `src/Resources/config/routing.yaml`). It receives the raw QuickPay server + callback, resolves the order by `order_id`, finds the matching payment by `quickpayPaymentId`, then dispatches the + Payum `Notify` request. +- `src/Action/NotifyAction.php` is the **Payum action** that actually handles that `Notify` request (checksum + confirm). + +The controller strips `QUICKPAY_ORDER_PREFIX` from the incoming `order_id` to recover the Sylius order number — this +prefix handling is the source of several documented "order_id" troubleshooting cases (see README). + +### State machine integration +`src/Resources/config/app/config.yaml` registers a `winzou_state_machine` **before** callback on the +`sylius_payment` machine for the `complete`, `refund`, and `cancel` transitions, invoking +`StateMachine/PaymentProcessor`. That processor translates each transition into the corresponding Payum request +(`Capture`/`Refund`/`Cancel`) against the gateway, but only when the payment actually has a `quickpayPaymentId` and +the operation hasn't already been approved. Each operation can be turned off via the plugin config +`disable_capture` / `disable_refund` / `disable_cancel` (defined in `DependencyInjection/Configuration.php`, passed +to the processor as container parameters). This config file must be imported by the host app (see README install steps). + +### Klarna-specific pieces +QuickPay's Klarna requires structured data that plain Sylius addresses don't provide: +- `Action/ConvertPaymentAction::convertAddress()` splits a one-line street into street + house number for `DE`/`NL` + using `viison/address-splitter`. +- `Validator/Constraints/AddressStreetEligibility` + `AddressStreetEligibilityValidator` + `Checker/StreetEligibilityChecker` + enforce that an address street can be split — the host app must opt in by registering the constraint on + `Sylius\Component\Addressing\Model\Address` (see README step 5 and `tests/Application/config/validator/Address.xml`). +- `Klarna/Matcher/CountryCurrencyMatcher` validates country↔currency pairs supported by QuickPay's acquirer. +- `Fixture/KlarnaTestShopUserFixture` seeds a test user/address for manual Klarna testing. + +### Gateway config & language +- `Form/Type/QuickPayGatewayConfigurationType` is the admin form for the gateway (tagged + `sylius.gateway_configuration_type` type `quickpay`). +- `FactoryBuilder/QuickpayGatewayFactoryBuilder` injects a guessed UI `language` into the gateway default config at + build time; `Guesser/QuickpayLanguageGuesser` derives it from Sylius's locale context (mapping `nb`/`nn` → `no`). + +## Conventions specific to this repo + +- The plugin reads runtime config from **env vars** (notably `QUICKPAY_ORDER_PREFIX`, plus the API credentials used + by `setono/payum-quickpay`). The order prefix must be unique per project/environment — re-used prefixes cause the + QuickPay "order_id already exists" / length errors documented in the README. +- New Payum behavior = a new class in `src/Action/` tagged `payum.action factory="quickpay"` in `services.xml`. + Services are wired explicitly in `services.xml` (no autowiring/autoconfiguration in this bundle). +- `composer.lock` is gitignored — this is a plugin, so no lockfile is committed. +- The plugin's `src/` still uses the **winzou** state machine (`SM\Event\TransitionEvent` + the + `winzou_state_machine` callback in `app/config.yaml`). Sylius 1.14 ships `SyliusStateMachineAbstractionBundle` and + also supports the `symfony_workflow` adapter; under that adapter the winzou callback would not fire. Migrating to + `Sylius\Abstraction\StateMachine` is a known follow-up. diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php new file mode 100644 index 0000000..8fccf73 --- /dev/null +++ b/composer-dependency-analyser.php @@ -0,0 +1,9 @@ +addPathToExclude(__DIR__ . '/tests') +; diff --git a/composer-require-checker.json b/composer-require-checker.json deleted file mode 100644 index c2299ec..0000000 --- a/composer-require-checker.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "symbol-whitelist": [ - "array", - "bool", - "callable", - "false", - "float", - "int", - "iterable", - "null", - "object", - "parent", - "self", - "static", - "string", - "true", - "void", - "Sylius\\Bundle\\CoreBundle\\Application\\SyliusPluginTrait", - "Sylius\\Bundle\\CoreBundle\\Fixture\\Factory\\ExampleFactoryInterface", - "Sylius\\Bundle\\FixturesBundle\\Fixture\\AbstractFixture", - "Sylius\\Component\\Addressing\\Model\\AddressInterface", - "Sylius\\Component\\Core\\Model\\AddressInterface", - "Sylius\\Component\\Core\\Model\\CustomerInterface", - "Sylius\\Component\\Core\\Model\\OrderInterface", - "Sylius\\Component\\Core\\Model\\PaymentInterface", - "Sylius\\Component\\Core\\Model\\ProductVariantInterface", - "Sylius\\Component\\Core\\OrderPaymentTransitions", - "Sylius\\Component\\Core\\Repository\\OrderRepositoryInterface", - "Sylius\\Component\\Locale\\Context\\LocaleContextInterface", - "Sylius\\Component\\Locale\\Context\\LocaleNotFoundException", - "Sylius\\Component\\Order\\Model\\OrderInterface", - "Sylius\\Component\\Order\\StateResolver\\StateResolverInterface", - "Sylius\\Component\\Payment\\PaymentTransitions", - "Faker\\Factory" - ] -} diff --git a/composer.json b/composer.json index 5f128fb..9cd1dbb 100644 --- a/composer.json +++ b/composer.json @@ -16,40 +16,46 @@ "php": ">=8.1", "ext-json": "*", "ext-mbstring": "*", - "doctrine/collections": "^1.6", - "doctrine/orm": "^2.7", + "doctrine/collections": "^1.6 || ^2.0", + "doctrine/orm": "^2.7 || ^3.0", "fakerphp/faker": "^1.21", "payum/payum": "^1.6", - "setono/payum-quickpay": "^1.3", - "symfony/config": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/http-foundation": "^5.4 || ^6.0", - "symfony/http-kernel": "^5.4 || ^6.0", - "symfony/intl": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0", + "setono/payum-quickpay": "^1.5", + "sylius/addressing": "^1.0", + "sylius/core": "^1.0", + "sylius/core-bundle": "^1.0", + "sylius/fixtures-bundle": "^1.0", + "sylius/locale": "^1.0", + "sylius/payment": "^1.0", + "symfony/config": "^6.4", + "symfony/dependency-injection": "^6.4", + "symfony/form": "^6.4", + "symfony/http-foundation": "^6.4", + "symfony/http-kernel": "^6.4", + "symfony/intl": "^6.4", + "symfony/validator": "^6.4", "viison/address-splitter": "^0.3.4", "webmozart/assert": "^1.11", "winzou/state-machine": "^0.3 || ^0.4" }, "require-dev": { - "api-platform/core": "^2.7", - "behat/behat": "^3.12", - "lexik/jwt-authentication-bundle": "^2.16", - "matthiasnoback/symfony-config-test": "^4.3", - "matthiasnoback/symfony-dependency-injection-test": "^4.3", - "phpspec/phpspec": "^7.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-phpunit": "^0.18", - "psalm/plugin-symfony": "^5.0", - "setono/code-quality-pack": "^2.4", - "sylius/sylius": "~1.12.3", - "symfony/debug-bundle": "^5.4 || ^6.0", - "symfony/dotenv": "^5.4 || ^6.0", - "symfony/property-info": "^5.4 || ^6.0", - "symfony/serializer": "^5.4 || ^6.0", - "symfony/web-profiler-bundle": "^5.4 || ^6.0", - "symfony/webpack-encore-bundle": "^1.16" + "api-platform/core": "^2.7.16", + "babdev/pagerfanta-bundle": "^3.8", + "behat/behat": "^3.14", + "doctrine/doctrine-bundle": "^2.11", + "jms/serializer-bundle": "^4.2", + "lexik/jwt-authentication-bundle": "^2.17", + "matthiasnoback/symfony-config-test": "^5.1", + "setono/sylius-plugin-pack": "~1.14.1", + "shipmonk/composer-dependency-analyser": "^1.6", + "sylius-labs/polyfill-symfony-security": "^1.1.2", + "symfony/debug-bundle": "^6.4", + "symfony/dotenv": "^6.4", + "symfony/property-info": "^6.4", + "symfony/serializer": "^6.4", + "symfony/web-profiler-bundle": "^6.4", + "symfony/webpack-encore-bundle": "^1.17.2", + "willdurand/negotiation": "^3.1" }, "prefer-stable": true, "autoload": { @@ -59,7 +65,7 @@ }, "autoload-dev": { "psr-4": { - "Tests\\Setono\\SyliusQuickpayPlugin\\": "tests/" + "Setono\\SyliusQuickpayPlugin\\Tests\\": "tests/" }, "classmap": [ "tests/Application/Kernel.php" @@ -67,20 +73,21 @@ }, "config": { "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": false, + "dealerdirect/phpcodesniffer-composer-installer": true, "ergebnis/composer-normalize": true, + "infection/extension-installer": true, + "phpstan/extension-installer": true, "symfony/thanks": false }, + "audit": { + "block-insecure": false + }, "sort-packages": true }, "scripts": { - "analyse": [ - "@ensure-test-container-exists", - "psalm" - ], + "analyse": "phpstan analyse", "check-style": "ecs check", - "ensure-test-container-exists": "[[ -f tests/Application/var/cache/test/Tests_Setono_SyliusQuickpayPlugin_Application_KernelTestDebugContainer.xml ]] || tests/Application/bin/console cache:warmup --env=test", "fix-style": "ecs check --fix", - "phpspec": "phpspec run" + "phpunit": "phpunit" } } diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000..c1ded53 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,16 @@ +{ + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "php://stderr", + "github": true, + "stryker": { + "badge": "1.x" + } + }, + "minMsi": 0, + "minCoveredMsi": 0 +} diff --git a/phpspec.yml.dist b/phpspec.yml.dist deleted file mode 100644 index 6d4af44..0000000 --- a/phpspec.yml.dist +++ /dev/null @@ -1,4 +0,0 @@ -suites: - main: - namespace: Setono\SyliusQuickpayPlugin - psr4_prefix: Setono\SyliusQuickpayPlugin diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..d04e70d --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,199 @@ +parameters: + ignoreErrors: + - + message: '#^Cannot access offset ''amount'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Action\\ConvertPaymentAction\:\:convertAddress\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Action\\ConvertPaymentAction\:\:convertOrderItems\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Action\\ConvertPaymentAction\:\:getRelatedOrderDetails\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Offset ''base'' does not exist on string\.$#' + identifier: offsetAccess.notFound + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Offset ''extension'' does not exist on string\.$#' + identifier: offsetAccess.notFound + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Parameter \#1 \$request \(Payum\\Core\\Request\\Convert\) of method Setono\\SyliusQuickpayPlugin\\Action\\ConvertPaymentAction\:\:execute\(\) should be contravariant with parameter \$request \(mixed\) of method Payum\\Core\\Action\\ActionInterface\:\:execute\(\)$#' + identifier: method.childParameterType + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Strict comparison using \=\=\= between null and mixed will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: src/Action/ConvertPaymentAction.php + + - + message: '#^Access to an undefined property Payum\\Core\\Request\\GetHttpRequest\:\:\$headers\.$#' + identifier: property.notFound + count: 1 + path: src/Action/NotifyAction.php + + - + message: '#^Cannot access offset ''quickpay\-checksum\-sha256'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Action/NotifyAction.php + + - + message: '#^Cannot access offset 0 on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Action/NotifyAction.php + + - + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string + count: 1 + path: src/Action/NotifyAction.php + + - + message: '#^Parameter \#1 \$request \(Payum\\Core\\Request\\Notify\) of method Setono\\SyliusQuickpayPlugin\\Action\\NotifyAction\:\:execute\(\) should be contravariant with parameter \$request \(mixed\) of method Payum\\Core\\Action\\ActionInterface\:\:execute\(\)$#' + identifier: method.childParameterType + count: 1 + path: src/Action/NotifyAction.php + + - + message: '#^Parameter \#1 \$request \(Payum\\Core\\Request\\GetStatusInterface\) of method Setono\\SyliusQuickpayPlugin\\Action\\StatusAction\:\:execute\(\) should be contravariant with parameter \$request \(mixed\) of method Payum\\Core\\Action\\ActionInterface\:\:execute\(\)$#' + identifier: method.childParameterType + count: 1 + path: src/Action/StatusAction.php + + - + message: '#^Cannot access offset ''base'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Checker/StreetEligibilityChecker.php + + - + message: '#^Cannot access offset ''extension'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Checker/StreetEligibilityChecker.php + + - + message: '#^Cannot cast mixed to int\.$#' + identifier: cast.int + count: 2 + path: src/Controller/NotifyAction.php + + - + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string + count: 1 + path: src/Controller/NotifyAction.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Controller\\NotifyAction\:\:__construct\(\) has parameter \$orderRepository with generic interface Sylius\\Component\\Core\\Repository\\OrderRepositoryInterface but does not specify its types\: T$#' + identifier: missingType.generics + count: 1 + path: src/Controller/NotifyAction.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\FactoryBuilder\\QuickpayGatewayFactoryBuilder\:\:build\(\) has parameter \$defaultConfig with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/FactoryBuilder/QuickpayGatewayFactoryBuilder.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:getKlarnaTestDataByCountry\(\) has parameter \$options with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:getKlarnaTestDataByCountry\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:getOptions\(\) has parameter \$requiredKeys with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:getOptions\(\) has parameter \$testData with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:getOptions\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:getSupportedCountries\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:load\(\) has parameter \$options with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Parameter \#1 \$countryCode of method Setono\\SyliusQuickpayPlugin\\Fixture\\KlarnaTestShopUserFixture\:\:getKlarnaTestDataByCountry\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Regex pattern is invalid\: No ending delimiter ''@'' found in pattern\: @$#' + identifier: regexp.pattern + count: 1 + path: src/Fixture/KlarnaTestShopUserFixture.php + + - + message: '#^Class Setono\\SyliusQuickpayPlugin\\Form\\Type\\QuickPayGatewayConfigurationType extends generic class Symfony\\Component\\Form\\AbstractType but does not specify its types\: TData$#' + identifier: missingType.generics + count: 1 + path: src/Form/Type/QuickPayGatewayConfigurationType.php + + - + message: '#^Method Setono\\SyliusQuickpayPlugin\\Klarna\\Matcher\\CountryCurrencyMatcher\:\:__construct\(\) has parameter \$map with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Klarna/Matcher/CountryCurrencyMatcher.php + + - + message: '#^Property Setono\\SyliusQuickpayPlugin\\Klarna\\Matcher\\CountryCurrencyMatcher\:\:\$map type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Klarna/Matcher/CountryCurrencyMatcher.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..cdea61f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,27 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: max + + paths: + - src + - tests + + excludePaths: + - tests/Application/* + + bootstrapFiles: + - vendor/autoload.php + + # Symfony Configuration + symfony: + consoleApplicationLoader: tests/PHPStan/console_application.php + + # Doctrine Configuration + doctrine: + repositoryClass: Doctrine\ORM\EntityRepository + objectManagerLoader: tests/PHPStan/object_manager.php + + reportUnmatchedIgnoredErrors: false + treatPhpDocTypesAsCertain: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..7cf2924 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + src/ + + + + + tests + + + + + + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml deleted file mode 100644 index a7fb381..0000000 --- a/psalm-baseline.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - payum->getTokenFactory()]]> - - - - - - - - - - - $request - - - ConvertPaymentAction - ConvertPaymentAction - - - notNull - - - - - headers['quickpay-checksum-sha256']]]> - headers['quickpay-checksum-sha256'][0]]]> - - - $request - - - - - $request - - - - - - - - - diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index ed1e7bc..0000000 --- a/psalm.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - tests/Application/var/cache/test/Tests_Setono_SyliusQuickpayPlugin_Application_KernelTestDebugContainer.xml - - - - - - - - - - - - - - diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..968b47e --- /dev/null +++ b/rector.php @@ -0,0 +1,25 @@ +cacheClass(FileCacheStorage::class); + $rectorConfig->cacheDirectory('./.build/rector'); + + $rectorConfig->paths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + $rectorConfig->skip([ + __DIR__ . '/tests/Application', + ]); + + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_81, + ]); +}; diff --git a/spec/Exception/UnsupportedPaymentTransitionExceptionSpec.php b/spec/Exception/UnsupportedPaymentTransitionExceptionSpec.php deleted file mode 100644 index 5cc3429..0000000 --- a/spec/Exception/UnsupportedPaymentTransitionExceptionSpec.php +++ /dev/null @@ -1,21 +0,0 @@ -shouldHaveType(UnsupportedPaymentTransitionException::class); - } - - public function it_is_an_exception_exception(): void - { - $this->shouldHaveType(\Exception::class); - } -} diff --git a/spec/Form/Type/QuickPayGatewayConfigurationTypeSpec.php b/spec/Form/Type/QuickPayGatewayConfigurationTypeSpec.php deleted file mode 100644 index bd43b96..0000000 --- a/spec/Form/Type/QuickPayGatewayConfigurationTypeSpec.php +++ /dev/null @@ -1,30 +0,0 @@ -shouldHaveType(QuickPayGatewayConfigurationType::class); - } - - public function it_is_an_abstract_type_form(): void - { - $this->shouldHaveType(AbstractType::class); - } - - public function it_takes_arguments(FormBuilderInterface $builder): void - { - $builder->add(Argument::type('string'), Argument::type('string'), Argument::any())->willReturn($builder); - $this->buildForm($builder, []); - } -} diff --git a/spec/Klarna/Matcher/CountryCurrencyMatcherSpec.php b/spec/Klarna/Matcher/CountryCurrencyMatcherSpec.php deleted file mode 100644 index 0deebbb..0000000 --- a/spec/Klarna/Matcher/CountryCurrencyMatcherSpec.php +++ /dev/null @@ -1,30 +0,0 @@ -shouldHaveType(CountryCurrencyMatcher::class); - } - - public function it_implements_interface(): void - { - $this->shouldImplement(CountryCurrencyMatcherInterface::class); - } - - public function it_checks_match(): void - { - $this->isMatch('DK', 'DKK')->shouldReturn(true); - $this->isMatch('dk', 'DKK')->shouldReturn(true); - - $this->isMatch('DK', 'EUR')->shouldReturn(false); - } -} diff --git a/src/Action/ConvertPaymentAction.php b/src/Action/ConvertPaymentAction.php index f2fbfbc..0b0a67c 100644 --- a/src/Action/ConvertPaymentAction.php +++ b/src/Action/ConvertPaymentAction.php @@ -36,11 +36,8 @@ class ConvertPaymentAction implements ActionInterface, ApiAwareInterface, Gatewa use GatewayAwareTrait; use ApiAwareTrait; - protected Payum $payum; - - public function __construct(Payum $payum) + public function __construct(protected Payum $payum) { - $this->payum = $payum; } /** @@ -128,7 +125,7 @@ protected function convertAddress(AddressInterface $address, CustomerInterface $ $details['street'] = $splittedStreet['streetName']; $details['house_number'] = $splittedStreet['houseNumber']; - } catch (SplittingException $e) { + } catch (SplittingException) { $details['street'] = $street; $details['house_number'] = ''; } @@ -142,7 +139,7 @@ protected function convertAddress(AddressInterface $address, CustomerInterface $ $details['street'] = $splittedStreet['streetName']; $details['house_number'] = $splittedStreet['houseNumberParts']['base']; $details['house_extension'] = $splittedStreet['houseNumberParts']['extension']; - } catch (SplittingException $e) { + } catch (SplittingException) { $details['street'] = $street; $details['house_number'] = ''; $details['house_extension'] = ''; @@ -171,7 +168,6 @@ protected function convertAddress(AddressInterface $address, CustomerInterface $ /** * @param Collection|OrderItemInterface[] $items - * * @psalm-param Collection $items */ protected function convertOrderItems(Collection $items): array diff --git a/src/Action/NotifyAction.php b/src/Action/NotifyAction.php index 16bc3f6..a9ed7db 100644 --- a/src/Action/NotifyAction.php +++ b/src/Action/NotifyAction.php @@ -48,7 +48,7 @@ public function execute($request): void * @var stdClass $data */ $data = json_decode($httpRequest->content, false, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { + } catch (JsonException) { throw new BadRequestHttpException(); } diff --git a/src/Checker/StreetEligibilityChecker.php b/src/Checker/StreetEligibilityChecker.php index abcc74a..e9e0ab4 100644 --- a/src/Checker/StreetEligibilityChecker.php +++ b/src/Checker/StreetEligibilityChecker.php @@ -41,7 +41,7 @@ public function isEligible(AddressInterface $address): bool return false; } } - } catch (SplittingException $e) { + } catch (SplittingException) { return false; } diff --git a/src/Controller/NotifyAction.php b/src/Controller/NotifyAction.php index 4e69933..2a363c4 100644 --- a/src/Controller/NotifyAction.php +++ b/src/Controller/NotifyAction.php @@ -20,17 +20,8 @@ */ final class NotifyAction { - private Payum $payum; - - private OrderRepositoryInterface $orderRepository; - - private string $orderPrefix; - - public function __construct(Payum $payum, OrderRepositoryInterface $orderRepository, string $orderPrefix) + public function __construct(private readonly Payum $payum, private readonly OrderRepositoryInterface $orderRepository, private readonly string $orderPrefix) { - $this->payum = $payum; - $this->orderRepository = $orderRepository; - $this->orderPrefix = $orderPrefix; } public function __invoke(Request $request): Response @@ -49,7 +40,7 @@ public function __invoke(Request $request): Response * @var \stdClass $data */ $data = json_decode($request->getContent(), false, 512, \JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { + } catch (\JsonException) { throw new BadRequestHttpException(); } diff --git a/src/FactoryBuilder/QuickpayGatewayFactoryBuilder.php b/src/FactoryBuilder/QuickpayGatewayFactoryBuilder.php index 8a52f30..9a86e18 100644 --- a/src/FactoryBuilder/QuickpayGatewayFactoryBuilder.php +++ b/src/FactoryBuilder/QuickpayGatewayFactoryBuilder.php @@ -10,13 +10,9 @@ class QuickpayGatewayFactoryBuilder extends GatewayFactoryBuilder { - protected QuickpayLanguageGuesserInterface $languageGuesser; - - public function __construct(string $gatewayFactoryClass, QuickpayLanguageGuesserInterface $languageGuesser) + public function __construct(string $gatewayFactoryClass, protected QuickpayLanguageGuesserInterface $languageGuesser) { parent::__construct($gatewayFactoryClass); - - $this->languageGuesser = $languageGuesser; } public function build(array $defaultConfig, GatewayFactoryInterface $coreGatewayFactory): GatewayFactoryInterface diff --git a/src/Fixture/KlarnaTestShopUserFixture.php b/src/Fixture/KlarnaTestShopUserFixture.php index 78cf8d3..eddf611 100644 --- a/src/Fixture/KlarnaTestShopUserFixture.php +++ b/src/Fixture/KlarnaTestShopUserFixture.php @@ -18,23 +18,13 @@ class KlarnaTestShopUserFixture extends AbstractFixture { - protected ExampleFactoryInterface $shopUserExampleFactory; - - protected ExampleFactoryInterface $addressExampleFactory; - - protected EntityManagerInterface $shopUserManager; - - private Generator $faker; + private readonly Generator $faker; public function __construct( - ExampleFactoryInterface $shopUserExampleFactory, - ExampleFactoryInterface $addressExampleFactory, - EntityManagerInterface $shopUserManager, + protected ExampleFactoryInterface $shopUserExampleFactory, + protected ExampleFactoryInterface $addressExampleFactory, + protected EntityManagerInterface $shopUserManager, ) { - $this->shopUserExampleFactory = $shopUserExampleFactory; - $this->addressExampleFactory = $addressExampleFactory; - $this->shopUserManager = $shopUserManager; - $this->faker = Factory::create(); } diff --git a/src/Guesser/QuickpayLanguageGuesser.php b/src/Guesser/QuickpayLanguageGuesser.php index 1fcfa64..7ec9461 100644 --- a/src/Guesser/QuickpayLanguageGuesser.php +++ b/src/Guesser/QuickpayLanguageGuesser.php @@ -21,11 +21,8 @@ class QuickpayLanguageGuesser implements QuickpayLanguageGuesserInterface 'nn' => 'no', ]; - protected LocaleContextInterface $localeContext; - - public function __construct(LocaleContextInterface $localeContext) + public function __construct(protected LocaleContextInterface $localeContext) { - $this->localeContext = $localeContext; } public function guess(): string diff --git a/src/StateMachine/PaymentProcessor.php b/src/StateMachine/PaymentProcessor.php index 2893c6e..760feab 100644 --- a/src/StateMachine/PaymentProcessor.php +++ b/src/StateMachine/PaymentProcessor.php @@ -17,20 +17,8 @@ final class PaymentProcessor { - private Payum $payum; - - private bool $disableCapture; - - private bool $disableRefund; - - private bool $disableCancel; - - public function __construct(Payum $payum, bool $disableCapture, bool $disableRefund, bool $disableCancel) + public function __construct(private readonly Payum $payum, private readonly bool $disableCapture, private readonly bool $disableRefund, private readonly bool $disableCancel) { - $this->payum = $payum; - $this->disableCapture = $disableCapture; - $this->disableRefund = $disableRefund; - $this->disableCancel = $disableCancel; } public function __invoke(PaymentInterface $payment, TransitionEvent $event): void diff --git a/src/Validator/AddressStreetEligibilityValidator.php b/src/Validator/AddressStreetEligibilityValidator.php index aa8ca6f..6d70cfa 100644 --- a/src/Validator/AddressStreetEligibilityValidator.php +++ b/src/Validator/AddressStreetEligibilityValidator.php @@ -13,11 +13,8 @@ final class AddressStreetEligibilityValidator extends ConstraintValidator { - private StreetEligibilityCheckerInterface $streetEligibilityChecker; - - public function __construct(StreetEligibilityCheckerInterface $streetEligibilityChecker) + public function __construct(private readonly StreetEligibilityCheckerInterface $streetEligibilityChecker) { - $this->streetEligibilityChecker = $streetEligibilityChecker; } /** diff --git a/tests/Application/.env b/tests/Application/.env index 27eb02c..03d2088 100644 --- a/tests/Application/.env +++ b/tests/Application/.env @@ -23,13 +23,13 @@ APP_SECRET=EDITME # Format described at https://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For a sqlite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Set "serverVersion" to your server version to avoid edge-case exceptions and extra database calls -DATABASE_URL=mysql://root@127.0.0.1/setono_sylius_quickpay_%kernel.environment%?serverVersion=5.7 +DATABASE_URL=mysql://root@127.0.0.1/setono_sylius_quickpay_%kernel.environment%?serverVersion=8.0 ###< doctrine/doctrine-bundle ### ###> lexik/jwt-authentication-bundle ### JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem -JWT_PASSPHRASE=acme_plugin_development +JWT_PASSPHRASE=setono_plugin_development ###< lexik/jwt-authentication-bundle ### ###> symfony/messenger ### @@ -39,40 +39,14 @@ MESSENGER_TRANSPORT_DSN=doctrine://default # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages ###< symfony/messenger ### -###> symfony/swiftmailer-bundle ### -# For Gmail as a transport, use: "gmail://username:password@localhost" -# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" -# Delivery is disabled by default via "null://localhost" -MAILER_DSN=smtp://localhost -###< symfony/swiftmailer-bundle ### +###> symfony/mailer ### +MAILER_DSN=null://localhost +###< symfony/mailer ### ###> setono/sylius-quickpay-plugin ### -# Register at https://manage.quickpay.net/ - -# Create custom integration -# Go to Settings > Integration > Default payment settings -# - Check Allow test transactions -# Go to Settings > Integration > Integration -# - Copy Merchant id -# - Click on Private Key button and copy QUICKPAY_PRIVATE_KEY= QUICKPAY_MERCHANT_ID= - -# Then, go to Settings > Users > System users > API User -# - Copy API User's Agreement id (NOT Payment Window's Agreement id) -# - Press See API key to copy API User's Api key QUICKPAY_API_KEY= QUICKPAY_AGREEMENT_ID= - QUICKPAY_ORDER_PREFIX=qp_ -# For non-prod environment, put unique project name -# and update date whenever you recreate dev database -# to be sure it unique: -# QUICKPAY_ORDER_PREFIX= `__` -# For example: -# QUICKPAY_ORDER_PREFIX= `qp_190928_` -# Make sure your prefix not longer than 11 chars -# as it will be contatenated with 9 chars of sylius -# order number (e.g. 000000023) and payum have limit -# of 20 chars for order_id ###< setono/sylius-quickpay-plugin ### diff --git a/tests/Application/.env.test b/tests/Application/.env.test index 05ebdbc..8862d83 100644 --- a/tests/Application/.env.test +++ b/tests/Application/.env.test @@ -1,3 +1,3 @@ APP_SECRET='ch4mb3r0f5ecr3ts' -KERNEL_CLASS='Tests\Setono\SyliusQuickpayPlugin\Application\Kernel' +KERNEL_CLASS='Setono\SyliusQuickpayPlugin\Tests\Application\Kernel' diff --git a/tests/Application/Kernel.php b/tests/Application/Kernel.php index ae610d4..7663f63 100644 --- a/tests/Application/Kernel.php +++ b/tests/Application/Kernel.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Setono\SyliusQuickpayPlugin\Application; +namespace Setono\SyliusQuickpayPlugin\Tests\Application; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\HttpKernel\Bundle\BundleInterface; diff --git a/tests/Application/bin/console b/tests/Application/bin/console index 2be0efc..aa22308 100755 --- a/tests/Application/bin/console +++ b/tests/Application/bin/console @@ -1,7 +1,7 @@ #!/usr/bin/env php ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true], + Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true], Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true], Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true], @@ -38,10 +40,10 @@ FOS\RestBundle\FOSRestBundle::class => ['all' => true], Knp\Bundle\GaufretteBundle\KnpGaufretteBundle::class => ['all' => true], Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true], - League\FlysystemBundle\FlysystemBundle::class => ['all' => true], Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true], Payum\Bundle\PayumBundle\PayumBundle::class => ['all' => true], Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], + BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Sylius\Bundle\FixturesBundle\SyliusFixturesBundle::class => ['all' => true], Sylius\Bundle\PayumBundle\SyliusPayumBundle::class => ['all' => true], @@ -50,12 +52,12 @@ Sylius\Bundle\ShopBundle\SyliusShopBundle::class => ['all' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], + Sylius\Behat\Application\SyliusTestPlugin\SyliusTestPlugin::class => ['test' => true, 'test_cached' => true], ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], Sylius\Bundle\ApiBundle\SyliusApiBundle::class => ['all' => true], SyliusLabs\DoctrineMigrationsExtraBundle\SyliusLabsDoctrineMigrationsExtraBundle::class => ['all' => true], - BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true], SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle::class => ['all' => true], + League\FlysystemBundle\FlysystemBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], - Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], ]; diff --git a/tests/Application/config/packages/framework.yaml b/tests/Application/config/packages/framework.yaml index 12d0e62..3df2c0a 100644 --- a/tests/Application/config/packages/framework.yaml +++ b/tests/Application/config/packages/framework.yaml @@ -1,5 +1,6 @@ framework: secret: '%env(APP_SECRET)%' + ide: phpstorm form: enabled: true legacy_error_messages: false diff --git a/tests/Application/config/packages/staging/monolog.yaml b/tests/Application/config/packages/staging/monolog.yaml deleted file mode 100644 index 6461211..0000000 --- a/tests/Application/config/packages/staging/monolog.yaml +++ /dev/null @@ -1,10 +0,0 @@ -monolog: - handlers: - main: - type: fingers_crossed - action_level: error - handler: nested - nested: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug diff --git a/tests/Application/config/packages/test_cached/doctrine.yaml b/tests/Application/config/packages/test_cached/doctrine.yaml deleted file mode 100644 index 4952860..0000000 --- a/tests/Application/config/packages/test_cached/doctrine.yaml +++ /dev/null @@ -1,16 +0,0 @@ -doctrine: - orm: - entity_managers: - default: - result_cache_driver: - type: memcached - host: localhost - port: 11211 - query_cache_driver: - type: memcached - host: localhost - port: 11211 - metadata_cache_driver: - type: memcached - host: localhost - port: 11211 diff --git a/tests/Application/config/packages/test_cached/fos_rest.yaml b/tests/Application/config/packages/test_cached/fos_rest.yaml deleted file mode 100644 index 2b4189d..0000000 --- a/tests/Application/config/packages/test_cached/fos_rest.yaml +++ /dev/null @@ -1,3 +0,0 @@ -fos_rest: - exception: - debug: true diff --git a/tests/Application/config/packages/test_cached/framework.yaml b/tests/Application/config/packages/test_cached/framework.yaml deleted file mode 100644 index 76d7e5e..0000000 --- a/tests/Application/config/packages/test_cached/framework.yaml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - test: ~ - session: - storage_id: session.storage.mock_file diff --git a/tests/Application/config/packages/test_cached/monolog.yaml b/tests/Application/config/packages/test_cached/monolog.yaml deleted file mode 100644 index 7e2b9e3..0000000 --- a/tests/Application/config/packages/test_cached/monolog.yaml +++ /dev/null @@ -1,6 +0,0 @@ -monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: error diff --git a/tests/Application/config/packages/test_cached/security.yaml b/tests/Application/config/packages/test_cached/security.yaml deleted file mode 100644 index 21cc377..0000000 --- a/tests/Application/config/packages/test_cached/security.yaml +++ /dev/null @@ -1,3 +0,0 @@ -security: - encoders: - sha512: sha512 diff --git a/tests/Application/config/packages/test_cached/sylius_channel.yaml b/tests/Application/config/packages/test_cached/sylius_channel.yaml deleted file mode 100644 index bab83ef..0000000 --- a/tests/Application/config/packages/test_cached/sylius_channel.yaml +++ /dev/null @@ -1,2 +0,0 @@ -sylius_channel: - debug: true diff --git a/tests/Application/config/packages/test_cached/sylius_theme.yaml b/tests/Application/config/packages/test_cached/sylius_theme.yaml deleted file mode 100644 index 4d34199..0000000 --- a/tests/Application/config/packages/test_cached/sylius_theme.yaml +++ /dev/null @@ -1,3 +0,0 @@ -sylius_theme: - sources: - test: ~ diff --git a/tests/Application/config/packages/test_cached/twig.yaml b/tests/Application/config/packages/test_cached/twig.yaml deleted file mode 100644 index 8c6e0b4..0000000 --- a/tests/Application/config/packages/test_cached/twig.yaml +++ /dev/null @@ -1,2 +0,0 @@ -twig: - strict_variables: true diff --git a/tests/Application/config/packages/twig_extensions.yaml b/tests/Application/config/packages/twig_extensions.yaml deleted file mode 100644 index 157f98b..0000000 --- a/tests/Application/config/packages/twig_extensions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -services: - _defaults: - public: false - autowire: true - autoconfigure: true - - Twig\Extra\Intl\IntlExtension: ~ - diff --git a/tests/Application/config/routes.yaml b/tests/Application/config/routes.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Application/package.json b/tests/Application/package.json index e990ba4..990ef01 100644 --- a/tests/Application/package.json +++ b/tests/Application/package.json @@ -1,30 +1,13 @@ { - "dependencies": { - "chart.js": "^2.9", - "jquery": "^3.6", - "jquery.dirtyforms": "^2.0", - "lightbox2": "^2.9", - "semantic-ui-css": "^2.2", - "slick-carousel": "^1.8" - }, - "devDependencies": { - "@symfony/webpack-encore": "^1.8", - "eslint": "^8.18", - "eslint-config-airbnb-base": "^15.0", - "eslint-import-resolver-babel-module": "^5.3", - "eslint-plugin-import": "^2.26", - "node-sass": "^7.0", - "sass-loader": "^12.0" - }, + "license": "UNLICENSED", "scripts": { - "dev": "encore dev", - "build": "encore production", + "build": "encore dev", + "build:prod": "encore production", + "postinstall": "semantic-ui-css-patch", + "lint": "yarn lint:js", "watch": "encore dev --watch" }, - "repository": { - "type": "git", - "url": "git+https://github.com/Sylius/Sylius.git" - }, - "author": "Paweł Jędrzejewski", - "license": "MIT" + "devDependencies": { + "@sylius-ui/frontend": "^1.0.3" + } } diff --git a/tests/Application/public/index.php b/tests/Application/public/index.php index 4b217ae..56da39b 100644 --- a/tests/Application/public/index.php +++ b/tests/Application/public/index.php @@ -2,9 +2,9 @@ declare(strict_types=1); +use Setono\SyliusQuickpayPlugin\Tests\Application\Kernel; use Symfony\Component\ErrorHandler\Debug; use Symfony\Component\HttpFoundation\Request; -use Tests\Setono\SyliusQuickpayPlugin\Application\Kernel; require dirname(__DIR__) . '/config/bootstrap.php'; @@ -15,7 +15,7 @@ } if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { - Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); + Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); } if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) { diff --git a/tests/Application/public/media/image/.gitignore b/tests/Application/public/media/image/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Application/webpack.config.js b/tests/Application/webpack.config.js index fae1429..0335bfc 100644 --- a/tests/Application/webpack.config.js +++ b/tests/Application/webpack.config.js @@ -9,7 +9,7 @@ const uiBundleResources = path.resolve(syliusBundles, 'UiBundle/Resources/privat Encore .setOutputPath('public/build/shop/') .setPublicPath('/build/shop') - .addEntry('shop-entry', './assets/shop/entry.js') + .addEntry('shop-entry', '../../vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/Resources/private/entry.js') .disableSingleRuntimeChunk() .cleanupOutputBeforeBuild() .enableSourceMaps(!Encore.isProduction()) @@ -29,7 +29,7 @@ Encore.reset(); Encore .setOutputPath('public/build/admin/') .setPublicPath('/build/admin') - .addEntry('admin-entry', './assets/admin/entry.js') + .addEntry('admin-entry', '../../vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/Resources/private/entry.js') .disableSingleRuntimeChunk() .cleanupOutputBeforeBuild() .enableSourceMaps(!Encore.isProduction()) @@ -44,4 +44,6 @@ adminConfig.resolve.alias['sylius/bundle'] = syliusBundles; adminConfig.externals = Object.assign({}, adminConfig.externals, { window: 'window', document: 'document' }); adminConfig.name = 'admin'; +Encore.reset(); + module.exports = [shopConfig, adminConfig]; diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..dd7927a --- /dev/null +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,46 @@ +assertProcessedConfigurationEquals([], [ + 'disable_capture' => false, + 'disable_refund' => false, + 'disable_cancel' => false, + ]); + } + + /** + * @test + */ + public function it_allows_disabling_individual_operations(): void + { + $this->assertProcessedConfigurationEquals([ + ['disable_capture' => true], + ['disable_cancel' => true], + ], [ + 'disable_capture' => true, + 'disable_refund' => false, + 'disable_cancel' => true, + ]); + } +} diff --git a/tests/DependencyInjection/SetonoSyliusQuickpayExtensionTest.php b/tests/DependencyInjection/SetonoSyliusQuickpayExtensionTest.php new file mode 100644 index 0000000..0722c0b --- /dev/null +++ b/tests/DependencyInjection/SetonoSyliusQuickpayExtensionTest.php @@ -0,0 +1,42 @@ +load(); + + $this->assertContainerBuilderHasParameter('setono_sylius_quickpay.disable_capture', false); + $this->assertContainerBuilderHasParameter('setono_sylius_quickpay.disable_refund', false); + $this->assertContainerBuilderHasParameter('setono_sylius_quickpay.disable_cancel', false); + } + + /** + * @test + */ + public function it_registers_the_payment_processor_service(): void + { + $this->load(); + + $this->assertContainerBuilderHasService( + 'setono_sylius_quickpay.state_machine.payment_processor', + ); + } +} diff --git a/tests/Exception/UnsupportedPaymentTransitionExceptionTest.php b/tests/Exception/UnsupportedPaymentTransitionExceptionTest.php new file mode 100644 index 0000000..d42aab3 --- /dev/null +++ b/tests/Exception/UnsupportedPaymentTransitionExceptionTest.php @@ -0,0 +1,22 @@ +expectException(UnsupportedPaymentTransitionException::class); + $this->expectExceptionMessage('unsupported transition'); + + throw new UnsupportedPaymentTransitionException('unsupported transition'); + } +} diff --git a/tests/Form/Type/QuickPayGatewayConfigurationTypeTest.php b/tests/Form/Type/QuickPayGatewayConfigurationTypeTest.php new file mode 100644 index 0000000..4e4a65e --- /dev/null +++ b/tests/Form/Type/QuickPayGatewayConfigurationTypeTest.php @@ -0,0 +1,28 @@ +createMock(FormBuilderInterface::class); + $builder->method('add')->willReturnSelf(); + + $type = new QuickPayGatewayConfigurationType(); + $type->buildForm($builder, []); + + // buildForm chains ->add() for every gateway configuration field; reaching this + // point without an exception proves the whole form definition is wired correctly. + $this->addToAssertionCount(1); + } +} diff --git a/tests/Klarna/Matcher/CountryCurrencyMatcherTest.php b/tests/Klarna/Matcher/CountryCurrencyMatcherTest.php new file mode 100644 index 0000000..d1a3c2e --- /dev/null +++ b/tests/Klarna/Matcher/CountryCurrencyMatcherTest.php @@ -0,0 +1,33 @@ +isMatch('DK', 'DKK')); + self::assertTrue($matcher->isMatch('dk', 'DKK')); + } + + /** + * @test + */ + public function it_does_not_match_unsupported_pairs(): void + { + $matcher = new CountryCurrencyMatcher(); + + self::assertFalse($matcher->isMatch('DK', 'EUR')); + self::assertFalse($matcher->isMatch('XX', 'DKK')); + } +} diff --git a/tests/PHPStan/console_application.php b/tests/PHPStan/console_application.php new file mode 100644 index 0000000..4f9b42a --- /dev/null +++ b/tests/PHPStan/console_application.php @@ -0,0 +1,13 @@ +boot(); + +return new Application($kernel); diff --git a/tests/PHPStan/object_manager.php b/tests/PHPStan/object_manager.php new file mode 100644 index 0000000..a5890c0 --- /dev/null +++ b/tests/PHPStan/object_manager.php @@ -0,0 +1,13 @@ +boot(); + +/** @phpstan-ignore method.notFound,method.nonObject */ +return $kernel->getContainer()->get('doctrine')->getManager();